Skip to content
Open
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
15 changes: 13 additions & 2 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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/)
Expand Down
8 changes: 8 additions & 0 deletions crates/agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
21 changes: 21 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>]` tables are handled by
/// the tui-side flatten map; this named slot keeps the canonical
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
}
Expand Down Expand Up @@ -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(),
}
Expand Down Expand Up @@ -4082,6 +4095,8 @@ struct EnvRuntimeOverrides {
deepinfra_model: Option<String>,
sakana_base_url: Option<String>,
sakana_model: Option<String>,
opencode_zen_base_url: Option<String>,
opencode_zen_model: Option<String>,
}

impl EnvRuntimeOverrides {
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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.<name>]` table.
ProviderKind::Custom => None,
Expand Down Expand Up @@ -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,
}?;

Expand Down
65 changes: 54 additions & 11 deletions crates/config/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.<name>]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -708,6 +750,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 30] = [
&MINIMAX,
&DEEPINFRA,
&SAKANA,
&OPENCODE_ZEN,
&CUSTOM,
];

Expand Down
3 changes: 3 additions & 0 deletions crates/config/src/provider_defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
10 changes: 9 additions & 1 deletion crates/config/src/provider_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>]
Expand All @@ -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,
Expand Down Expand Up @@ -141,6 +148,7 @@ impl ProviderKind {
Self::Minimax,
Self::Deepinfra,
Self::Sakana,
Self::OpenCodeZen,
Self::Custom,
];

Expand Down
2 changes: 1 addition & 1 deletion crates/config/src/route/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 3 additions & 1 deletion crates/config/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions crates/tui/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1807,6 +1808,7 @@ pub(super) fn apply_reasoning_effort(
}
ApiProvider::Stepfun => {}
ApiProvider::Sakana => {}
ApiProvider::OpenCodeZen => {}
},
"xhigh" | "max" | "highest" | "ultracode" => match provider {
ApiProvider::Deepseek
Expand Down Expand Up @@ -1876,6 +1878,7 @@ pub(super) fn apply_reasoning_effort(
}
ApiProvider::Stepfun => {}
ApiProvider::Sakana => {}
ApiProvider::OpenCodeZen => {}
},
_ => {}
}
Expand Down
Loading