diff --git a/src-tauri/crates/agent-core/src/core/providers/registry.rs b/src-tauri/crates/agent-core/src/core/providers/registry.rs index ac3c753a2..9c23e42a5 100644 --- a/src-tauri/crates/agent-core/src/core/providers/registry.rs +++ b/src-tauri/crates/agent-core/src/core/providers/registry.rs @@ -158,7 +158,7 @@ pub static PROVIDERS: &[ProviderSpec] = &[ litellm_prefix: None, skip_prefixes: &["atlascloud/"], default_api_base: Some("https://api.atlascloud.ai/v1"), - default_anthropic_api_base: None, + default_anthropic_api_base: Some("https://api.atlascloud.ai"), is_local: false, env_key: Some("ATLASCLOUD_API_KEY"), }, diff --git a/src-tauri/crates/key-vault/src/commands/registry/data/cli_agents.rs b/src-tauri/crates/key-vault/src/commands/registry/data/cli_agents.rs index 5f9ecc2ee..0078d8b2e 100644 --- a/src-tauri/crates/key-vault/src/commands/registry/data/cli_agents.rs +++ b/src-tauri/crates/key-vault/src/commands/registry/data/cli_agents.rs @@ -102,6 +102,7 @@ pub(crate) fn cli_agent_registry() -> Vec { has_subscription_plan: true, compatible_api_providers: &[ "anthropic_api", + "atlascloud_api", "moonshot_api", "zenmux_api", "longcat_api", diff --git a/src-tauri/crates/key-vault/src/commands/registry/mod.rs b/src-tauri/crates/key-vault/src/commands/registry/mod.rs index 707e7e570..a28f5a146 100644 --- a/src-tauri/crates/key-vault/src/commands/registry/mod.rs +++ b/src-tauri/crates/key-vault/src/commands/registry/mod.rs @@ -159,9 +159,10 @@ mod compatibility_tests { #[test] fn compatibility_comes_from_the_central_cli_registry() { - assert!(is_cli_provider_compatible("codex", "openai_api")); assert!(is_cli_provider_compatible("codex", "atlascloud_api")); assert!(is_cli_provider_compatible("opencode", "atlascloud_api")); + assert!(is_cli_provider_compatible("claude_code", "atlascloud_api")); + assert!(is_cli_provider_compatible("codex", "openai_api")); assert!(is_cli_provider_compatible("claude_code", "anthropic_api")); assert!(!is_cli_provider_compatible("unknown", "openai_api")); assert_eq!(cli_agent_display_name("opencode"), Some("OpenCode")); diff --git a/src-tauri/crates/key-vault/src/key_store/agent_env_builder.rs b/src-tauri/crates/key-vault/src/key_store/agent_env_builder.rs index cfec47551..0b06edf35 100644 --- a/src-tauri/crates/key-vault/src/key_store/agent_env_builder.rs +++ b/src-tauri/crates/key-vault/src/key_store/agent_env_builder.rs @@ -10,6 +10,7 @@ const ZENMUX_OPENAI_BASE_URL: &str = "https://zenmux.ai/api/v1"; const ZENMUX_ANTHROPIC_BASE_URL: &str = "https://zenmux.ai/api/anthropic"; const LONGCAT_OPENAI_BASE_URL: &str = "https://api.longcat.chat/openai"; const LONGCAT_ANTHROPIC_BASE_URL: &str = "https://api.longcat.chat/anthropic"; +const ATLASCLOUD_ANTHROPIC_BASE_URL: &str = "https://api.atlascloud.ai"; impl KeyService { /// Get environment variables for running an agent @@ -116,6 +117,14 @@ impl KeyService { entry.id, entry.base_url ); + } else if entry.model_type == ModelType::AtlascloudApi { + // Atlas keys persist the OpenAI-protocol /v1 URL; its + // Anthropic surface lives at the bare host, so the stored + // URL must never reach Claude Code. + env.insert( + "ANTHROPIC_BASE_URL".to_string(), + ATLASCLOUD_ANTHROPIC_BASE_URL.to_string(), + ); } else if let Some(ref url) = entry.base_url { env.insert("ANTHROPIC_BASE_URL".to_string(), url.clone()); } else if entry.model_type == ModelType::ZenmuxApi { diff --git a/src-tauri/crates/key-vault/src/key_store/tests/tests.rs b/src-tauri/crates/key-vault/src/key_store/tests/tests.rs index cda64d0a0..9fc922235 100644 --- a/src-tauri/crates/key-vault/src/key_store/tests/tests.rs +++ b/src-tauri/crates/key-vault/src/key_store/tests/tests.rs @@ -1228,6 +1228,41 @@ fn test_cross_type_env_zenmux_as_codex_uses_openai_endpoint() { assert!(!env.contains_key("ZENMUX_API_KEY")); } +#[test] +fn test_cross_type_env_atlascloud_as_claude_code_uses_anthropic_endpoint() { + let temp_dir = tempdir().unwrap(); + let service = KeyService::new(Some(temp_dir.path().to_path_buf())); + + let mut atlas_key = ModelKey::new(ModelType::AtlascloudApi); + atlas_key.api_key = Some("atlas-test-key".to_string()); + atlas_key.enabled_models = vec!["zai-org/glm-5.1".to_string()]; + // The stored /v1 URL is OpenAI-protocol; the Anthropic export must + // ignore it and use the bare host instead. + atlas_key.base_url = Some("https://api.atlascloud.ai/v1".to_string()); + let key_id = atlas_key.id.clone(); + service.save_key(atlas_key).unwrap(); + + let env = service.get_env_for_agent(&ModelType::ClaudeCode, Some(&key_id)); + assert_eq!( + env.get("ANTHROPIC_API_KEY").map(String::as_str), + Some("atlas-test-key"), + ); + assert_eq!( + env.get("ANTHROPIC_BASE_URL").map(String::as_str), + Some("https://api.atlascloud.ai"), + ); + assert_eq!( + env.get("ANTHROPIC_MODEL").map(String::as_str), + Some("zai-org/glm-5.1"), + ); + assert_eq!( + env.get("CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS") + .map(String::as_str), + Some("1"), + ); + assert!(!env.contains_key("ATLASCLOUD_API_KEY")); +} + #[test] fn test_atlascloud_provider_exports_canonical_environment() { let temp_dir = tempdir().unwrap(); diff --git a/src-tauri/crates/key-vault/src/provider_config.rs b/src-tauri/crates/key-vault/src/provider_config.rs index 0148471ae..ff00145eb 100644 --- a/src-tauri/crates/key-vault/src/provider_config.rs +++ b/src-tauri/crates/key-vault/src/provider_config.rs @@ -175,6 +175,13 @@ const ZENMUX_ENDPOINTS: &[ProviderEndpointSpec] = &[ProviderEndpointSpec { anthropic_base_url: Some("https://zenmux.ai/api/anthropic"), }]; +const ATLASCLOUD_ENDPOINTS: &[ProviderEndpointSpec] = &[ProviderEndpointSpec { + id: "default", + label: "Atlas Cloud", + base_url: "https://api.atlascloud.ai/v1", + anthropic_base_url: Some("https://api.atlascloud.ai"), +}]; + const LONGCAT_ENDPOINTS: &[ProviderEndpointSpec] = &[ProviderEndpointSpec { id: "default", label: "LongCat", @@ -357,12 +364,15 @@ pub fn get_provider_config(model_type: &str) -> ProviderConfig { true, Some("https://api.openai.com/v1"), ), - "atlascloud_api" => ProviderConfig::new( + "atlascloud_api" => ProviderConfig::with_protocols( "ATLASCLOUD_API_KEY", Some("ATLASCLOUD_BASE_URL"), true, - Some("https://api.atlascloud.ai/v1"), - ), + None, + &["openai", "anthropic"], + "openai", + ) + .with_endpoints(ATLASCLOUD_ENDPOINTS), "deepseek_api" => ProviderConfig::new( "DEEPSEEK_API_KEY", None, @@ -594,8 +604,12 @@ mod tests { config.default_base_url, Some("https://api.atlascloud.ai/v1".to_string()) ); - assert_eq!(config.supported_protocols, vec!["openai"]); + assert_eq!(config.supported_protocols, vec!["openai", "anthropic"]); assert_eq!(config.default_protocol, "openai"); + assert_eq!( + config.default_anthropic_base_url(), + Some("https://api.atlascloud.ai".to_string()) + ); } #[test]