diff --git a/src/config.rs b/src/config.rs index 6e1c1586a..abb6f8288 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1803,7 +1803,9 @@ impl Config { /// Load from environment variables only (no config file). pub fn load_from_env(instance_dir: &Path) -> Result { let mut llm = LlmConfig { - anthropic_key: std::env::var("ANTHROPIC_API_KEY").ok(), + anthropic_key: std::env::var("ANTHROPIC_API_KEY") + .ok() + .or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok()), openai_key: std::env::var("OPENAI_API_KEY").ok(), openrouter_key: std::env::var("OPENROUTER_API_KEY").ok(), zhipu_key: std::env::var("ZHIPU_API_KEY").ok(), @@ -1826,11 +1828,13 @@ impl Config { // Populate providers from env vars (same as from_toml does) if let Some(anthropic_key) = llm.anthropic_key.clone() { + let base_url = std::env::var("ANTHROPIC_BASE_URL") + .unwrap_or_else(|_| ANTHROPIC_PROVIDER_BASE_URL.to_string()); llm.providers .entry("anthropic".to_string()) .or_insert_with(|| ProviderConfig { api_type: ApiType::Anthropic, - base_url: ANTHROPIC_PROVIDER_BASE_URL.to_string(), + base_url, api_key: anthropic_key, name: None, }); @@ -1938,8 +1942,16 @@ impl Config { // Note: We allow boot without provider keys now. System starts in setup mode. // Agents are initialized later when keys are added via API. - // Env-only routing: check for env overrides on channel/worker models + // Env-only routing: check for env overrides on channel/worker models. + // SPACEBOT_MODEL overrides all process types at once; specific vars take precedence. let mut routing = RoutingConfig::default(); + if let Ok(model) = std::env::var("SPACEBOT_MODEL") { + routing.channel = model.clone(); + routing.branch = model.clone(); + routing.worker = model.clone(); + routing.compactor = model.clone(); + routing.cortex = model; + } if let Ok(channel_model) = std::env::var("SPACEBOT_CHANNEL_MODEL") { routing.channel = channel_model; } @@ -2039,7 +2051,8 @@ impl Config { .anthropic_key .as_deref() .and_then(resolve_env_value) - .or_else(|| std::env::var("ANTHROPIC_API_KEY").ok()), + .or_else(|| std::env::var("ANTHROPIC_API_KEY").ok()) + .or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok()), openai_key: toml .llm .openai_key @@ -2162,11 +2175,13 @@ impl Config { }; if let Some(anthropic_key) = llm.anthropic_key.clone() { + let base_url = std::env::var("ANTHROPIC_BASE_URL") + .unwrap_or_else(|_| ANTHROPIC_PROVIDER_BASE_URL.to_string()); llm.providers .entry("anthropic".to_string()) .or_insert_with(|| ProviderConfig { api_type: ApiType::Anthropic, - base_url: ANTHROPIC_PROVIDER_BASE_URL.to_string(), + base_url, api_key: anthropic_key, name: None, }); diff --git a/src/llm/anthropic/params.rs b/src/llm/anthropic/params.rs index 7bc59497e..2f45462af 100644 --- a/src/llm/anthropic/params.rs +++ b/src/llm/anthropic/params.rs @@ -7,7 +7,6 @@ use super::tools; use reqwest::RequestBuilder; use rig::completion::CompletionRequest; -const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages"; const CLAUDE_CODE_SYSTEM_PREAMBLE: &str = "You are Claude Code, Anthropic's official CLI for Claude."; @@ -32,13 +31,30 @@ fn is_opus(model_id: &str) -> bool { model_id.contains("opus") } +/// Construct the full messages endpoint URL from a base URL. +/// +/// If the base URL already ends with a path segment (e.g. `/v1/messages`), +/// use it as-is. Otherwise append `/v1/messages`. +fn messages_url(base_url: &str) -> String { + let trimmed = base_url.trim_end_matches('/'); + if trimmed.ends_with("/v1/messages") { + trimmed.to_string() + } else { + format!("{trimmed}/v1/messages") + } +} + /// Build a fully configured Anthropic API request from a CompletionRequest. /// +/// `base_url` is the provider's configured base URL (e.g. `https://api.anthropic.com` +/// or a custom proxy). The `/v1/messages` path is appended automatically. +/// /// `thinking_effort` controls adaptive thinking: "auto" picks max for Opus / /// high for others, or pass "max", "high", "medium", "low" explicitly. pub fn build_anthropic_request( http_client: &reqwest::Client, api_key: &str, + base_url: &str, model_name: &str, request: &CompletionRequest, thinking_effort: &str, @@ -46,7 +62,8 @@ pub fn build_anthropic_request( let is_oauth = auth::detect_auth_path(api_key) == AnthropicAuthPath::OAuthToken; let adaptive_thinking = supports_adaptive_thinking(model_name); let retention = cache::resolve_cache_retention(None); - let cache_control = cache::get_cache_control(ANTHROPIC_API_URL, retention); + let url = messages_url(base_url); + let cache_control = cache::get_cache_control(&url, retention); let mut body = serde_json::json!({ "model": model_name, @@ -80,7 +97,7 @@ pub fn build_anthropic_request( } let builder = http_client - .post(ANTHROPIC_API_URL) + .post(&url) .header("anthropic-version", "2023-06-01") .header("content-type", "application/json"); diff --git a/src/llm/model.rs b/src/llm/model.rs index fdb8d69de..95d81a914 100644 --- a/src/llm/model.rs +++ b/src/llm/model.rs @@ -352,6 +352,7 @@ impl SpacebotModel { let anthropic_request = crate::llm::anthropic::build_anthropic_request( self.llm_manager.http_client(), api_key, + &provider_config.base_url, &self.model_name, &request, effort,