diff --git a/config.example.toml b/config.example.toml index ffc1da282e..c303b6d9c8 100644 --- a/config.example.toml +++ b/config.example.toml @@ -20,7 +20,7 @@ # `api_key` / `base_url` are # still read as DeepSeek defaults when `[providers.deepseek]` is absent # (backward compatibility). -provider = "deepseek" # deepseek | deepseek-cn | deepseek-anthropic | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | zai | stepfun | minimax | sglang | vllm | ollama | huggingface | together | qianfan | openai-codex | anthropic | openmodel | deepinfra | sakana +provider = "deepseek" # deepseek | deepseek-cn | deepseek-anthropic | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | zai | stepfun | minimax | sglang | vllm | ollama | huggingface | together | qianfan | openai-codex | anthropic | openmodel | deepinfra | sakana | opencode-zen api_key = "YOUR_DEEPSEEK_API_KEY" # must be non-empty base_url = "https://api.deepseek.com/beta" # provider = "deepseek-cn" # legacy alias (official host is still https://api.deepseek.com) @@ -551,7 +551,18 @@ max_subagents = 10 # optional (1-20) [providers.sakana] # api_key = "YOUR_FUGU_API_KEY" # base_url = "https://api.sakana.ai/v1" -# model = "fugu" # or fugu-ultra-20260615 +# model = "fugu" + +# OpenCode Zen Provider (https://opencode.ai/zen) +# OpenCode Zen is a curated model gateway that provides access to +# tested and verified models through the OpenAI Responses API. +# Sign up at https://opencode.ai/zen, copy your API key. +# Provider aliases: opencode-zen, opencode_zen, zen, opencode +# Env var aliases: OPENCODE_ZEN_API_KEY, OPENCODE_API_KEY +[providers.opencode_zen] +# api_key = "YOUR_OPENCODE_ZEN_API_KEY" +# base_url = "https://opencode.ai/zen/v1" +# model = "gpt-5.5" # ───────────────────────────────────────────────────────────────────────────────── # Together AI Provider (https://www.together.ai/) diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs index 165306e4f3..1243fc9aa0 100644 --- a/crates/agent/src/lib.rs +++ b/crates/agent/src/lib.rs @@ -675,6 +675,14 @@ impl Default for ModelRegistry { supports_tools: true, supports_reasoning: true, }, + // OpenCode Zen curated gateway models + ModelInfo { + id: "gpt-5.5".to_string(), + provider: ProviderKind::OpenCodeZen, + aliases: vec!["zen-gpt-5.5".to_string()], + supports_tools: true, + supports_reasoning: true, + }, // Anthropic native Messages API models (#3014) ModelInfo { id: "claude-opus-4-8".to_string(), diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index e0bd5981c7..70d5226a22 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -159,6 +159,14 @@ pub struct ProvidersToml { pub deepinfra: ProviderConfigToml, #[serde(default, alias = "sakana-ai", alias = "sakana_ai", alias = "fugu")] pub sakana: ProviderConfigToml, + #[serde( + default, + alias = "opencode-zen", + alias = "opencode_zen", + alias = "zen", + alias = "opencode" + )] + pub opencode_zen: ProviderConfigToml, /// Catch-all table for the dynamic OpenAI-compatible custom provider /// identity (#1519). Arbitrary `[providers.]` tables are handled by /// the tui-side flatten map; this named slot keeps the canonical @@ -257,6 +265,7 @@ impl ProvidersToml { ProviderKind::Minimax => &self.minimax, ProviderKind::Deepinfra => &self.deepinfra, ProviderKind::Sakana => &self.sakana, + ProviderKind::OpenCodeZen => &self.opencode_zen, ProviderKind::Custom => &self.custom, } } @@ -292,6 +301,7 @@ impl ProvidersToml { ProviderKind::Minimax => &mut self.minimax, ProviderKind::Deepinfra => &mut self.deepinfra, ProviderKind::Sakana => &mut self.sakana, + ProviderKind::OpenCodeZen => &mut self.opencode_zen, ProviderKind::Custom => &mut self.custom, } } @@ -1944,6 +1954,7 @@ impl ConfigToml { ProviderKind::Minimax => DEFAULT_MINIMAX_BASE_URL.to_string(), ProviderKind::Deepinfra => DEFAULT_DEEPINFRA_BASE_URL.to_string(), ProviderKind::Sakana => DEFAULT_SAKANA_BASE_URL.to_string(), + ProviderKind::OpenCodeZen => DEFAULT_OPENCODE_ZEN_BASE_URL.to_string(), // The custom provider has no built-in endpoint; fall back to its // descriptor placeholder so the lookup is total. Real custom // routes always supply a configured base_url before this point. @@ -2520,6 +2531,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str { ProviderKind::Minimax => DEFAULT_MINIMAX_MODEL, ProviderKind::Deepinfra => DEFAULT_DEEPINFRA_MODEL, ProviderKind::Sakana => DEFAULT_SAKANA_MODEL, + ProviderKind::OpenCodeZen => DEFAULT_OPENCODE_ZEN_MODEL, // No built-in default model; the registry placeholder keeps this total. ProviderKind::Custom => provider.provider().default_model(), } @@ -2556,6 +2568,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str { ProviderKind::Minimax => DEFAULT_MINIMAX_BASE_URL, ProviderKind::Deepinfra => DEFAULT_DEEPINFRA_BASE_URL, ProviderKind::Sakana => DEFAULT_SAKANA_BASE_URL, + ProviderKind::OpenCodeZen => DEFAULT_OPENCODE_ZEN_BASE_URL, // No built-in default base URL; the registry placeholder keeps this total. ProviderKind::Custom => provider.provider().default_base_url(), } @@ -4082,6 +4095,8 @@ struct EnvRuntimeOverrides { deepinfra_model: Option, sakana_base_url: Option, sakana_model: Option, + opencode_zen_base_url: Option, + opencode_zen_model: Option, } impl EnvRuntimeOverrides { @@ -4314,6 +4329,10 @@ impl EnvRuntimeOverrides { deepinfra_model: std::env::var("DEEPINFRA_MODEL") .ok() .filter(|v| !v.trim().is_empty()), + opencode_zen_base_url: std::env::var("OPENCODE_ZEN_BASE_URL") + .or_else(|_| std::env::var("OPENCODE_ZEN_BASE")) + .ok(), + opencode_zen_model: std::env::var("OPENCODE_ZEN_MODEL").ok(), sakana_base_url: std::env::var("SAKANA_BASE_URL") .ok() .filter(|v| !v.trim().is_empty()), @@ -4371,6 +4390,7 @@ impl EnvRuntimeOverrides { ProviderKind::Minimax => self.minimax_base_url.clone(), ProviderKind::Deepinfra => self.deepinfra_base_url.clone(), ProviderKind::Sakana => self.sakana_base_url.clone(), + ProviderKind::OpenCodeZen => self.opencode_zen_base_url.clone(), // No dedicated CODEWHALE_CUSTOM_BASE_URL env override: a custom // provider's base URL comes from its `[providers.]` table. ProviderKind::Custom => None, @@ -4401,6 +4421,7 @@ impl EnvRuntimeOverrides { ProviderKind::Minimax => self.minimax_model.clone(), ProviderKind::Deepinfra => self.deepinfra_model.clone(), ProviderKind::Sakana => self.sakana_model.clone(), + ProviderKind::OpenCodeZen => self.opencode_zen_model.clone(), _ => None, }?; diff --git a/crates/config/src/provider.rs b/crates/config/src/provider.rs index c5964a9d17..c2b2866103 100644 --- a/crates/config/src/provider.rs +++ b/crates/config/src/provider.rs @@ -14,16 +14,16 @@ use super::{ DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL, DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_CODEX_BASE_URL, - DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_OPENMODEL_BASE_URL, - DEFAULT_OPENMODEL_MODEL, DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL, - DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, - DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL, - DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL, - DEFAULT_STEPFUN_MODEL, DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL, - DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL, - DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL, - DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL, - DEFAULT_ZAI_MODEL, ProviderKind, + DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_OPENCODE_ZEN_BASE_URL, + DEFAULT_OPENCODE_ZEN_MODEL, DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL, + DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL, DEFAULT_QIANFAN_BASE_URL, + DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, DEFAULT_SGLANG_BASE_URL, + DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL, + DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL, + DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, + DEFAULT_VOLCENGINE_BASE_URL, DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, + DEFAULT_WANJIE_ARK_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, + DEFAULT_ZAI_BASE_URL, DEFAULT_ZAI_MODEL, ProviderKind, }; /// Wire protocol spoken by a provider. @@ -594,6 +594,47 @@ provider!( aliases: ["sakana-ai", "sakana_ai", "fugu"] ); +/// OpenCode Zen curated model gateway using the Responses API. +pub struct OpenCodeZen; + +impl Provider for OpenCodeZen { + fn id(&self) -> &'static str { + "opencode-zen" + } + + fn kind(&self) -> ProviderKind { + ProviderKind::OpenCodeZen + } + + fn display_name(&self) -> &'static str { + "OpenCode Zen" + } + + fn default_base_url(&self) -> &'static str { + DEFAULT_OPENCODE_ZEN_BASE_URL + } + + fn default_model(&self) -> &'static str { + DEFAULT_OPENCODE_ZEN_MODEL + } + + fn env_vars(&self) -> &'static [&'static str] { + &["OPENCODE_ZEN_API_KEY", "OPENCODE_API_KEY"] + } + + fn provider_config_key(&self) -> &'static str { + "opencode_zen" + } + + fn aliases(&self) -> &'static [&'static str] { + &["opencode_zen", "opencodezen", "zen", "opencode"] + } + + fn wire(&self) -> WireFormat { + WireFormat::Responses + } +} + /// User-defined OpenAI-compatible endpoint (#1519). /// /// A single dynamic provider identity for arbitrary `[providers.] @@ -676,9 +717,10 @@ static STEPFUN: Stepfun = Stepfun; static MINIMAX: Minimax = Minimax; static DEEPINFRA: Deepinfra = Deepinfra; static SAKANA: Sakana = Sakana; +static OPENCODE_ZEN: OpenCodeZen = OpenCodeZen; static CUSTOM: Custom = Custom; -static PROVIDER_REGISTRY: [&dyn Provider; 30] = [ +static PROVIDER_REGISTRY: [&dyn Provider; 31] = [ &DEEPSEEK, &DEEPSEEK_ANTHROPIC, &NVIDIA_NIM, @@ -708,6 +750,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 30] = [ &MINIMAX, &DEEPINFRA, &SAKANA, + &OPENCODE_ZEN, &CUSTOM, ]; diff --git a/crates/config/src/provider_defaults.rs b/crates/config/src/provider_defaults.rs index 795b023e6c..3420ebe6e8 100644 --- a/crates/config/src/provider_defaults.rs +++ b/crates/config/src/provider_defaults.rs @@ -130,3 +130,6 @@ pub(crate) const DEFAULT_DEEPINFRA_BASE_URL: &str = "https://api.deepinfra.com/v // Sakana AI Fugu defaults pub(crate) const DEFAULT_SAKANA_MODEL: &str = "fugu"; pub(crate) const DEFAULT_SAKANA_BASE_URL: &str = "https://api.sakana.ai/v1"; +// OpenCode Zen defaults +pub(crate) const DEFAULT_OPENCODE_ZEN_MODEL: &str = "gpt-5.5"; +pub(crate) const DEFAULT_OPENCODE_ZEN_BASE_URL: &str = "https://opencode.ai/zen/v1"; diff --git a/crates/config/src/provider_kind.rs b/crates/config/src/provider_kind.rs index 8c84d66116..32e847a6ef 100644 --- a/crates/config/src/provider_kind.rs +++ b/crates/config/src/provider_kind.rs @@ -100,6 +100,13 @@ pub enum ProviderKind { Deepinfra, #[serde(alias = "sakana-ai", alias = "sakana_ai", alias = "fugu")] Sakana, + #[serde( + alias = "opencode-zen", + alias = "opencode_zen", + alias = "zen", + alias = "opencode" + )] + OpenCodeZen, /// User-defined OpenAI-compatible endpoint (#1519). /// /// A single dynamic identity for arbitrary `[providers.] @@ -111,7 +118,7 @@ pub enum ProviderKind { } impl ProviderKind { - pub const ALL: [Self; 30] = [ + pub const ALL: [Self; 31] = [ Self::Deepseek, Self::DeepseekAnthropic, Self::NvidiaNim, @@ -141,6 +148,7 @@ impl ProviderKind { Self::Minimax, Self::Deepinfra, Self::Sakana, + Self::OpenCodeZen, Self::Custom, ]; diff --git a/crates/config/src/route/tests.rs b/crates/config/src/route/tests.rs index 1e54dfc10f..9721b29bde 100644 --- a/crates/config/src/route/tests.rs +++ b/crates/config/src/route/tests.rs @@ -160,7 +160,7 @@ fn descriptor_protocol_matches_provider_wire() { "{kind:?} protocol must equal provider().wire()" ); let expected = match kind { - ProviderKind::OpenaiCodex => RequestProtocol::Responses, + ProviderKind::OpenaiCodex | ProviderKind::OpenCodeZen => RequestProtocol::Responses, ProviderKind::DeepseekAnthropic | ProviderKind::Anthropic | ProviderKind::Openmodel => { RequestProtocol::AnthropicMessages } diff --git a/crates/config/src/tests.rs b/crates/config/src/tests.rs index 08aa8165fd..7a970cd251 100644 --- a/crates/config/src/tests.rs +++ b/crates/config/src/tests.rs @@ -3396,7 +3396,9 @@ fn provider_metadata_defaults_match_runtime_helpers() { // speaks the native Messages API; every other built-in provider // is OpenAI-compatible Chat Completions. let expected_wire = match kind { - ProviderKind::OpenaiCodex => provider::WireFormat::Responses, + ProviderKind::OpenaiCodex | ProviderKind::OpenCodeZen => { + provider::WireFormat::Responses + } ProviderKind::Anthropic | ProviderKind::DeepseekAnthropic | ProviderKind::Openmodel => { provider::WireFormat::AnthropicMessages } diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index 66ed88fc45..3dc0f3cfdb 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -1718,6 +1718,7 @@ pub(super) fn apply_reasoning_effort( } ApiProvider::Stepfun => {} ApiProvider::Sakana => {} + ApiProvider::OpenCodeZen => {} }, "low" | "minimal" | "medium" | "mid" | "high" | "" => match provider { // DeepSeek compatibility: low/medium both map to high @@ -1807,6 +1808,7 @@ pub(super) fn apply_reasoning_effort( } ApiProvider::Stepfun => {} ApiProvider::Sakana => {} + ApiProvider::OpenCodeZen => {} }, "xhigh" | "max" | "highest" | "ultracode" => match provider { ApiProvider::Deepseek @@ -1876,6 +1878,7 @@ pub(super) fn apply_reasoning_effort( } ApiProvider::Stepfun => {} ApiProvider::Sakana => {} + ApiProvider::OpenCodeZen => {} }, _ => {} } diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 94672fe8c3..00ae8a8c08 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -71,6 +71,7 @@ pub enum ApiProvider { Minimax, Deepinfra, Sakana, + OpenCodeZen, /// User-defined OpenAI-compatible endpoint (#1519). /// /// Selected when `provider = ""` names a `[providers.] @@ -204,6 +205,7 @@ impl ApiProvider { Self::Minimax => "https://platform.minimax.io/docs/guides/quickstart-preparation", Self::Deepinfra => "https://deepinfra.com/dash/api_keys", Self::Sakana => "https://api.sakana.ai/", + Self::OpenCodeZen => "https://opencode.ai/zen", Self::OpenaiCodex | Self::Sglang | Self::Vllm | Self::Ollama => return None, // Custom endpoints have no canonical credential page; the user // supplies the key via their own `api_key_env`. @@ -219,7 +221,7 @@ impl ApiProvider { /// `ApiProvider` discriminant → `ProviderKind` lookup. /// Index 1 is `None` for the legacy `DeepseekCN` variant. - const KIND_LOOKUP: [Option; 31] = [ + const KIND_LOOKUP: [Option; 32] = [ Some(codewhale_config::ProviderKind::Deepseek), None, // DeepseekCN Some(codewhale_config::ProviderKind::DeepseekAnthropic), @@ -250,11 +252,12 @@ impl ApiProvider { Some(codewhale_config::ProviderKind::Minimax), Some(codewhale_config::ProviderKind::Deepinfra), Some(codewhale_config::ProviderKind::Sakana), + Some(codewhale_config::ProviderKind::OpenCodeZen), Some(codewhale_config::ProviderKind::Custom), ]; /// `ProviderKind` discriminant → `ApiProvider` lookup. - const FROM_KIND_LOOKUP: [Self; 30] = [ + const FROM_KIND_LOOKUP: [Self; 31] = [ Self::Deepseek, Self::DeepseekAnthropic, Self::NvidiaNim, @@ -284,6 +287,7 @@ impl ApiProvider { Self::Minimax, Self::Deepinfra, Self::Sakana, + Self::OpenCodeZen, Self::Custom, ]; @@ -354,6 +358,10 @@ fn subagent_provider_key_matches(key: &str, provider: ApiProvider) -> bool { normalized.as_str(), "openai_codex" | "codex" | "chatgpt" | "openai_chatgpt" ), + ApiProvider::OpenCodeZen => matches!( + normalized.as_str(), + "opencode_zen" | "opencode-zen" | "zen" | "opencode" + ), ApiProvider::Anthropic => { matches!( normalized.as_str(), @@ -459,11 +467,19 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi }; } - if matches!(provider, ApiProvider::OpenaiCodex) { + if matches!( + provider, + ApiProvider::OpenaiCodex | ApiProvider::OpenCodeZen + ) { + let ctx_window = if provider == ApiProvider::OpenaiCodex { + OPENAI_CODEX_EFFECTIVE_CONTEXT_WINDOW_TOKENS + } else { + crate::models::context_window_for_model(resolved_model).unwrap_or(200_000) + }; return ProviderCapability { provider, resolved_model: resolved_model.to_string(), - context_window: OPENAI_CODEX_EFFECTIVE_CONTEXT_WINDOW_TOKENS, + context_window: ctx_window, max_output: crate::models::max_output_tokens_for_model(resolved_model).unwrap_or(4096), thinking_supported: true, cache_telemetry_supported: false, @@ -1138,6 +1154,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati ApiProvider::Together => vec![DEFAULT_TOGETHER_MODEL, DEFAULT_TOGETHER_FLASH_MODEL], ApiProvider::Qianfan => vec![DEFAULT_QIANFAN_MODEL], ApiProvider::OpenaiCodex => vec![DEFAULT_OPENAI_CODEX_MODEL], + ApiProvider::OpenCodeZen => vec![DEFAULT_OPENCODE_ZEN_MODEL], ApiProvider::Openmodel => vec![DEFAULT_OPENMODEL_MODEL], ApiProvider::Zai => vec![DEFAULT_ZAI_MODEL, ZAI_GLM_5_1_MODEL, ZAI_GLM_5_TURBO_MODEL], ApiProvider::Stepfun => vec![DEFAULT_STEPFUN_MODEL], @@ -2572,6 +2589,15 @@ pub struct ProvidersConfig { pub minimax: ProviderConfig, #[serde(default, alias = "sakana-ai", alias = "sakana_ai", alias = "fugu")] pub sakana: ProviderConfig, + #[serde( + default, + alias = "opencode-zen", + alias = "opencode_zen", + alias = "opencodezen", + alias = "zen", + alias = "opencode" + )] + pub opencode_zen: ProviderConfig, /// Arbitrary user-named custom providers (#1519). /// /// Captures every `[providers.]` table whose key is not one of the @@ -2622,6 +2648,7 @@ impl ProvidersConfig { ("providers.stepfun", &self.stepfun), ("providers.minimax", &self.minimax), ("providers.sakana", &self.sakana), + ("providers.opencode_zen", &self.opencode_zen), ]; for (name, config) in builtins { validate_provider_context_window(name, config.context_window)?; @@ -2950,6 +2977,7 @@ impl Config { ApiProvider::Together => &providers.together, ApiProvider::Qianfan => &providers.qianfan, ApiProvider::OpenaiCodex => &providers.openai_codex, + ApiProvider::OpenCodeZen => &providers.opencode_zen, ApiProvider::Anthropic => &providers.anthropic, ApiProvider::Openmodel => &providers.openmodel, ApiProvider::Zai => &providers.zai, @@ -3010,6 +3038,7 @@ impl Config { ApiProvider::Together => &mut providers.together, ApiProvider::Qianfan => &mut providers.qianfan, ApiProvider::OpenaiCodex => &mut providers.openai_codex, + ApiProvider::OpenCodeZen => &mut providers.opencode_zen, ApiProvider::Anthropic => &mut providers.anthropic, ApiProvider::Openmodel => &mut providers.openmodel, ApiProvider::Zai => &mut providers.zai, @@ -3138,6 +3167,9 @@ impl Config { if provider == ApiProvider::OpenaiCodex { return DEFAULT_OPENAI_CODEX_MODEL.to_string(); } + if provider == ApiProvider::OpenCodeZen { + return DEFAULT_OPENCODE_ZEN_MODEL.to_string(); + } let moonshot_config = (provider == ApiProvider::Moonshot) .then(|| self.provider_config()) @@ -3202,6 +3234,7 @@ impl Config { ApiProvider::Together => DEFAULT_TOGETHER_MODEL, ApiProvider::Qianfan => DEFAULT_QIANFAN_MODEL, ApiProvider::OpenaiCodex => DEFAULT_OPENAI_CODEX_MODEL, + ApiProvider::OpenCodeZen => DEFAULT_OPENCODE_ZEN_MODEL, ApiProvider::Openmodel => DEFAULT_OPENMODEL_MODEL, ApiProvider::Zai => DEFAULT_ZAI_MODEL, ApiProvider::Stepfun => DEFAULT_STEPFUN_MODEL, @@ -3257,6 +3290,7 @@ impl Config { | ApiProvider::Together | ApiProvider::Qianfan | ApiProvider::OpenaiCodex + | ApiProvider::OpenCodeZen | ApiProvider::Zai | ApiProvider::Stepfun | ApiProvider::Minimax @@ -3315,6 +3349,7 @@ impl Config { ApiProvider::Together => DEFAULT_TOGETHER_BASE_URL, ApiProvider::Qianfan => DEFAULT_QIANFAN_BASE_URL, ApiProvider::OpenaiCodex => DEFAULT_OPENAI_CODEX_BASE_URL, + ApiProvider::OpenCodeZen => DEFAULT_OPENCODE_ZEN_BASE_URL, ApiProvider::Openmodel => DEFAULT_OPENMODEL_BASE_URL, ApiProvider::Zai => DEFAULT_ZAI_BASE_URL, ApiProvider::Stepfun => DEFAULT_STEPFUN_BASE_URL, @@ -3483,7 +3518,7 @@ impl Config { ApiProvider::Anthropic | ApiProvider::Openmodel => { anyhow::bail!("{}", missing_provider_api_key_message(provider)?) } - ApiProvider::OpenaiCodex => anyhow::bail!( + ApiProvider::OpenaiCodex | ApiProvider::OpenCodeZen => anyhow::bail!( "OpenAI Codex OAuth credentials not found.\n\ \n\ CodeWhale uses your existing ChatGPT/Codex login.\n\ @@ -4431,6 +4466,13 @@ fn apply_env_overrides(config: &mut Config) { .openai_codex .base_url = Some(value); } + ApiProvider::OpenCodeZen => { + config + .providers + .get_or_insert_with(ProvidersConfig::default) + .opencode_zen + .base_url = Some(value); + } ApiProvider::Zai => { config .providers @@ -4681,6 +4723,7 @@ fn apply_env_overrides(config: &mut Config) { ApiProvider::Together => &mut providers.together, ApiProvider::Qianfan => &mut providers.qianfan, ApiProvider::OpenaiCodex => &mut providers.openai_codex, + ApiProvider::OpenCodeZen => &mut providers.opencode_zen, ApiProvider::Anthropic => &mut providers.anthropic, ApiProvider::Openmodel => &mut providers.openmodel, ApiProvider::Zai => &mut providers.zai, @@ -4903,6 +4946,7 @@ fn apply_env_overrides(config: &mut Config) { ApiProvider::Together => &mut providers.together, ApiProvider::Qianfan => &mut providers.qianfan, ApiProvider::OpenaiCodex => &mut providers.openai_codex, + ApiProvider::OpenCodeZen => &mut providers.opencode_zen, ApiProvider::Anthropic => &mut providers.anthropic, ApiProvider::Openmodel => &mut providers.openmodel, ApiProvider::Zai => &mut providers.zai, @@ -5676,6 +5720,7 @@ fn merge_providers( stepfun: merge_provider_config(base.stepfun, override_cfg.stepfun), minimax: merge_provider_config(base.minimax, override_cfg.minimax), sakana: merge_provider_config(base.sakana, override_cfg.sakana), + opencode_zen: merge_provider_config(base.opencode_zen, override_cfg.opencode_zen), custom: merge_custom_providers(base.custom, override_cfg.custom), }), } @@ -6102,6 +6147,12 @@ pub fn active_provider_has_config_api_key(config: &Config) -> bool { // active_provider_has_env_api_key. return crate::oauth::auth_file_path().exists(); } + if provider == ApiProvider::OpenCodeZen { + return config + .provider_config_for(provider) + .and_then(|c| c.api_key.as_deref()) + .is_some_and(|k| !k.is_empty()); + } if matches!(provider, ApiProvider::Huggingface) && std::env::var("HUGGINGFACE_API_KEY") .or_else(|_| std::env::var("HF_TOKEN")) @@ -6166,6 +6217,12 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool { // login on disk. return crate::oauth::auth_file_path().exists(); } + if provider == ApiProvider::OpenCodeZen { + return config + .provider_config_for(provider) + .and_then(|c| c.api_key.as_deref()) + .is_some_and(|k| !k.is_empty()); + } // Self-hosted providers typically run without authentication. if provider.is_self_hosted() { diff --git a/crates/tui/src/config/models.rs b/crates/tui/src/config/models.rs index a8bf8c6fb4..b8d8a922c8 100644 --- a/crates/tui/src/config/models.rs +++ b/crates/tui/src/config/models.rs @@ -122,6 +122,9 @@ pub const DEFAULT_QIANFAN_BASE_URL: &str = "https://api.baiduqianfan.ai/v1"; pub const DEFAULT_OPENAI_CODEX_MODEL: &str = "gpt-5.5"; pub const DEFAULT_OPENAI_CODEX_BASE_URL: &str = "https://chatgpt.com/backend-api"; pub const OPENAI_CODEX_EFFECTIVE_CONTEXT_WINDOW_TOKENS: u32 = 400_000; +// OpenCode Zen defaults +pub const DEFAULT_OPENCODE_ZEN_MODEL: &str = "gpt-5.5"; +pub const DEFAULT_OPENCODE_ZEN_BASE_URL: &str = "https://opencode.ai/zen/v1"; /// Legacy `deepseek-cn` provider alias. /// /// DeepSeek's official API host is the same worldwide. Keep this alias for diff --git a/crates/tui/src/config_persistence.rs b/crates/tui/src/config_persistence.rs index 58344f190b..845992c5ec 100644 --- a/crates/tui/src/config_persistence.rs +++ b/crates/tui/src/config_persistence.rs @@ -317,6 +317,7 @@ fn provider_base_url_table_key(provider: ApiProvider) -> anyhow::Result<&'static ApiProvider::Stepfun => Ok("stepfun"), ApiProvider::Minimax => Ok("minimax"), ApiProvider::Sakana => Ok("sakana"), + ApiProvider::OpenCodeZen => Ok("opencode_zen"), // Custom providers live under a user-chosen `[providers.]` table, // not a fixed key. Persisting base_url through this static-key path is // out of scope for the #1519 constrained slice; users edit the named diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index a71aec4699..e04606e03f 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -8558,6 +8558,7 @@ fn render(f: &mut Frame, app: &mut App, config: &Config) { crate::config::ApiProvider::DeepseekAnthropic => Some("DS-A"), crate::config::ApiProvider::NvidiaNim => Some("NIM"), crate::config::ApiProvider::Openai => Some("OpenAI"), + crate::config::ApiProvider::OpenCodeZen => Some("Zen"), crate::config::ApiProvider::Anthropic => Some("Claude"), crate::config::ApiProvider::Openmodel => None, crate::config::ApiProvider::Atlascloud => Some("Atlas"), @@ -9769,6 +9770,7 @@ async fn apply_provider_picker_api_key( // Guarded by the outer `if` above; safety net against refactors. return; } + ApiProvider::OpenCodeZen => &mut providers.opencode_zen, ApiProvider::Custom => providers .custom .entry(custom_key.expect("custom key captured for custom provider")) @@ -9849,6 +9851,7 @@ fn set_provider_auth_mode_in_memory(config: &mut Config, provider: ApiProvider, .get_or_insert_with(ProvidersConfig::default); let entry: &mut ProviderConfig = match provider { ApiProvider::Deepseek | ApiProvider::DeepseekCN => return, + ApiProvider::OpenCodeZen => &mut providers.opencode_zen, ApiProvider::Custom => providers .custom .entry(custom_key.expect("custom key captured for custom provider")) diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 4011eea457..3b75c67449 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -32,7 +32,7 @@ The canonical provider IDs are: `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `huggingface`, `together`, `qianfan`, `openai-codex`, `anthropic`, -`openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, and `sakana`. +`openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, and `opencode-zen`. Use any of these surfaces to select a provider: @@ -106,6 +106,7 @@ the listed provider env vars. | `minimax` | `[providers.minimax]` | OpenAI Chat Completions | `MINIMAX_API_KEY` | | `deepinfra` | `[providers.deepinfra]` | OpenAI Chat Completions | `DEEPINFRA_API_KEY`, `DEEPINFRA_TOKEN` | | `sakana` | `[providers.sakana]` | OpenAI Chat Completions | `FUGU_API_KEY`, `SAKANA_API_KEY` | +| `opencode-zen` | `[providers.opencode_zen]` | OpenAI Responses | `OPENCODE_ZEN_API_KEY`, `OPENCODE_API_KEY` | Default base URLs and models for each route are listed in the shipped provider table below. The wire protocol values above are derived from @@ -269,6 +270,7 @@ the same links where possible. | `anthropic` | `[providers.anthropic]` | `ANTHROPIC_API_KEY` | `ANTHROPIC_BASE_URL`; default `https://api.anthropic.com` | `claude-opus-4-8`, `claude-sonnet-4-6` (default), `claude-haiku-4-5` | Native Anthropic Messages API route (`/v1/messages`, `x-api-key` + `anthropic-version: 2023-06-01`) — not OpenAI-compatible. Prompt caching via `cache_control` breakpoints, adaptive thinking + `output_config.effort`, signed thinking blocks replayed verbatim, cache telemetry normalized per #2961. `ANTHROPIC_MODEL` is accepted. | | `openmodel` | `[providers.openmodel]` | `OPENMODEL_API_KEY` | `OPENMODEL_BASE_URL`; default `https://api.openmodel.ai` | `deepseek-v4-flash`; provider-scoped custom model IDs pass through | OpenModel Anthropic-compatible Messages route. Uses `/v1/messages`, Bearer auth, and `anthropic-version: 2023-06-01`; OpenModel selects DeepSeek, DashScope, Xiaomi, Claude, and other routes by model id. `OPENMODEL_MODEL` is accepted. | | `sakana` | `[providers.sakana]` | `FUGU_API_KEY`, `SAKANA_API_KEY` | `SAKANA_BASE_URL`; default `https://api.sakana.ai/v1` | `fugu` (default), `fugu-ultra-20260615` | Sakana AI Fugu OpenAI-compatible route. Standard Chat Completions wire protocol; streaming supported. `fugu-ultra-20260615` is the heavy/reasoning variant. Env var aliases: `FUGU_API_KEY` (primary), `SAKANA_API_KEY`; provider aliases: `sakana-ai`, `sakana_ai`, `fugu`. | +| `opencode-zen` | `[providers.opencode_zen]` | `OPENCODE_ZEN_API_KEY`, `OPENCODE_API_KEY` | `OPENCODE_ZEN_BASE_URL`; default `https://opencode.ai/zen/v1` | `gpt-5.5` (default); `/models` lists all available Zen models dynamically | OpenCode Zen curated model gateway. Uses the OpenAI Responses API wire protocol. Sign up at https://opencode.ai/zen for an API key. Models include GPT 5.5, Claude Opus, Gemini, DeepSeek V4, GLM, Qwen, Kimi, and more. Env var aliases: `OPENCODE_ZEN_API_KEY` (primary), `OPENCODE_API_KEY`; provider aliases: `opencode-zen`, `opencode_zen`, `opencodezen`, `zen`, `opencode`. | ### Hugging Face Provider vs MCP vs Hub