Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core-version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8.0.36
8.0.38
24 changes: 24 additions & 0 deletions docs/release-notes/v0.3.51/en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
- If you have any questions or suggestions about the app, please contact us through our [QQ group](https://qm.qq.com/q/3queDaIG) or [GitHub Issues](https://github.com/router-for-me/EasyCLIProxyAPI/issues). We will respond as soon as possible.

## Improvements

- Updated gateway configuration for newer Claude Desktop versions. CPA now explicitly uses static credentials and a specified model list to prevent legacy authentication, automatic configuration, or session service settings from overriding the CPA connection.
- Updating Claude Desktop configuration now correctly switches to the local CPA profile. Restoring a backup also restores the previous configuration selection.
- Added recognition of the Fable and Mythos model families in Claude Desktop configuration.

## Fixes

- Fixed incomplete Claude Desktop model alias handling for xAI, Meta, Vertex, and Interactions API key providers. API key models are no longer incorrectly mapped to OAuth channels, which could make models unavailable.
- Fixed aliases still in use being removed when saving configuration again. Valid existing routes are preserved when multiple providers offer the same model ID, preventing unintended provider changes.
- Fixed disabled or excluded API key models being incorrectly accepted through stale OAuth mappings.
- Improved Claude Desktop configuration checks to detect authentication and configuration source conflicts, as well as incomplete model lists.
- Improved credential protection and rollback handling when saving model mappings, preserving provider keys and connection settings.

## Release Workflow

- Restored automatic Miaomiao preview builds and publication. Non-draft PRs authored by Roomen and targeting `miaomiao` are automatically merged after all six platform builds pass. The merged commit is then built and the rolling preview is updated.

## Usage Notes

- After upgrading, save the Claude Desktop configuration again in CPA, then fully quit and reopen Claude Desktop.
- Model aliases must point to an available source model. If the source model is disabled or excluded, adjust its access configuration or select another available model.
24 changes: 24 additions & 0 deletions docs/release-notes/v0.3.51/ja.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
- アプリに関するご質問やご提案は、[QQ グループ](https://qm.qq.com/q/3queDaIG) または [GitHub Issues](https://github.com/router-for-me/EasyCLIProxyAPI/issues) までお寄せください。できる限り速やかに対応します。

## 改善

- 新しい Claude Desktop のゲートウェイ設定に対応しました。静的な認証情報と指定したモデル一覧を明示的に使用し、以前の認証方式、自動設定、セッションサービスの設定によって CPA 接続が上書きされることを防ぎます。
- Claude Desktop の設定更新時に、CPA のローカル設定へ正しく切り替わるようになりました。バックアップの復元時には、以前の設定の選択状態も復元します。
- Claude Desktop の設定で Fable、Mythos モデル系列を認識できるようになりました。

## 修正

- xAI、Meta、Vertex、Interactions の API キー接続における Claude Desktop のモデルエイリアス処理を修正しました。API キーのモデルが誤って OAuth チャネルに割り当てられ、利用できなくなる問題を防ぎます。
- 設定を再保存すると、使用中のモデルエイリアスが削除される問題を修正しました。複数のプロバイダーに同名のモデルがある場合も、有効な既存のルーティングを維持し、意図しないプロバイダーの切り替えを防ぎます。
- 無効化または除外された API キーのモデルが、残存する OAuth マッピングによって誤って受け入れられる問題を修正しました。
- Claude Desktop の設定状態の確認を改善し、認証方式や設定元の競合、モデル一覧の不備を検出できるようにしました。
- モデルマッピングの保存時における認証情報の保護と失敗時のロールバック処理を改善し、プロバイダーのキーと接続設定を保持します。

## リリースフロー

- Miaomiao プレビュー版の自動ビルドと公開を復旧しました。Roomen が作成した `miaomiao` 向けのドラフトではない PR は、全 6 プラットフォームのビルド成功後に自動マージされます。その後、マージ済みのコミットをビルドし、ローリングプレビュー版を更新します。

## 使用上の注意

- アップグレード後は、CPA で Claude Desktop の設定を再保存し、Claude Desktop を完全に終了してから再起動してください。
- モデルエイリアスには、利用可能な元のモデルが必要です。元のモデルが無効化または除外されている場合は、該当する接続設定を調整するか、別の利用可能なモデルを選択してください。
25 changes: 25 additions & 0 deletions docs/release-notes/v0.3.51/zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
- 关于软件,如果您有任何问题或建议,请通过 [QQ群](https://qm.qq.com/q/3queDaIG) 或 [GitHub Issues](https://github.com/router-for-me/EasyCLIProxyAPI/issues) 反馈,我们会尽快处理。

## 优化

- 适配新版 Claude Desktop 的网关配置,明确使用静态凭证和指定模型列表,避免旧的认证方式、自动配置或会话服务设置覆盖 CPA 连接。
- 更新 Claude Desktop 配置时正确切换到 CPA 本地配置,并支持在恢复备份时还原原有配置选择。
- 补充 Claude Desktop 对 Fable、Mythos 模型系列的识别。

## 修复

- 修复 xAI、Meta、Vertex 和 Interactions API Key 通道的 Claude Desktop 模型别名处理不完整的问题,避免将 API Key 模型错误映射到 OAuth 通道,导致模型不可用。
- 修复重复保存配置时,仍在使用的模型别名被清理的问题。同名模型存在于多个提供方时,保留有效的原有路由,避免意外切换提供方。
- 修复被禁用或排除的 API Key 模型可能通过残留 OAuth 映射被错误接受的问题。
- 完善 Claude Desktop 配置状态检查,识别认证方式、配置来源冲突及模型列表不完整的情况。
- 完善模型映射保存时的凭证保护与失败回滚处理,保留提供方的密钥和连接设置。

## 发布流程

- 恢复 Miaomiao 预览版自动构建与发布:由 Roomen 提交、目标为 `miaomiao` 的非草稿 PR,在六个平台构建通过后自动合并,并构建合并后的提交、更新滚动预览版。

## 使用说明

- 升级后,请在 CPA 中重新保存 Claude Desktop 配置,并完全退出、重新打开 Claude Desktop。
- 模型别名需要对应可用的原始模型。若原始模型已被停用或排除,请调整对应接入配置,或选择其他可用模型。

24 changes: 24 additions & 0 deletions docs/release-notes/v0.3.51/zh-TW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
- 關於軟體,如果您有任何問題或建議,請透過 [QQ群組](https://qm.qq.com/q/3queDaIG) 或 [GitHub Issues](https://github.com/router-for-me/EasyCLIProxyAPI/issues) 回饋,我們會盡快處理。

## 優化

- 適配新版 Claude Desktop 的閘道設定,明確使用靜態憑證與指定模型清單,避免舊的驗證方式、自動設定或工作階段服務設定覆寫 CPA 連線。
- 更新 Claude Desktop 設定時正確切換至 CPA 本機設定,並支援在還原備份時恢復原有的設定選擇。
- 補充 Claude Desktop 對 Fable、Mythos 模型系列的識別。

## 修復

- 修復 xAI、Meta、Vertex 和 Interactions API Key 通道的 Claude Desktop 模型別名處理不完整的問題,避免將 API Key 模型錯誤對應至 OAuth 通道,導致模型無法使用。
- 修復重複儲存設定時,仍在使用的模型別名被清除的問題。同名模型存在於多個提供者時,保留有效的原有路由,避免意外切換提供者。
- 修復被停用或排除的 API Key 模型可能透過殘留 OAuth 對應被錯誤接受的問題。
- 完善 Claude Desktop 設定狀態檢查,識別驗證方式、設定來源衝突及模型清單不完整的情況。
- 完善模型對應儲存時的憑證保護與失敗回復處理,保留提供者的金鑰與連線設定。

## 發布流程

- 恢復 Miaomiao 預覽版自動建置與發布:由 Roomen 提交、目標為 `miaomiao` 的非草稿 PR,在六個平台建置通過後自動合併,並建置合併後的提交、更新滾動預覽版。

## 使用說明

- 升級後,請在 CPA 中重新儲存 Claude Desktop 設定,並完全結束、重新開啟 Claude Desktop。
- 模型別名需要對應可用的原始模型。若原始模型已被停用或排除,請調整對應連線設定,或選擇其他可用模型。
37 changes: 37 additions & 0 deletions src-tauri/src/agents/backups/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,43 @@ fn claude_desktop_cowork_hosts_survive_update_close_and_reconnect() {
);
}

#[test]
fn claude_desktop_update_exits_hybrid_and_backup_restores_pointer() {
if !AgentClient::ClaudeDesktop.supported_platform() { return; }
let home = Home::new();
let paths = config_paths("claude-desktop", &home.0).unwrap();
let pointer = serde_json::json!({"bootstrapUrl":"https://config.example.test"});
let meta = serde_json::json!({"appliedId":"other", "hybridPointer":pointer,
"entries":[{"id":"other","name":"Original"}], "custom":true});
for path in &paths[..2] { save(path, r#"{"deploymentMode":"3p"}"#); }
save(&paths[3], meta.to_string());
let original = config_images(&paths).unwrap();
apply(&home.0, AgentClient::ClaudeDesktop, "gpt-one").unwrap();
let connected: Value = serde_json::from_slice(&fs::read(&paths[3]).unwrap()).unwrap();
assert!(connected.get("hybridPointer").is_none());
assert_eq!(connected["appliedId"], CLAUDE_DESKTOP_PROFILE_ID);
assert_eq!(connected["custom"], true);
let restored = prepare_recorded_integration_restore(AgentClient::ClaudeDesktop,
&paths, &config_images(&paths).unwrap()).unwrap().unwrap();
for ((path, restored), (_, original)) in restored.iter().zip(&original) {
assert_eq!(restored.is_some(), original.is_some());
assert_eq!(parse(path, text(restored.as_deref()).unwrap()).unwrap(),
parse(path, text(original.as_deref()).unwrap()).unwrap());
}

// A user who switches back to a hybrid source must not lose 3P mode
// when subsequently removing the dormant CPA integration.
let mut hybrid = connected;
hybrid["hybridPointer"] = pointer.clone();
save(&paths[3], hybrid.to_string());
assert!(!inspect_claude_desktop_agent_config(&paths, 8317, "test-secret").unwrap().0);
clear_agent_managed_configuration(AgentClient::ClaudeDesktop, &home.0, 8317).unwrap();
let meta: Value = serde_json::from_slice(&fs::read(&paths[3]).unwrap()).unwrap();
assert_eq!(meta["hybridPointer"], pointer);
let settings: Value = serde_json::from_slice(&fs::read(&paths[1]).unwrap()).unwrap();
assert_eq!(settings["deploymentMode"], "3p");
}

#[test]
fn clear_integration_preserves_claude_settings_for_other_endpoints() {
let home = Home::new();
Expand Down
57 changes: 38 additions & 19 deletions src-tauri/src/agents/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,21 @@ pub(crate) fn build_claude_desktop_deployment_config(
render_agent_json(root, "Claude Desktop configuration")
}

// Keep generation, removal and selective restoration in agreement. These keys
// belong to CPA's gateway profile; all other preferences remain user-owned.
pub(crate) const CLAUDE_DESKTOP_PROFILE_KEYS: &[&str] = &[
"disableDeploymentModeChooser",
"inferenceGatewayApiKey",
"inferenceGatewayAuthScheme",
"inferenceGatewayBaseUrl",
"inferenceProvider",
"inferenceCredentialKind",
"inferenceModels",
"modelDiscoveryEnabled",
"bootstrapEnabled",
"selfHostedUrl",
];

pub(crate) fn build_claude_desktop_profile(
existing: Option<&str>,
base_url: &str,
Expand All @@ -885,6 +900,15 @@ pub(crate) fn build_claude_desktop_profile(
mappings: Option<&ClaudeDesktopModelMappings>,
) -> Result<String, String> {
let mut root = parse_agent_json_object(existing, "Claude Desktop gateway configuration")?;
// An explicit credential source prevents an old helper/SSO setting from
// taking precedence over the local proxy key. The full model IDs below
// are authoritative, so discovery must not resolve bare tier aliases.
root.insert("inferenceCredentialKind".into(), serde_json::json!("static"));
root.insert("modelDiscoveryEnabled".into(), serde_json::json!(false));
// Preserve bootstrap details for restoration, but don't fetch an overlay
// which could replace this gateway. A session host also overrides inference.
root.insert("bootstrapEnabled".into(), serde_json::json!(false));
root.remove("selfHostedUrl");
root.insert(
"disableDeploymentModeChooser".to_string(),
serde_json::json!(true),
Expand Down Expand Up @@ -1023,13 +1047,20 @@ pub(crate) fn claude_desktop_family_tier(route_model: &str) -> Option<&'static s
|| route_model.eq_ignore_ascii_case(LEGACY_CLAUDE_DESKTOP_MODEL_IDS[2])
{
Some("haiku")
} else if lowered.starts_with("claude-fable-") || lowered.contains("-fable-") {
Some("fable")
} else if lowered.starts_with("claude-mythos-") || lowered.contains("-mythos-") {
Some("mythos")
} else {
None
}
}

pub(crate) fn build_claude_desktop_meta(existing: Option<&str>) -> Result<String, String> {
let mut root = parse_agent_json_object(existing, "Claude Desktop configuration index")?;
// Desktop resolves a hybrid pointer before appliedId. Match its own
// "apply locally" transition; transaction snapshots retain the old pointer.
root.remove("hybridPointer");
repair_claude_desktop_meta_names(&mut root);
let entries = ensure_json_array_entry(&mut root, "entries");
let mut managed_entry = None;
Expand Down Expand Up @@ -1155,8 +1186,9 @@ pub(crate) fn prepare_claude_desktop_managed_removal(paths: &[PathBuf]) -> Resul
}
let meta = read_agent_json_or_empty(&paths[3], "Claude Desktop configuration index")?;
let applied_id = meta.get("appliedId").and_then(serde_json::Value::as_str);
let managed_active = applied_id == Some(CLAUDE_DESKTOP_PROFILE_ID)
|| (applied_id.is_none() && agent_has_managed_marker(AgentClient::ClaudeDesktop, paths)?);
let managed_active = !claude_desktop_has_hybrid_pointer(&meta)
&& (applied_id == Some(CLAUDE_DESKTOP_PROFILE_ID)
|| (applied_id.is_none() && agent_has_managed_marker(AgentClient::ClaudeDesktop, paths)?));
let mut changed = Vec::new();
for (path, label) in [
(&paths[0], "Claude Desktop main configuration"),
Expand All @@ -1182,15 +1214,8 @@ pub(crate) fn prepare_claude_desktop_managed_removal(paths: &[PathBuf]) -> Resul
if let Some(update) =
prepare_agent_json_removal(&paths[2], "Claude Desktop gateway configuration", |root| {
let mut updated = false;
for key in [
"disableDeploymentModeChooser",
"inferenceGatewayApiKey",
"inferenceGatewayAuthScheme",
"inferenceGatewayBaseUrl",
"inferenceProvider",
"inferenceModels",
] {
updated |= root.remove(key).is_some();
for key in CLAUDE_DESKTOP_PROFILE_KEYS {
updated |= root.remove(*key).is_some();
}
updated
})?
Expand Down Expand Up @@ -2018,19 +2043,13 @@ pub(crate) fn build_restored_claude_desktop_config(
match index {
0 | 1 => restore_json_key(&mut root, original_root.as_ref(), "deploymentMode"),
2 => {
for key in [
"disableDeploymentModeChooser",
"inferenceGatewayApiKey",
"inferenceGatewayAuthScheme",
"inferenceGatewayBaseUrl",
"inferenceProvider",
"inferenceModels",
] {
for key in CLAUDE_DESKTOP_PROFILE_KEYS {
restore_json_key(&mut root, original_root.as_ref(), key);
}
}
3 => {
restore_json_key(&mut root, original_root.as_ref(), "appliedId");
restore_json_key(&mut root, original_root.as_ref(), "hybridPointer");
let mut entries = root
.remove("entries")
.and_then(|value| value.as_array().cloned())
Expand Down
29 changes: 28 additions & 1 deletion src-tauri/src/agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3393,6 +3393,31 @@ pub(crate) fn claude_code_model_presentation_environment(
.collect())
}

pub(crate) fn claude_desktop_has_hybrid_pointer(meta: &serde_json::Value) -> bool {
meta.get("hybridPointer")
.and_then(|pointer| pointer.get("bootstrapUrl"))
.and_then(serde_json::Value::as_str)
.is_some_and(|url| !url.trim().is_empty())
}

pub(crate) fn claude_desktop_profile_uses_local_gateway(profile: &serde_json::Value) -> bool {
profile.get("inferenceProvider").and_then(serde_json::Value::as_str) == Some("gateway")
&& profile.get("inferenceCredentialKind")
.is_none_or(|kind| kind.as_str() == Some("static"))
&& profile.get("inferenceGatewayAuthScheme")
.is_none_or(|scheme| scheme.as_str() == Some("bearer"))
&& !profile.get("selfHostedUrl").and_then(serde_json::Value::as_str)
.is_some_and(|url| !url.trim().is_empty())
&& (!profile.get("bootstrapUrl").and_then(serde_json::Value::as_str)
.is_some_and(|url| !url.trim().is_empty())
|| profile.get("bootstrapEnabled").and_then(serde_json::Value::as_bool) == Some(false))
&& profile.get("inferenceModels").and_then(serde_json::Value::as_array)
.is_some_and(|models| !models.is_empty() && models.iter().all(|model| {
model.as_str().or_else(|| model.get("name").and_then(serde_json::Value::as_str))
.is_some_and(|name| !name.trim().is_empty())
}))
}

pub(crate) fn inspect_claude_desktop_agent_config(
paths: &[PathBuf],
port: u16,
Expand All @@ -3406,7 +3431,9 @@ pub(crate) fn inspect_claude_desktop_agent_config(
let profile = read_agent_json_or_empty(&paths[2], "Claude Desktop gateway configuration")?;
let meta = read_agent_json_or_empty(&paths[3], "Claude Desktop configuration index")?;
let expected_base = managed_core_loopback_origin(port);
let configured = normal
let configured = claude_desktop_profile_uses_local_gateway(&profile)
&& !claude_desktop_has_hybrid_pointer(&meta)
&& normal
.get("deploymentMode")
.and_then(serde_json::Value::as_str)
== Some("3p")
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/agents/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,7 @@ fn restore_text(
let mut root = parse(path, Some(current))?;
let target = parse(path, target)?;
set(&mut root, &["appliedId".into()], target.get("appliedId"));
set(&mut root, &["hybridPointer".into()], target.get("hybridPointer"));
let mut entries = root
.get("entries")
.and_then(Value::as_array)
Expand Down
14 changes: 4 additions & 10 deletions src-tauri/src/core_config/alias_save.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ fn validate_alias_api_access_preserved(current: &str, updated: &str) -> Result<(
};
let current = parse(current)?;
let updated = parse(updated)?;
for section in MODEL_ALIAS_CONFIG_SECTIONS
.iter()
.copied()
.chain(["vertex-api-key", "xai-api-key", "interactions-api-key"])
{
for (section, _) in V8_PROVIDER_FAMILIES {
let without_models = |root: &serde_norway::Value| -> Result<_, String> {
let mut value = root
.get(section)
Expand All @@ -62,11 +58,9 @@ fn validate_alias_api_access_preserved(current: &str, updated: &str) -> Result<(
let providers = value
.as_sequence_mut()
.ok_or_else(|| format!("{section} must be an array; alias was not saved"))?;
if MODEL_ALIAS_CONFIG_SECTIONS.contains(&section) {
for provider in providers {
if let Some(provider) = provider.as_mapping_mut() {
provider.remove(yaml_key("models"));
}
for provider in providers {
if let Some(provider) = provider.as_mapping_mut() {
provider.remove(yaml_key("models"));
}
}
Ok(value)
Expand Down
Loading
Loading