Add SiliconFlow provider support

Add SiliconFlow as an additive OpenAI-compatible hosted provider across config, secrets, CLI, agent registry, TUI runtime, picker, and docs.

Credit: based in part on the SiliconFlow provider direction from #1864 by @qychen2001, extended here with broader registry, documentation, and test coverage on current main.
This commit is contained in:
Lee-take
2026-05-21 15:06:54 +08:00
committed by Hunter B
parent 84de626883
commit 4861bb2797
18 changed files with 782 additions and 35 deletions
+7 -2
View File
@@ -328,6 +328,10 @@ codewhale --provider novita --model deepseek/deepseek-v4-pro
codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"
codewhale --provider fireworks --model deepseek-v4-pro
# SiliconFlow
codewhale auth set --provider siliconflow --api-key "YOUR_SILICONFLOW_API_KEY"
codewhale --provider siliconflow --model deepseek-ai/DeepSeek-V4-Pro
# Generic OpenAI-compatible endpoint
codewhale auth set --provider openai --api-key "YOUR_OPENAI_COMPATIBLE_API_KEY"
OPENAI_BASE_URL="https://openai-compatible.example/v4" codewhale --provider openai --model glm-5
@@ -498,11 +502,11 @@ Key environment variables:
| `DEEPSEEK_HTTP_HEADERS` | Optional custom model request headers, e.g. `X-Model-Provider-Id=your-model-provider` |
| `DEEPSEEK_MODEL` | Default model |
| `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` | Stream idle timeout in seconds, default `300`, clamped to `1..=3600` |
| `CODEWHALE_PROVIDER` / `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, `ollama` |
| `CODEWHALE_PROVIDER` / `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `moonshot`, `sglang`, `vllm`, `ollama` |
| `DEEPSEEK_PROFILE` | Config profile name |
| `DEEPSEEK_MEMORY` | Set to `on` to enable user memory |
| `DEEPSEEK_ALLOW_INSECURE_HTTP=1` | Allow non-local `http://` API base URLs on trusted networks |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `VOLCENGINE_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `VOLCENGINE_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SILICONFLOW_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
| `OPENAI_BASE_URL` / `OPENAI_MODEL` | Generic OpenAI-compatible endpoint and model ID |
| `ATLASCLOUD_BASE_URL` / `ATLASCLOUD_MODEL` | AtlasCloud endpoint and model override |
| `WANJIE_ARK_BASE_URL` / `WANJIE_ARK_MODEL` | Wanjie Ark endpoint and model override |
@@ -510,6 +514,7 @@ Key environment variables:
| `XIAOMI_MIMO_BASE_URL` / `MIMO_BASE_URL` / `XIAOMI_MIMO_MODEL` / `MIMO_MODEL` | Xiaomi MiMo endpoint and model override |
| `NOVITA_BASE_URL` | Novita endpoint override |
| `FIREWORKS_BASE_URL` | Fireworks endpoint override |
| `SILICONFLOW_BASE_URL` / `SILICONFLOW_MODEL` | SiliconFlow endpoint and model override |
| `SGLANG_BASE_URL` | Self-hosted SGLang endpoint |
| `SGLANG_MODEL` | Self-hosted SGLang model ID |
| `VLLM_BASE_URL` | Self-hosted vLLM endpoint |
+7 -2
View File
@@ -275,6 +275,10 @@ codewhale --provider novita --model deepseek/deepseek-v4-pro
codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"
codewhale --provider fireworks --model deepseek-v4-pro
# SiliconFlow
codewhale auth set --provider siliconflow --api-key "YOUR_SILICONFLOW_API_KEY"
codewhale --provider siliconflow --model deepseek-ai/DeepSeek-V4-Pro
# 通用 OpenAI 兼容端点
codewhale auth set --provider openai --api-key "YOUR_OPENAI_COMPATIBLE_API_KEY"
OPENAI_BASE_URL="https://openai-compatible.example/v4" codewhale --provider openai --model glm-5
@@ -409,11 +413,11 @@ DeepSeek 可作为自定义 Agent Client Protocol 服务器运行,供 Zed 等
| `DEEPSEEK_HTTP_HEADERS` | 可选模型请求头,例如 `X-Model-Provider-Id=your-model-provider` |
| `DEEPSEEK_MODEL` | 默认模型 |
| `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` | 流式响应空闲超时秒数,默认 `300`,限制在 `1..=3600` |
| `DEEPSEEK_PROVIDER` | `codewhale`(默认)、`nvidia-nim``openai``atlascloud``wanjie-ark``openrouter``xiaomi-mimo``novita``fireworks``sglang``vllm``ollama` |
| `CODEWHALE_PROVIDER` / `DEEPSEEK_PROVIDER` | `deepseek`(默认)、`nvidia-nim``openai``atlascloud``wanjie-ark``openrouter``xiaomi-mimo``novita``fireworks``siliconflow``moonshot``sglang``vllm``ollama` |
| `DEEPSEEK_PROFILE` | 配置 profile 名称 |
| `DEEPSEEK_MEMORY` | 设为 `on` 启用用户记忆 |
| `DEEPSEEK_ALLOW_INSECURE_HTTP=1` | 在可信网络上允许非本机 `http://` API base URL |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | 提供商认证 |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SILICONFLOW_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | 提供商认证 |
| `OPENAI_BASE_URL` / `OPENAI_MODEL` | 通用 OpenAI 兼容端点和模型 ID |
| `ATLASCLOUD_BASE_URL` / `ATLASCLOUD_MODEL` | AtlasCloud 端点和模型覆盖 |
| `WANJIE_ARK_BASE_URL` / `WANJIE_ARK_MODEL` | Wanjie Ark 端点和模型覆盖 |
@@ -421,6 +425,7 @@ DeepSeek 可作为自定义 Agent Client Protocol 服务器运行,供 Zed 等
| `XIAOMI_MIMO_BASE_URL` / `MIMO_BASE_URL` / `XIAOMI_MIMO_MODEL` / `MIMO_MODEL` | Xiaomi MiMo 端点和模型覆盖 |
| `NOVITA_BASE_URL` | Novita 端点覆盖 |
| `FIREWORKS_BASE_URL` | Fireworks 端点覆盖 |
| `SILICONFLOW_BASE_URL` / `SILICONFLOW_MODEL` | SiliconFlow 端点和模型覆盖 |
| `SGLANG_BASE_URL` | 自托管 SGLang 端点 |
| `SGLANG_MODEL` | 自托管 SGLang 模型 ID |
| `VLLM_BASE_URL` | 自托管 vLLM 端点 |
+16 -6
View File
@@ -13,12 +13,13 @@
# `[providers.*]` sections near the bottom of
# this file — keeping both stored at once means `/provider deepseek` and
# `/provider nvidia-nim` (or `--provider openai`, `--provider wanjie-ark`,
# `--provider volcengine`, `--provider xiaomi-mimo`, `--provider fireworks`, `/provider sglang`,
# `/provider vllm`, `/provider ollama`) toggle without having to re-enter keys.
# Top-level `api_key` / `base_url` are
# `--provider volcengine`, `--provider xiaomi-mimo`, `--provider fireworks`,
# `--provider siliconflow`, `/provider sglang`, `/provider vllm`,
# `/provider ollama`) toggle without having to re-enter keys. Top-level
# `api_key` / `base_url` are
# still read as DeepSeek defaults when `[providers.deepseek]` is absent
# (backward compatibility).
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | sglang | vllm | ollama
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | sglang | vllm | ollama
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)
@@ -40,6 +41,8 @@ base_url = "https://api.deepseek.com/beta"
# deepseek-reasoner — default Wanjie Ark model ID
# mimo-v2.5-pro — default Xiaomi MiMo model ID
# accounts/fireworks/models/deepseek-v4-pro — Fireworks AI Pro model ID
# deepseek-ai/DeepSeek-V4-Pro — SiliconFlow Pro model ID
# deepseek-ai/DeepSeek-V4-Flash — SiliconFlow Flash model ID
# deepseek-ai/DeepSeek-V4-Pro — SGLang self-hosted Pro model ID
# deepseek-ai/DeepSeek-V4-Flash — SGLang self-hosted Flash model ID
default_text_model = "deepseek-v4-pro"
@@ -180,8 +183,8 @@ max_subagents = 10 # optional (1-20)
# ─────────────────────────────────────────────────────────────────────────────────
# Providers can be stored at once; `provider = "..."` (top of file) or
# `/provider deepseek` / `/provider nvidia-nim` / `--provider openai` /
# `--provider wanjie-ark` / `/provider fireworks` switches between them without
# having to re-enter keys. Env vars override anything set here:
# `--provider wanjie-ark` / `/provider fireworks` / `--provider siliconflow`
# switches between them without having to re-enter keys. Env vars override anything set here:
# DeepSeek: DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL
# NIM: NVIDIA_API_KEY (or NVIDIA_NIM_API_KEY), NIM_BASE_URL
# (or NVIDIA_NIM_BASE_URL / NVIDIA_BASE_URL), NVIDIA_NIM_MODEL
@@ -191,6 +194,7 @@ max_subagents = 10 # optional (1-20)
# Xiaomi MiMo: XIAOMI_MIMO_API_KEY (or MIMO_API_KEY), XIAOMI_MIMO_BASE_URL, XIAOMI_MIMO_MODEL
# Novita: NOVITA_API_KEY, NOVITA_BASE_URL, NOVITA_MODEL
# Fireworks: FIREWORKS_API_KEY, FIREWORKS_BASE_URL
# SiliconFlow: SILICONFLOW_API_KEY, SILICONFLOW_BASE_URL, SILICONFLOW_MODEL
# SGLang: SGLANG_BASE_URL, SGLANG_MODEL, optional SGLANG_API_KEY
# vLLM: VLLM_BASE_URL, VLLM_MODEL, optional VLLM_API_KEY
# Ollama: OLLAMA_BASE_URL, OLLAMA_MODEL, optional OLLAMA_API_KEY
@@ -271,6 +275,12 @@ max_subagents = 10 # optional (1-20)
# base_url = "https://api.fireworks.ai/inference/v1"
# model = "accounts/fireworks/models/deepseek-v4-pro"
# SiliconFlow-hosted DeepSeek V4 (https://siliconflow.com)
[providers.siliconflow]
# api_key = "YOUR_SILICONFLOW_API_KEY"
# base_url = "https://api.siliconflow.com/v1"
# model = "deepseek-ai/DeepSeek-V4-Pro" # or deepseek-ai/DeepSeek-V4-Flash
# Self-hosted SGLang OpenAI-compatible server
[providers.sglang]
# api_key = "OPTIONAL_SGLANG_TOKEN"
+52
View File
@@ -188,6 +188,30 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
provider: ProviderKind::Siliconflow,
aliases: vec![
"deepseek-v4-pro".to_string(),
"deepseek-reasoner".to_string(),
"deepseek-r1".to_string(),
"siliconflow-deepseek-v4-pro".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
provider: ProviderKind::Siliconflow,
aliases: vec![
"deepseek-v4-flash".to_string(),
"deepseek-chat".to_string(),
"deepseek-v3".to_string(),
"siliconflow-deepseek-v4-flash".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "kimi-k2.6".to_string(),
provider: ProviderKind::Moonshot,
@@ -460,6 +484,34 @@ mod tests {
);
}
#[test]
fn siliconflow_default_uses_canonical_pro_model_id() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
assert!(resolved.resolved.supports_reasoning);
}
#[test]
fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
}
#[test]
fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
}
#[test]
fn sglang_default_uses_canonical_model_id() {
let registry = ModelRegistry::default();
+28 -2
View File
@@ -33,6 +33,7 @@ enum ProviderArg {
XiaomiMimo,
Novita,
Fireworks,
Siliconflow,
Moonshot,
Sglang,
Vllm,
@@ -52,6 +53,7 @@ impl From<ProviderArg> for ProviderKind {
ProviderArg::XiaomiMimo => ProviderKind::XiaomiMimo,
ProviderArg::Novita => ProviderKind::Novita,
ProviderArg::Fireworks => ProviderKind::Fireworks,
ProviderArg::Siliconflow => ProviderKind::Siliconflow,
ProviderArg::Moonshot => ProviderKind::Moonshot,
ProviderArg::Sglang => ProviderKind::Sglang,
ProviderArg::Vllm => ProviderKind::Vllm,
@@ -732,6 +734,7 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
ProviderKind::XiaomiMimo => "xiaomi-mimo",
ProviderKind::Novita => "novita",
ProviderKind::Fireworks => "fireworks",
ProviderKind::Siliconflow => "siliconflow",
ProviderKind::Moonshot => "moonshot",
ProviderKind::Sglang => "sglang",
ProviderKind::Vllm => "vllm",
@@ -740,7 +743,7 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
}
/// Provider order used by the `auth list` and `auth status` outputs.
const PROVIDER_LIST: [ProviderKind; 14] = [
const PROVIDER_LIST: [ProviderKind; 15] = [
ProviderKind::Deepseek,
ProviderKind::NvidiaNim,
ProviderKind::Openai,
@@ -751,6 +754,7 @@ const PROVIDER_LIST: [ProviderKind; 14] = [
ProviderKind::XiaomiMimo,
ProviderKind::Novita,
ProviderKind::Fireworks,
ProviderKind::Siliconflow,
ProviderKind::Moonshot,
ProviderKind::Sglang,
ProviderKind::Vllm,
@@ -807,6 +811,7 @@ fn provider_env_vars(provider: ProviderKind) -> &'static [&'static str] {
ProviderKind::Novita => &["NOVITA_API_KEY"],
ProviderKind::NvidiaNim => &["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"],
ProviderKind::Fireworks => &["FIREWORKS_API_KEY"],
ProviderKind::Siliconflow => &["SILICONFLOW_API_KEY"],
ProviderKind::Moonshot => &["MOONSHOT_API_KEY", "KIMI_API_KEY"],
ProviderKind::Sglang => &["SGLANG_API_KEY"],
ProviderKind::Vllm => &["VLLM_API_KEY"],
@@ -1496,13 +1501,14 @@ fn build_tui_command(
| ProviderKind::XiaomiMimo
| ProviderKind::Novita
| ProviderKind::Fireworks
| ProviderKind::Siliconflow
| ProviderKind::Moonshot
| ProviderKind::Sglang
| ProviderKind::Vllm
| ProviderKind::Ollama
) {
bail!(
"The interactive TUI supports DeepSeek, NVIDIA NIM, OpenAI-compatible, AtlasCloud, Wanjie Ark, OpenRouter, Xiaomi MiMo, Novita, Fireworks, Moonshot/Kimi, SGLang, vLLM, and Ollama providers. Remove --provider {} or use `codewhale model ...` for provider registry inspection.",
"The interactive TUI supports DeepSeek, NVIDIA NIM, OpenAI-compatible, AtlasCloud, Wanjie Ark, OpenRouter, Xiaomi MiMo, Novita, Fireworks, SiliconFlow, Moonshot/Kimi, SGLang, vLLM, and Ollama providers. Remove --provider {} or use `codewhale model ...` for provider registry inspection.",
resolved_runtime.provider.as_str()
);
}
@@ -1566,6 +1572,9 @@ fn build_tui_command(
if resolved_runtime.provider == ProviderKind::Volcengine {
cmd.env("VOLCENGINE_API_KEY", api_key);
}
if resolved_runtime.provider == ProviderKind::Siliconflow {
cmd.env("SILICONFLOW_API_KEY", api_key);
}
cmd.env("DEEPSEEK_API_KEY_SOURCE", "cli");
}
if let Some(base_url) = cli.base_url.as_ref() {
@@ -2200,6 +2209,18 @@ mod tests {
}))
));
let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "siliconflow"]);
assert!(matches!(
cli.command,
Some(Commands::Auth(AuthArgs {
command: AuthCommand::Set {
provider: ProviderArg::Siliconflow,
api_key: None,
api_key_stdin: false,
}
}))
));
let cli = parse_ok(&["deepseek", "auth", "set", "--provider", "moonshot"]);
assert!(matches!(
cli.command,
@@ -2941,6 +2962,11 @@ mod tests {
&["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY"],
),
(ProviderKind::Fireworks, "fireworks", &["FIREWORKS_API_KEY"]),
(
ProviderKind::Siliconflow,
"siliconflow",
&["SILICONFLOW_API_KEY"],
),
(ProviderKind::Sglang, "sglang", &["SGLANG_API_KEY"]),
(ProviderKind::Vllm, "vllm", &["VLLM_API_KEY"]),
(ProviderKind::Ollama, "ollama", &["OLLAMA_API_KEY"]),
+255 -4
View File
@@ -33,6 +33,8 @@ const DEFAULT_XIAOMI_MIMO_MODEL: &str = "mimo-v2.5-pro";
const DEFAULT_NOVITA_MODEL: &str = "deepseek/deepseek-v4-pro";
const DEFAULT_NOVITA_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
const DEFAULT_FIREWORKS_MODEL: &str = "accounts/fireworks/models/deepseek-v4-pro";
const DEFAULT_SILICONFLOW_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro";
const DEFAULT_SILICONFLOW_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
const DEFAULT_MOONSHOT_MODEL: &str = "kimi-k2.6";
const DEFAULT_MOONSHOT_BASE_URL: &str = "https://api.moonshot.ai/v1";
const DEFAULT_KIMI_CODE_MODEL: &str = "kimi-for-coding";
@@ -43,6 +45,7 @@ const DEFAULT_OPENROUTER_BASE_URL: &str = "https://openrouter.ai/api/v1";
const DEFAULT_XIAOMI_MIMO_BASE_URL: &str = "https://api.xiaomimimo.com/v1";
const DEFAULT_NOVITA_BASE_URL: &str = "https://api.novita.ai/v1";
const DEFAULT_FIREWORKS_BASE_URL: &str = "https://api.fireworks.ai/inference/v1";
const DEFAULT_SILICONFLOW_BASE_URL: &str = "https://api.siliconflow.com/v1";
const DEFAULT_SGLANG_BASE_URL: &str = "http://localhost:30000/v1";
const DEFAULT_VLLM_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro";
const DEFAULT_VLLM_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
@@ -81,6 +84,8 @@ pub enum ProviderKind {
XiaomiMimo,
Novita,
Fireworks,
#[serde(alias = "silicon-flow", alias = "silicon_flow")]
Siliconflow,
Moonshot,
Sglang,
Vllm,
@@ -101,6 +106,7 @@ impl ProviderKind {
Self::XiaomiMimo => "xiaomi-mimo",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
Self::Siliconflow => "siliconflow",
Self::Moonshot => "moonshot",
Self::Sglang => "sglang",
Self::Vllm => "vllm",
@@ -126,6 +132,7 @@ impl ProviderKind {
}
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
"siliconflow" | "silicon-flow" | "silicon_flow" => Some(Self::Siliconflow),
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => Some(Self::Moonshot),
"sglang" | "sg-lang" => Some(Self::Sglang),
"vllm" | "v-llm" => Some(Self::Vllm),
@@ -168,6 +175,8 @@ pub struct ProvidersToml {
#[serde(default)]
pub fireworks: ProviderConfigToml,
#[serde(default)]
pub siliconflow: ProviderConfigToml,
#[serde(default)]
pub moonshot: ProviderConfigToml,
#[serde(default)]
pub sglang: ProviderConfigToml,
@@ -191,6 +200,7 @@ impl ProvidersToml {
ProviderKind::XiaomiMimo => &self.xiaomi_mimo,
ProviderKind::Novita => &self.novita,
ProviderKind::Fireworks => &self.fireworks,
ProviderKind::Siliconflow => &self.siliconflow,
ProviderKind::Moonshot => &self.moonshot,
ProviderKind::Sglang => &self.sglang,
ProviderKind::Vllm => &self.vllm,
@@ -210,6 +220,7 @@ impl ProvidersToml {
ProviderKind::XiaomiMimo => &mut self.xiaomi_mimo,
ProviderKind::Novita => &mut self.novita,
ProviderKind::Fireworks => &mut self.fireworks,
ProviderKind::Siliconflow => &mut self.siliconflow,
ProviderKind::Moonshot => &mut self.moonshot,
ProviderKind::Sglang => &mut self.sglang,
ProviderKind::Vllm => &mut self.vllm,
@@ -434,6 +445,10 @@ impl ConfigToml {
);
merge_project_provider_config(&mut self.providers.novita, &project.providers.novita);
merge_project_provider_config(&mut self.providers.fireworks, &project.providers.fireworks);
merge_project_provider_config(
&mut self.providers.siliconflow,
&project.providers.siliconflow,
);
merge_project_provider_config(&mut self.providers.sglang, &project.providers.sglang);
merge_project_provider_config(&mut self.providers.vllm, &project.providers.vllm);
merge_project_provider_config(&mut self.providers.ollama, &project.providers.ollama);
@@ -512,6 +527,12 @@ impl ConfigToml {
"providers.fireworks.http_headers" => {
serialize_http_headers(&self.providers.fireworks.http_headers)
}
"providers.siliconflow.api_key" => self.providers.siliconflow.api_key.clone(),
"providers.siliconflow.base_url" => self.providers.siliconflow.base_url.clone(),
"providers.siliconflow.model" => self.providers.siliconflow.model.clone(),
"providers.siliconflow.http_headers" => {
serialize_http_headers(&self.providers.siliconflow.http_headers)
}
"providers.moonshot.api_key" => self.providers.moonshot.api_key.clone(),
"providers.moonshot.base_url" => self.providers.moonshot.base_url.clone(),
"providers.moonshot.model" => self.providers.moonshot.model.clone(),
@@ -690,6 +711,18 @@ impl ConfigToml {
"providers.fireworks.http_headers" => {
self.providers.fireworks.http_headers = parse_http_headers(value)?;
}
"providers.siliconflow.api_key" => {
self.providers.siliconflow.api_key = Some(value.to_string());
}
"providers.siliconflow.base_url" => {
self.providers.siliconflow.base_url = Some(value.to_string());
}
"providers.siliconflow.model" => {
self.providers.siliconflow.model = Some(value.to_string());
}
"providers.siliconflow.http_headers" => {
self.providers.siliconflow.http_headers = parse_http_headers(value)?;
}
"providers.moonshot.api_key" => {
self.providers.moonshot.api_key = Some(value.to_string());
}
@@ -818,6 +851,12 @@ impl ConfigToml {
"providers.fireworks.base_url" => self.providers.fireworks.base_url = None,
"providers.fireworks.model" => self.providers.fireworks.model = None,
"providers.fireworks.http_headers" => self.providers.fireworks.http_headers.clear(),
"providers.siliconflow.api_key" => self.providers.siliconflow.api_key = None,
"providers.siliconflow.base_url" => self.providers.siliconflow.base_url = None,
"providers.siliconflow.model" => self.providers.siliconflow.model = None,
"providers.siliconflow.http_headers" => {
self.providers.siliconflow.http_headers.clear();
}
"providers.moonshot.api_key" => self.providers.moonshot.api_key = None,
"providers.moonshot.base_url" => self.providers.moonshot.base_url = None,
"providers.moonshot.model" => self.providers.moonshot.model = None,
@@ -1003,6 +1042,21 @@ impl ConfigToml {
if let Some(v) = serialize_http_headers(&self.providers.fireworks.http_headers) {
out.insert("providers.fireworks.http_headers".to_string(), v);
}
if let Some(v) = self.providers.siliconflow.api_key.as_ref() {
out.insert(
"providers.siliconflow.api_key".to_string(),
redact_secret(v),
);
}
if let Some(v) = self.providers.siliconflow.base_url.as_ref() {
out.insert("providers.siliconflow.base_url".to_string(), v.clone());
}
if let Some(v) = self.providers.siliconflow.model.as_ref() {
out.insert("providers.siliconflow.model".to_string(), v.clone());
}
if let Some(v) = serialize_http_headers(&self.providers.siliconflow.http_headers) {
out.insert("providers.siliconflow.http_headers".to_string(), v);
}
if let Some(v) = self.providers.moonshot.api_key.as_ref() {
out.insert("providers.moonshot.api_key".to_string(), redact_secret(v));
}
@@ -1120,6 +1174,7 @@ impl ConfigToml {
ProviderKind::XiaomiMimo => DEFAULT_XIAOMI_MIMO_BASE_URL.to_string(),
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL.to_string(),
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL.to_string(),
ProviderKind::Siliconflow => DEFAULT_SILICONFLOW_BASE_URL.to_string(),
ProviderKind::Moonshot => {
if auth_mode.as_deref().is_some_and(auth_mode_uses_kimi_oauth) {
DEFAULT_KIMI_CODE_BASE_URL.to_string()
@@ -1163,9 +1218,10 @@ impl ConfigToml {
}
};
let env_provider_model = env.model_for(provider, &base_url);
let explicit_model = cli.model.is_some()
|| env.model.is_some()
|| env.model_for(provider).is_some()
|| env_provider_model.is_some()
|| provider_cfg.model.is_some()
|| root_deepseek_model.is_some()
|| self.model.is_some();
@@ -1173,7 +1229,7 @@ impl ConfigToml {
.model
.clone()
.or_else(|| env.model.clone())
.or_else(|| env.model_for(provider))
.or(env_provider_model)
.or_else(|| provider_cfg.model.clone())
.or(root_deepseek_model)
.or_else(|| self.model.clone())
@@ -1358,6 +1414,14 @@ fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String {
(ProviderKind::Fireworks, "deepseek-v4-pro" | "deepseek-v4pro") => {
DEFAULT_FIREWORKS_MODEL.to_string()
}
(
ProviderKind::Siliconflow,
"deepseek-v4-pro" | "deepseek-v4pro" | "deepseek-reasoner" | "deepseek-r1",
) => DEFAULT_SILICONFLOW_MODEL.to_string(),
(
ProviderKind::Siliconflow,
"deepseek-v4-flash" | "deepseek-v4flash" | "deepseek-chat" | "deepseek-v3",
) => DEFAULT_SILICONFLOW_FLASH_MODEL.to_string(),
(ProviderKind::Moonshot, "kimi-k2.6" | "kimi-k2") => DEFAULT_MOONSHOT_MODEL.to_string(),
(ProviderKind::Sglang, "deepseek-v4-pro" | "deepseek-v4pro") => {
DEFAULT_SGLANG_MODEL.to_string()
@@ -1391,6 +1455,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::XiaomiMimo => DEFAULT_XIAOMI_MIMO_MODEL,
ProviderKind::Novita => DEFAULT_NOVITA_MODEL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_MODEL,
ProviderKind::Siliconflow => DEFAULT_SILICONFLOW_MODEL,
ProviderKind::Moonshot => DEFAULT_MOONSHOT_MODEL,
ProviderKind::Sglang => DEFAULT_SGLANG_MODEL,
ProviderKind::Vllm => DEFAULT_VLLM_MODEL,
@@ -1410,6 +1475,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::XiaomiMimo => DEFAULT_XIAOMI_MIMO_BASE_URL,
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
ProviderKind::Siliconflow => DEFAULT_SILICONFLOW_BASE_URL,
ProviderKind::Moonshot => DEFAULT_MOONSHOT_BASE_URL,
ProviderKind::Sglang => DEFAULT_SGLANG_BASE_URL,
ProviderKind::Vllm => DEFAULT_VLLM_BASE_URL,
@@ -1425,11 +1491,21 @@ fn moonshot_base_url_uses_kimi_code(base_url: &str) -> bool {
}
fn base_url_is_custom_for_provider(provider: ProviderKind, base_url: &str) -> bool {
if provider == ProviderKind::Siliconflow && siliconflow_base_url_is_official(base_url) {
return false;
}
let actual = base_url.trim_end_matches('/');
let default = default_base_url_for_provider(provider).trim_end_matches('/');
actual != default
}
fn siliconflow_base_url_is_official(base_url: &str) -> bool {
matches!(
base_url.trim_end_matches('/').to_ascii_lowercase().as_str(),
"https://api.siliconflow.com/v1" | "https://api.siliconflow.cn/v1"
)
}
fn provider_preserves_custom_base_url_model(provider: ProviderKind, base_url: &str) -> bool {
base_url_is_custom_for_provider(provider, base_url)
}
@@ -1923,6 +1999,8 @@ struct EnvRuntimeOverrides {
xiaomi_mimo_base_url: Option<String>,
novita_base_url: Option<String>,
fireworks_base_url: Option<String>,
siliconflow_base_url: Option<String>,
siliconflow_model: Option<String>,
moonshot_base_url: Option<String>,
sglang_base_url: Option<String>,
vllm_base_url: Option<String>,
@@ -2012,6 +2090,12 @@ impl EnvRuntimeOverrides {
fireworks_base_url: std::env::var("FIREWORKS_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
siliconflow_base_url: std::env::var("SILICONFLOW_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
siliconflow_model: std::env::var("SILICONFLOW_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
moonshot_base_url: std::env::var("MOONSHOT_BASE_URL")
.or_else(|_| std::env::var("KIMI_BASE_URL"))
.ok()
@@ -2042,6 +2126,7 @@ impl EnvRuntimeOverrides {
ProviderKind::XiaomiMimo => self.xiaomi_mimo_base_url.clone(),
ProviderKind::Novita => self.novita_base_url.clone(),
ProviderKind::Fireworks => self.fireworks_base_url.clone(),
ProviderKind::Siliconflow => self.siliconflow_base_url.clone(),
ProviderKind::Moonshot => self.moonshot_base_url.clone(),
ProviderKind::Sglang => self.sglang_base_url.clone(),
ProviderKind::Vllm => self.vllm_base_url.clone(),
@@ -2049,13 +2134,20 @@ impl EnvRuntimeOverrides {
}
}
fn model_for(&self, provider: ProviderKind) -> Option<String> {
match provider {
fn model_for(&self, provider: ProviderKind, base_url: &str) -> Option<String> {
let model = match provider {
ProviderKind::WanjieArk => self.wanjie_ark_model.clone(),
ProviderKind::Volcengine => self.volcengine_model.clone(),
ProviderKind::Siliconflow => self.siliconflow_model.clone(),
ProviderKind::Moonshot => self.moonshot_model.clone(),
ProviderKind::XiaomiMimo => self.xiaomi_mimo_model.clone(),
_ => None,
}?;
if provider_preserves_custom_base_url_model(provider, base_url) {
Some(model.trim().to_string())
} else {
Some(normalize_model_for_provider(provider, &model))
}
}
}
@@ -2121,6 +2213,9 @@ mod tests {
novita_base_url: Option<OsString>,
fireworks_api_key: Option<OsString>,
fireworks_base_url: Option<OsString>,
siliconflow_api_key: Option<OsString>,
siliconflow_base_url: Option<OsString>,
siliconflow_model: Option<OsString>,
moonshot_api_key: Option<OsString>,
moonshot_base_url: Option<OsString>,
moonshot_model: Option<OsString>,
@@ -2177,6 +2272,9 @@ mod tests {
novita_base_url: env::var_os("NOVITA_BASE_URL"),
fireworks_api_key: env::var_os("FIREWORKS_API_KEY"),
fireworks_base_url: env::var_os("FIREWORKS_BASE_URL"),
siliconflow_api_key: env::var_os("SILICONFLOW_API_KEY"),
siliconflow_base_url: env::var_os("SILICONFLOW_BASE_URL"),
siliconflow_model: env::var_os("SILICONFLOW_MODEL"),
moonshot_api_key: env::var_os("MOONSHOT_API_KEY"),
moonshot_base_url: env::var_os("MOONSHOT_BASE_URL"),
moonshot_model: env::var_os("MOONSHOT_MODEL"),
@@ -2227,6 +2325,9 @@ mod tests {
env::remove_var("NOVITA_BASE_URL");
env::remove_var("FIREWORKS_API_KEY");
env::remove_var("FIREWORKS_BASE_URL");
env::remove_var("SILICONFLOW_API_KEY");
env::remove_var("SILICONFLOW_BASE_URL");
env::remove_var("SILICONFLOW_MODEL");
env::remove_var("MOONSHOT_API_KEY");
env::remove_var("MOONSHOT_BASE_URL");
env::remove_var("MOONSHOT_MODEL");
@@ -2295,6 +2396,9 @@ mod tests {
Self::restore_var("NOVITA_BASE_URL", self.novita_base_url.take());
Self::restore_var("FIREWORKS_API_KEY", self.fireworks_api_key.take());
Self::restore_var("FIREWORKS_BASE_URL", self.fireworks_base_url.take());
Self::restore_var("SILICONFLOW_API_KEY", self.siliconflow_api_key.take());
Self::restore_var("SILICONFLOW_BASE_URL", self.siliconflow_base_url.take());
Self::restore_var("SILICONFLOW_MODEL", self.siliconflow_model.take());
Self::restore_var("MOONSHOT_API_KEY", self.moonshot_api_key.take());
Self::restore_var("MOONSHOT_BASE_URL", self.moonshot_base_url.take());
Self::restore_var("MOONSHOT_MODEL", self.moonshot_model.take());
@@ -2881,6 +2985,14 @@ mod tests {
ProviderKind::parse("fireworks-ai"),
Some(ProviderKind::Fireworks)
);
assert_eq!(
ProviderKind::parse("silicon-flow"),
Some(ProviderKind::Siliconflow)
);
assert_eq!(
ProviderKind::parse("silicon_flow"),
Some(ProviderKind::Siliconflow)
);
assert_eq!(ProviderKind::parse("kimi"), Some(ProviderKind::Moonshot));
assert_eq!(
ProviderKind::parse("moonshot-ai"),
@@ -2906,6 +3018,10 @@ mod tests {
let parsed: ConfigToml =
toml::from_str("provider = \"ark-wanjie\"").expect("wanjie provider alias");
assert_eq!(parsed.provider, ProviderKind::WanjieArk);
let parsed: ConfigToml =
toml::from_str("provider = \"silicon-flow\"").expect("siliconflow provider alias");
assert_eq!(parsed.provider, ProviderKind::Siliconflow);
}
#[test]
@@ -2988,6 +3104,22 @@ mod tests {
assert_eq!(resolved.model, DEFAULT_FIREWORKS_MODEL);
}
#[test]
fn siliconflow_provider_defaults_to_canonical_endpoint_and_model() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let config = ConfigToml {
provider: ProviderKind::Siliconflow,
..ConfigToml::default()
};
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.base_url, DEFAULT_SILICONFLOW_BASE_URL);
assert_eq!(resolved.model, DEFAULT_SILICONFLOW_MODEL);
}
#[test]
fn moonshot_provider_defaults_to_kimi_k2() {
let _lock = env_lock();
@@ -3417,6 +3549,56 @@ mod tests {
assert_eq!(resolved.base_url, DEFAULT_FIREWORKS_BASE_URL);
}
#[test]
fn siliconflow_env_overrides_key_base_url_and_model() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
// Safety: test-only environment mutation guarded by a module mutex.
unsafe {
env::set_var("CODEWHALE_PROVIDER", "siliconflow");
env::set_var("SILICONFLOW_API_KEY", "sf-env-key");
env::set_var("SILICONFLOW_BASE_URL", "https://sf-mirror.example/v1");
env::set_var("SILICONFLOW_MODEL", "deepseek-v4-flash");
}
let resolved =
ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.api_key.as_deref(), Some("sf-env-key"));
assert_eq!(resolved.base_url, "https://sf-mirror.example/v1");
assert_eq!(resolved.model, "deepseek-v4-flash");
}
#[test]
fn siliconflow_cn_base_url_env_normalizes_model_aliases() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
// Safety: test-only environment mutation guarded by a module mutex.
unsafe {
env::set_var("CODEWHALE_PROVIDER", "siliconflow");
env::set_var("SILICONFLOW_API_KEY", "sf-env-key");
env::set_var("SILICONFLOW_BASE_URL", "https://api.siliconflow.cn/v1");
}
for (alias, expected) in [
("deepseek-v4-flash", DEFAULT_SILICONFLOW_FLASH_MODEL),
("deepseek-reasoner", DEFAULT_SILICONFLOW_MODEL),
] {
// Safety: test-only environment mutation guarded by a module mutex.
unsafe {
env::set_var("SILICONFLOW_MODEL", alias);
}
let resolved =
ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.base_url, "https://api.siliconflow.cn/v1");
assert_eq!(resolved.model, expected);
}
}
#[test]
fn wanjie_ark_env_api_key_and_base_url_fall_back_when_config_missing() {
let _lock = env_lock();
@@ -3470,6 +3652,57 @@ mod tests {
assert_eq!(resolved.model, DEFAULT_NOVITA_FLASH_MODEL);
}
#[test]
fn siliconflow_provider_normalizes_flash_aliases() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let cli = CliRuntimeOverrides {
provider: Some(ProviderKind::Siliconflow),
model: Some("deepseek-v4-flash".to_string()),
..CliRuntimeOverrides::default()
};
let resolved = ConfigToml::default().resolve_runtime_options(&cli);
assert_eq!(resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.model, DEFAULT_SILICONFLOW_FLASH_MODEL);
}
#[test]
fn siliconflow_provider_normalizes_reasoning_aliases_to_pro() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
for alias in ["deepseek-reasoner", "deepseek-r1"] {
let cli = CliRuntimeOverrides {
provider: Some(ProviderKind::Siliconflow),
model: Some(alias.to_string()),
..CliRuntimeOverrides::default()
};
let resolved = ConfigToml::default().resolve_runtime_options(&cli);
assert_eq!(resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.model, DEFAULT_SILICONFLOW_MODEL);
}
}
#[test]
fn siliconflow_provider_preserves_deepseek_v3_2_alias() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let cli = CliRuntimeOverrides {
provider: Some(ProviderKind::Siliconflow),
model: Some("deepseek-v3.2".to_string()),
..CliRuntimeOverrides::default()
};
let resolved = ConfigToml::default().resolve_runtime_options(&cli);
assert_eq!(resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.model, "deepseek-v3.2");
}
#[test]
fn sglang_provider_normalizes_flash_aliases() {
let _lock = env_lock();
@@ -3556,6 +3789,24 @@ mod tests {
assert_eq!(resolved.model, "DeepSeek-V4-Pro");
}
#[test]
fn siliconflow_custom_base_url_preserves_provider_model() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let mut config = ConfigToml {
provider: ProviderKind::Siliconflow,
..ConfigToml::default()
};
config.providers.siliconflow.base_url = Some("https://my-gateway.example/v1".to_string());
config.providers.siliconflow.model = Some("DeepSeek-V4-Pro".to_string());
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Siliconflow);
assert_eq!(resolved.base_url, "https://my-gateway.example/v1");
assert_eq!(resolved.model, "DeepSeek-V4-Pro");
}
#[test]
fn config_file_resolves_above_env_and_keyring() {
use codewhale_secrets::KeyringStore;
+16
View File
@@ -536,6 +536,7 @@ pub fn env_for(name: &str) -> Option<String> {
&["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"]
}
"fireworks" | "fireworks-ai" => &["FIREWORKS_API_KEY"],
"siliconflow" | "silicon-flow" | "silicon_flow" => &["SILICONFLOW_API_KEY"],
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => &["MOONSHOT_API_KEY", "KIMI_API_KEY"],
"sglang" | "sg-lang" => &["SGLANG_API_KEY"],
"vllm" | "v-llm" => &["VLLM_API_KEY"],
@@ -588,6 +589,7 @@ mod tests {
"NVIDIA_API_KEY",
"NVIDIA_NIM_API_KEY",
"FIREWORKS_API_KEY",
"SILICONFLOW_API_KEY",
"SGLANG_API_KEY",
"VLLM_API_KEY",
"OLLAMA_API_KEY",
@@ -802,6 +804,20 @@ mod tests {
unsafe { std::env::remove_var("FIREWORKS_API_KEY") };
}
#[test]
fn siliconflow_env_aliases_resolve() {
let _lock = env_lock();
clear_known_envs();
// Safety: env mutation guarded by env_lock().
unsafe { std::env::set_var("SILICONFLOW_API_KEY", "sf-key") };
assert_eq!(env_for("siliconflow").as_deref(), Some("sf-key"));
assert_eq!(env_for("silicon-flow").as_deref(), Some("sf-key"));
assert_eq!(env_for("silicon_flow").as_deref(), Some("sf-key"));
// Safety: env mutation guarded by env_lock().
unsafe { std::env::remove_var("SILICONFLOW_API_KEY") };
}
#[test]
fn moonshot_kimi_env_aliases_resolve() {
let _lock = env_lock();
+3
View File
@@ -884,6 +884,7 @@ pub(super) fn apply_reasoning_effort(
| ApiProvider::Openrouter
| ApiProvider::XiaomiMimo
| ApiProvider::Novita
| ApiProvider::Siliconflow
| ApiProvider::Sglang
| ApiProvider::Volcengine => {
body["thinking"] = json!({ "type": "disabled" });
@@ -918,6 +919,7 @@ pub(super) fn apply_reasoning_effort(
// DeepSeek compatibility: low/medium both map to high
ApiProvider::Deepseek
| ApiProvider::DeepseekCN
| ApiProvider::Siliconflow
| ApiProvider::Sglang
| ApiProvider::Volcengine => {
body["reasoning_effort"] = json!("high");
@@ -969,6 +971,7 @@ pub(super) fn apply_reasoning_effort(
"xhigh" | "max" | "highest" => match provider {
ApiProvider::Deepseek
| ApiProvider::DeepseekCN
| ApiProvider::Siliconflow
| ApiProvider::Sglang
| ApiProvider::Volcengine => {
body["reasoning_effort"] = json!("max");
+1
View File
@@ -1745,6 +1745,7 @@ fn provider_accepts_reasoning_content(provider: ApiProvider) -> bool {
| ApiProvider::XiaomiMimo
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Siliconflow
| ApiProvider::Sglang
)
}
+14 -1
View File
@@ -27,7 +27,7 @@ pub fn provider(app: &mut App, args: Option<&str>) -> CommandResult {
let Some(target) = ApiProvider::parse(name) else {
return CommandResult::error(format!(
"Unknown provider '{name}'. Expected: deepseek, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, xiaomi-mimo, novita, fireworks, sglang, vllm, or ollama."
"Unknown provider '{name}'. Expected: deepseek, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, xiaomi-mimo, novita, fireworks, siliconflow, moonshot, sglang, vllm, or ollama."
));
};
@@ -195,6 +195,19 @@ mod tests {
}
}
#[test]
fn switch_to_siliconflow_emits_action() {
let mut app = create_test_app();
let result = provider(&mut app, Some("siliconflow flash"));
match result.action {
Some(AppAction::SwitchProvider { provider, model }) => {
assert_eq!(provider, ApiProvider::Siliconflow);
assert_eq!(model.as_deref(), Some("deepseek-v4-flash"));
}
other => panic!("expected SwitchProvider, got {other:?}"),
}
}
#[test]
fn switch_to_sglang_flash_emits_action() {
let mut app = create_test_app();
+304 -2
View File
@@ -56,6 +56,9 @@ pub const DEFAULT_NOVITA_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
pub const DEFAULT_NOVITA_BASE_URL: &str = "https://api.novita.ai/v1";
pub const DEFAULT_FIREWORKS_MODEL: &str = "accounts/fireworks/models/deepseek-v4-pro";
pub const DEFAULT_FIREWORKS_BASE_URL: &str = "https://api.fireworks.ai/inference/v1";
pub const DEFAULT_SILICONFLOW_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro";
pub const DEFAULT_SILICONFLOW_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
pub const DEFAULT_SILICONFLOW_BASE_URL: &str = "https://api.siliconflow.com/v1";
pub const DEFAULT_MOONSHOT_MODEL: &str = "kimi-k2.6";
pub const DEFAULT_MOONSHOT_BASE_URL: &str = "https://api.moonshot.ai/v1";
pub const DEFAULT_KIMI_CODE_MODEL: &str = "kimi-for-coding";
@@ -100,6 +103,7 @@ pub enum ApiProvider {
XiaomiMimo,
Novita,
Fireworks,
Siliconflow,
Moonshot,
Sglang,
Vllm,
@@ -127,6 +131,7 @@ impl ApiProvider {
}
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
"siliconflow" | "silicon-flow" | "silicon_flow" => Some(Self::Siliconflow),
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => Some(Self::Moonshot),
"sglang" | "sg-lang" => Some(Self::Sglang),
"vllm" | "v-llm" => Some(Self::Vllm),
@@ -149,6 +154,7 @@ impl ApiProvider {
Self::XiaomiMimo => "xiaomi-mimo",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
Self::Siliconflow => "siliconflow",
Self::Moonshot => "moonshot",
Self::Sglang => "sglang",
Self::Vllm => "vllm",
@@ -171,6 +177,7 @@ impl ApiProvider {
Self::XiaomiMimo => "Xiaomi MiMo",
Self::Novita => "Novita AI",
Self::Fireworks => "Fireworks AI",
Self::Siliconflow => "SiliconFlow",
Self::Moonshot => "Moonshot/Kimi",
Self::Sglang => "SGLang",
Self::Vllm => "vLLM",
@@ -192,6 +199,7 @@ impl ApiProvider {
Self::XiaomiMimo,
Self::Novita,
Self::Fireworks,
Self::Siliconflow,
Self::Moonshot,
Self::Sglang,
Self::Vllm,
@@ -465,6 +473,12 @@ pub fn normalize_model_name_for_provider(provider: ApiProvider, model: &str) ->
}
return Some(canonical.to_string());
}
if matches!(provider, ApiProvider::Siliconflow) {
let provider_model = model_for_provider(provider, normalized.clone());
if provider_model != normalized {
return Some(provider_model);
}
}
if let Some(canonical) = canonical_official_deepseek_model_id(&normalized) {
return Some(model_for_provider(provider, canonical.to_string()));
}
@@ -480,6 +494,9 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
ApiProvider::XiaomiMimo => vec![DEFAULT_XIAOMI_MIMO_MODEL, "mimo-v2.5"],
ApiProvider::Novita => vec![DEFAULT_NOVITA_MODEL, DEFAULT_NOVITA_FLASH_MODEL],
ApiProvider::Fireworks => vec![DEFAULT_FIREWORKS_MODEL],
ApiProvider::Siliconflow => {
vec![DEFAULT_SILICONFLOW_MODEL, DEFAULT_SILICONFLOW_FLASH_MODEL]
}
ApiProvider::Moonshot => vec![DEFAULT_MOONSHOT_MODEL],
ApiProvider::WanjieArk => vec![DEFAULT_WANJIE_ARK_MODEL],
ApiProvider::Sglang => vec![DEFAULT_SGLANG_MODEL, DEFAULT_SGLANG_FLASH_MODEL],
@@ -1400,6 +1417,8 @@ pub struct ProvidersConfig {
#[serde(default)]
pub fireworks: ProviderConfig,
#[serde(default)]
pub siliconflow: ProviderConfig,
#[serde(default)]
pub moonshot: ProviderConfig,
#[serde(default)]
pub sglang: ProviderConfig,
@@ -1558,6 +1577,7 @@ impl Config {
ApiProvider::XiaomiMimo => "providers.xiaomi_mimo",
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
ApiProvider::Siliconflow => "providers.siliconflow",
ApiProvider::Moonshot => "providers.moonshot",
ApiProvider::Sglang => "providers.sglang",
ApiProvider::Vllm => "providers.vllm",
@@ -1579,7 +1599,7 @@ impl Config {
&& ApiProvider::parse(provider).is_none()
{
anyhow::bail!(
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, xiaomi-mimo, novita, fireworks, sglang, vllm, or ollama."
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, xiaomi-mimo, novita, fireworks, siliconflow, moonshot, sglang, vllm, or ollama."
);
}
if let Some(ref key) = self.api_key
@@ -1702,6 +1722,7 @@ impl Config {
ApiProvider::XiaomiMimo => &providers.xiaomi_mimo,
ApiProvider::Novita => &providers.novita,
ApiProvider::Fireworks => &providers.fireworks,
ApiProvider::Siliconflow => &providers.siliconflow,
ApiProvider::Moonshot => &providers.moonshot,
ApiProvider::Sglang => &providers.sglang,
ApiProvider::Vllm => &providers.vllm,
@@ -1794,6 +1815,7 @@ impl Config {
ApiProvider::XiaomiMimo => DEFAULT_XIAOMI_MIMO_MODEL,
ApiProvider::Novita => DEFAULT_NOVITA_MODEL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_MODEL,
ApiProvider::Siliconflow => DEFAULT_SILICONFLOW_MODEL,
ApiProvider::Moonshot => DEFAULT_MOONSHOT_MODEL,
ApiProvider::Sglang => DEFAULT_SGLANG_MODEL,
ApiProvider::Vllm => DEFAULT_VLLM_MODEL,
@@ -1828,6 +1850,7 @@ impl Config {
| ApiProvider::XiaomiMimo
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Siliconflow
| ApiProvider::Moonshot
| ApiProvider::Sglang
| ApiProvider::Vllm
@@ -1846,6 +1869,7 @@ impl Config {
ApiProvider::XiaomiMimo => DEFAULT_XIAOMI_MIMO_BASE_URL,
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
ApiProvider::Siliconflow => DEFAULT_SILICONFLOW_BASE_URL,
ApiProvider::Moonshot => {
if self
.provider_config()
@@ -1891,6 +1915,7 @@ impl Config {
ApiProvider::XiaomiMimo => "xiaomi-mimo",
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
ApiProvider::Siliconflow => "siliconflow",
ApiProvider::Moonshot => "moonshot",
ApiProvider::Sglang => "sglang",
ApiProvider::Vllm => "vllm",
@@ -1987,6 +2012,10 @@ impl Config {
"Fireworks AI API key not found. Run 'codewhale auth set --provider fireworks', \
set FIREWORKS_API_KEY, or add [providers.fireworks] api_key in ~/.deepseek/config.toml."
),
ApiProvider::Siliconflow => anyhow::bail!(
"SiliconFlow API key not found. Run 'codewhale auth set --provider siliconflow', \
set SILICONFLOW_API_KEY, or add [providers.siliconflow] api_key in ~/.deepseek/config.toml."
),
ApiProvider::Moonshot => anyhow::bail!(
"Moonshot/Kimi API key not found. Run 'codewhale auth set --provider moonshot', \
set MOONSHOT_API_KEY/KIMI_API_KEY, or add [providers.moonshot] api_key. \
@@ -2598,6 +2627,13 @@ fn apply_env_overrides(config: &mut Config) {
.fireworks
.base_url = Some(value);
}
ApiProvider::Siliconflow => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.siliconflow
.base_url = Some(value);
}
ApiProvider::Moonshot => {
config
.providers
@@ -2729,6 +2765,16 @@ fn apply_env_overrides(config: &mut Config) {
.fireworks
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Siliconflow)
&& let Ok(value) = std::env::var("SILICONFLOW_BASE_URL")
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.siliconflow
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Moonshot)
&& let Ok(value) =
std::env::var("MOONSHOT_BASE_URL").or_else(|_| std::env::var("KIMI_BASE_URL"))
@@ -2783,6 +2829,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::XiaomiMimo => &mut providers.xiaomi_mimo,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
@@ -2864,6 +2911,16 @@ fn apply_env_overrides(config: &mut Config) {
.moonshot
.model = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Siliconflow)
&& let Ok(value) = std::env::var("SILICONFLOW_MODEL")
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.siliconflow
.model = Some(value);
}
if let Some(value) = codewhale_env_var("CODEWHALE_MODEL", "DEEPSEEK_MODEL")
.ok()
.or_else(|| {
@@ -2899,6 +2956,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::XiaomiMimo => &mut providers.xiaomi_mimo,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
@@ -3136,6 +3194,15 @@ fn normalize_model_config(config: &mut Config) {
{
providers.fireworks.model = Some(normalized);
}
if let Some(model) = providers.siliconflow.model.as_deref()
&& !provider_entry_uses_custom_base_url(
ApiProvider::Siliconflow,
&providers.siliconflow,
)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Siliconflow, model)
{
providers.siliconflow.model = Some(normalized);
}
if let Some(model) = providers.moonshot.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::Moonshot, &providers.moonshot)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Moonshot, model)
@@ -3196,6 +3263,7 @@ fn default_base_url_for_provider(provider: ApiProvider) -> &'static str {
ApiProvider::XiaomiMimo => DEFAULT_XIAOMI_MIMO_BASE_URL,
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
ApiProvider::Siliconflow => DEFAULT_SILICONFLOW_BASE_URL,
ApiProvider::Moonshot => DEFAULT_MOONSHOT_BASE_URL,
ApiProvider::Sglang => DEFAULT_SGLANG_BASE_URL,
ApiProvider::Vllm => DEFAULT_VLLM_BASE_URL,
@@ -3205,6 +3273,9 @@ fn default_base_url_for_provider(provider: ApiProvider) -> &'static str {
}
fn base_url_is_custom_for_provider(provider: ApiProvider, base_url: &str) -> bool {
if provider == ApiProvider::Siliconflow && siliconflow_base_url_is_official(base_url) {
return false;
}
normalize_base_url(base_url) != normalize_base_url(default_base_url_for_provider(provider))
}
@@ -3212,6 +3283,13 @@ fn provider_preserves_custom_base_url_model(provider: ApiProvider, base_url: &st
base_url_is_custom_for_provider(provider, base_url)
}
fn siliconflow_base_url_is_official(base_url: &str) -> bool {
matches!(
normalize_base_url(base_url).to_ascii_lowercase().as_str(),
"https://api.siliconflow.com/v1" | "https://api.siliconflow.cn/v1"
)
}
fn moonshot_base_url_uses_kimi_code(base_url: &str) -> bool {
let normalized = normalize_base_url(base_url).to_ascii_lowercase();
normalized == DEFAULT_KIMI_CODE_BASE_URL
@@ -3276,6 +3354,12 @@ fn model_for_provider(provider: ApiProvider, normalized: String) -> String {
// Flash not yet available on Fireworks; fall through to normalized name
"accounts/fireworks/models/deepseek-v4-flash".to_string()
}
(ApiProvider::Siliconflow, "deepseek-v4-pro" | "deepseek-reasoner" | "deepseek-r1") => {
DEFAULT_SILICONFLOW_MODEL.to_string()
}
(ApiProvider::Siliconflow, "deepseek-v4-flash" | "deepseek-chat" | "deepseek-v3") => {
DEFAULT_SILICONFLOW_FLASH_MODEL.to_string()
}
(ApiProvider::Sglang, "deepseek-v4-pro") => DEFAULT_SGLANG_MODEL.to_string(),
(ApiProvider::Sglang, "deepseek-v4-flash") => DEFAULT_SGLANG_FLASH_MODEL.to_string(),
(ApiProvider::Vllm, "deepseek-v4-pro") => DEFAULT_VLLM_MODEL.to_string(),
@@ -3455,6 +3539,7 @@ fn merge_providers(
xiaomi_mimo: merge_provider_config(base.xiaomi_mimo, override_cfg.xiaomi_mimo),
novita: merge_provider_config(base.novita, override_cfg.novita),
fireworks: merge_provider_config(base.fireworks, override_cfg.fireworks),
siliconflow: merge_provider_config(base.siliconflow, override_cfg.siliconflow),
moonshot: merge_provider_config(base.moonshot, override_cfg.moonshot),
sglang: merge_provider_config(base.sglang, override_cfg.sglang),
vllm: merge_provider_config(base.vllm, override_cfg.vllm),
@@ -3882,6 +3967,9 @@ pub fn active_provider_has_env_api_key(config: &Config) -> bool {
ApiProvider::Fireworks => {
std::env::var("FIREWORKS_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
ApiProvider::Siliconflow => {
std::env::var("SILICONFLOW_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
ApiProvider::Moonshot => {
std::env::var("MOONSHOT_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("KIMI_API_KEY").is_ok_and(|k| !k.trim().is_empty())
@@ -3917,6 +4005,7 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Siliconflow => "SILICONFLOW_API_KEY",
ApiProvider::Moonshot => "MOONSHOT_API_KEY",
ApiProvider::Sglang => "SGLANG_API_KEY",
ApiProvider::Vllm => "VLLM_API_KEY",
@@ -4018,6 +4107,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::XiaomiMimo => "providers.xiaomi_mimo",
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
ApiProvider::Siliconflow => "providers.siliconflow",
ApiProvider::Moonshot => "providers.moonshot",
ApiProvider::Sglang => "providers.sglang",
ApiProvider::Vllm => "providers.vllm",
@@ -4057,6 +4147,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::XiaomiMimo => "xiaomi_mimo",
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
ApiProvider::Siliconflow => "siliconflow",
ApiProvider::Moonshot => "moonshot",
ApiProvider::Sglang => "sglang",
ApiProvider::Vllm => "vllm",
@@ -4149,6 +4240,7 @@ fn provider_config_key(provider: ApiProvider) -> Result<&'static str> {
ApiProvider::XiaomiMimo => Ok("xiaomi_mimo"),
ApiProvider::Novita => Ok("novita"),
ApiProvider::Fireworks => Ok("fireworks"),
ApiProvider::Siliconflow => Ok("siliconflow"),
ApiProvider::Moonshot => Ok("moonshot"),
ApiProvider::Sglang => Ok("sglang"),
ApiProvider::Vllm => Ok("vllm"),
@@ -4614,6 +4706,9 @@ mod tests {
novita_base_url: Option<OsString>,
fireworks_api_key: Option<OsString>,
fireworks_base_url: Option<OsString>,
siliconflow_api_key: Option<OsString>,
siliconflow_base_url: Option<OsString>,
siliconflow_model: Option<OsString>,
moonshot_api_key: Option<OsString>,
moonshot_base_url: Option<OsString>,
moonshot_model: Option<OsString>,
@@ -4685,6 +4780,9 @@ mod tests {
let novita_base_url_prev = env::var_os("NOVITA_BASE_URL");
let fireworks_api_key_prev = env::var_os("FIREWORKS_API_KEY");
let fireworks_base_url_prev = env::var_os("FIREWORKS_BASE_URL");
let siliconflow_api_key_prev = env::var_os("SILICONFLOW_API_KEY");
let siliconflow_base_url_prev = env::var_os("SILICONFLOW_BASE_URL");
let siliconflow_model_prev = env::var_os("SILICONFLOW_MODEL");
let moonshot_api_key_prev = env::var_os("MOONSHOT_API_KEY");
let moonshot_base_url_prev = env::var_os("MOONSHOT_BASE_URL");
let moonshot_model_prev = env::var_os("MOONSHOT_MODEL");
@@ -4751,6 +4849,9 @@ mod tests {
env::remove_var("NOVITA_BASE_URL");
env::remove_var("FIREWORKS_API_KEY");
env::remove_var("FIREWORKS_BASE_URL");
env::remove_var("SILICONFLOW_API_KEY");
env::remove_var("SILICONFLOW_BASE_URL");
env::remove_var("SILICONFLOW_MODEL");
env::remove_var("MOONSHOT_API_KEY");
env::remove_var("MOONSHOT_BASE_URL");
env::remove_var("MOONSHOT_MODEL");
@@ -4817,6 +4918,9 @@ mod tests {
novita_base_url: novita_base_url_prev,
fireworks_api_key: fireworks_api_key_prev,
fireworks_base_url: fireworks_base_url_prev,
siliconflow_api_key: siliconflow_api_key_prev,
siliconflow_base_url: siliconflow_base_url_prev,
siliconflow_model: siliconflow_model_prev,
moonshot_api_key: moonshot_api_key_prev,
moonshot_base_url: moonshot_base_url_prev,
moonshot_model: moonshot_model_prev,
@@ -4892,6 +4996,9 @@ mod tests {
Self::restore_var("NOVITA_BASE_URL", self.novita_base_url.take());
Self::restore_var("FIREWORKS_API_KEY", self.fireworks_api_key.take());
Self::restore_var("FIREWORKS_BASE_URL", self.fireworks_base_url.take());
Self::restore_var("SILICONFLOW_API_KEY", self.siliconflow_api_key.take());
Self::restore_var("SILICONFLOW_BASE_URL", self.siliconflow_base_url.take());
Self::restore_var("SILICONFLOW_MODEL", self.siliconflow_model.take());
Self::restore_var("MOONSHOT_API_KEY", self.moonshot_api_key.take());
Self::restore_var("MOONSHOT_BASE_URL", self.moonshot_base_url.take());
Self::restore_var("MOONSHOT_MODEL", self.moonshot_model.take());
@@ -5234,6 +5341,9 @@ mod tests {
"fireworks" => {
providers.fireworks.api_key = Some(api_key.to_string());
}
"siliconflow" => {
providers.siliconflow.api_key = Some(api_key.to_string());
}
"sglang" => {
providers.sglang.api_key = Some(api_key.to_string());
}
@@ -5255,7 +5365,14 @@ mod tests {
#[test]
fn has_api_key_uses_active_provider_scoped_config_key() {
for provider in ["openai", "wanjie-ark", "openrouter", "novita", "fireworks"] {
for provider in [
"openai",
"wanjie-ark",
"openrouter",
"novita",
"fireworks",
"siliconflow",
] {
let config = config_with_provider_scoped_key(provider, "provider-config-key");
assert!(
@@ -5274,6 +5391,7 @@ mod tests {
("openrouter", "OPENROUTER_API_KEY"),
("novita", "NOVITA_API_KEY"),
("fireworks", "FIREWORKS_API_KEY"),
("siliconflow", "SILICONFLOW_API_KEY"),
] {
unsafe {
std::env::set_var(env_var, "provider-env-key");
@@ -5785,6 +5903,32 @@ api_key = "old-openrouter-key"
.as_deref(),
Some(DEFAULT_OPENROUTER_FLASH_MODEL)
);
assert_eq!(
normalize_model_name_for_provider(ApiProvider::Siliconflow, "deepseek-v4-pro")
.as_deref(),
Some(DEFAULT_SILICONFLOW_MODEL)
);
assert_eq!(
normalize_model_name_for_provider(ApiProvider::Siliconflow, "deepseek-reasoner")
.as_deref(),
Some(DEFAULT_SILICONFLOW_MODEL)
);
assert_eq!(
normalize_model_name_for_provider(ApiProvider::Siliconflow, "deepseek-r1").as_deref(),
Some(DEFAULT_SILICONFLOW_MODEL)
);
assert_eq!(
normalize_model_name_for_provider(ApiProvider::Siliconflow, "deepseek-chat").as_deref(),
Some(DEFAULT_SILICONFLOW_FLASH_MODEL)
);
assert_eq!(
normalize_model_name_for_provider(ApiProvider::Siliconflow, "deepseek-v3").as_deref(),
Some(DEFAULT_SILICONFLOW_FLASH_MODEL)
);
assert_eq!(
normalize_model_name_for_provider(ApiProvider::Siliconflow, "deepseek-v3.2").as_deref(),
Some("deepseek-v3.2")
);
}
#[test]
@@ -6648,6 +6792,36 @@ model = "glm-5"
Ok(())
}
#[test]
fn siliconflow_provider_uses_canonical_defaults() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-siliconflow-defaults-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
let config = Config {
provider: Some("siliconflow".to_string()),
..Default::default()
};
config.validate()?;
assert_eq!(config.api_provider(), ApiProvider::Siliconflow);
assert_eq!(config.default_model(), DEFAULT_SILICONFLOW_MODEL);
assert_eq!(config.deepseek_base_url(), DEFAULT_SILICONFLOW_BASE_URL);
assert_eq!(
model_completion_names_for_provider(ApiProvider::Siliconflow),
vec![DEFAULT_SILICONFLOW_MODEL, DEFAULT_SILICONFLOW_FLASH_MODEL]
);
Ok(())
}
#[test]
fn sglang_provider_works_without_api_key() -> Result<()> {
let _lock = lock_test_env();
@@ -6885,6 +7059,68 @@ model = "qwen2.5-coder:7b"
Ok(())
}
#[test]
fn siliconflow_env_overrides_key_base_url_and_model() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-siliconflow-env-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::set_var("CODEWHALE_PROVIDER", "siliconflow");
env::set_var("SILICONFLOW_API_KEY", "sf-env-key");
env::set_var("SILICONFLOW_BASE_URL", "https://sf-mirror.example/v1");
env::set_var("SILICONFLOW_MODEL", "deepseek-v4-flash");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::Siliconflow);
assert_eq!(config.deepseek_api_key()?, "sf-env-key");
assert_eq!(config.deepseek_base_url(), "https://sf-mirror.example/v1");
assert_eq!(config.default_model(), "deepseek-v4-flash");
Ok(())
}
#[test]
fn siliconflow_cn_base_url_env_normalizes_model_aliases() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-siliconflow-cn-env-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
// Safety: test-only environment mutation guarded by a global mutex.
unsafe {
env::set_var("CODEWHALE_PROVIDER", "siliconflow");
env::set_var("SILICONFLOW_API_KEY", "sf-env-key");
env::set_var("SILICONFLOW_BASE_URL", "https://api.siliconflow.cn/v1");
env::set_var("SILICONFLOW_MODEL", "deepseek-reasoner");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::Siliconflow);
assert_eq!(config.deepseek_api_key()?, "sf-env-key");
assert_eq!(config.deepseek_base_url(), "https://api.siliconflow.cn/v1");
assert_eq!(config.default_model(), DEFAULT_SILICONFLOW_MODEL);
Ok(())
}
#[test]
fn openrouter_base_url_env_overrides_default() -> Result<()> {
let _lock = lock_test_env();
@@ -6946,6 +7182,41 @@ base_url = "https://or-table.example/v1"
Ok(())
}
#[test]
fn siliconflow_reads_provider_table_from_config_file() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-siliconflow-table-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
let config_path = temp_root.join(".deepseek").join("config.toml");
ensure_parent_dir(&config_path)?;
fs::write(
&config_path,
r#"provider = "siliconflow"
[providers.siliconflow]
api_key = "sf-table-key"
model = "deepseek-v4-flash"
"#,
)?;
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::Siliconflow);
assert_eq!(config.deepseek_api_key()?, "sf-table-key");
assert_eq!(config.deepseek_base_url(), DEFAULT_SILICONFLOW_BASE_URL);
assert_eq!(config.default_model(), DEFAULT_SILICONFLOW_FLASH_MODEL);
Ok(())
}
#[test]
fn openrouter_custom_base_url_preserves_provider_model() -> Result<()> {
let _lock = lock_test_env();
@@ -7326,6 +7597,7 @@ api_key = "moonshot-platform-key"
assert!(!has_api_key_for(&config, ApiProvider::WanjieArk));
assert!(!has_api_key_for(&config, ApiProvider::Openrouter));
assert!(!has_api_key_for(&config, ApiProvider::XiaomiMimo));
assert!(!has_api_key_for(&config, ApiProvider::Siliconflow));
assert!(
has_api_key_for(&config, ApiProvider::Sglang),
"SGLang is self-hosted and does not require a key by default"
@@ -7341,11 +7613,13 @@ api_key = "moonshot-platform-key"
env::set_var("OPENAI_API_KEY", "openai-env");
env::set_var("WANJIE_API_KEY", "wanjie-env");
env::set_var("MIMO_API_KEY", "mimo-env");
env::set_var("SILICONFLOW_API_KEY", "sf-env");
}
assert!(has_api_key_for(&config, ApiProvider::Openai));
assert!(has_api_key_for(&config, ApiProvider::WanjieArk));
assert!(has_api_key_for(&config, ApiProvider::Openrouter));
assert!(has_api_key_for(&config, ApiProvider::XiaomiMimo));
assert!(has_api_key_for(&config, ApiProvider::Siliconflow));
assert!(!has_api_key_for(&config, ApiProvider::Novita));
// Safety: test-only environment mutation guarded by a global mutex.
@@ -7354,17 +7628,20 @@ api_key = "moonshot-platform-key"
env::remove_var("OPENAI_API_KEY");
env::remove_var("WANJIE_API_KEY");
env::remove_var("MIMO_API_KEY");
env::remove_var("SILICONFLOW_API_KEY");
}
let mut providers = ProvidersConfig::default();
providers.openai.api_key = Some("file-openai".to_string());
providers.wanjie_ark.api_key = Some("file-wanjie".to_string());
providers.xiaomi_mimo.api_key = Some("file-mimo".to_string());
providers.novita.api_key = Some("file-novita".to_string());
providers.siliconflow.api_key = Some("file-siliconflow".to_string());
config.providers = Some(providers);
assert!(has_api_key_for(&config, ApiProvider::Openai));
assert!(has_api_key_for(&config, ApiProvider::WanjieArk));
assert!(has_api_key_for(&config, ApiProvider::XiaomiMimo));
assert!(has_api_key_for(&config, ApiProvider::Novita));
assert!(has_api_key_for(&config, ApiProvider::Siliconflow));
assert!(!has_api_key_for(&config, ApiProvider::Openrouter));
Ok(())
}
@@ -7457,6 +7734,7 @@ api_key = "moonshot-platform-key"
save_api_key_for(ApiProvider::WanjieArk, "wanjie-saved-key")?;
save_api_key_for(ApiProvider::Fireworks, "fireworks-saved-key")?;
save_api_key_for(ApiProvider::XiaomiMimo, "mimo-saved-key")?;
save_api_key_for(ApiProvider::Siliconflow, "sf-saved-key")?;
save_api_key_for(ApiProvider::Sglang, "sglang-saved-key")?;
let contents = fs::read_to_string(&path)?;
let parsed: toml::Value = toml::from_str(&contents)?;
@@ -7492,6 +7770,14 @@ api_key = "moonshot-platform-key"
.and_then(toml::Value::as_str),
Some("mimo-saved-key")
);
assert_eq!(
parsed
.get("providers")
.and_then(|p| p.get("siliconflow"))
.and_then(|t| t.get("api_key"))
.and_then(toml::Value::as_str),
Some("sf-saved-key")
);
assert_eq!(
parsed
.get("providers")
@@ -7764,6 +8050,22 @@ model = "deepseek-ai/deepseek-v4-pro"
assert!(!cap.cache_telemetry_supported);
}
#[test]
fn provider_capability_siliconflow_v4_pro_has_thinking_no_cache() {
let cap = provider_capability(ApiProvider::Siliconflow, DEFAULT_SILICONFLOW_MODEL);
assert_eq!(
cap.context_window,
crate::models::DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS
);
assert_eq!(cap.max_output, 384_000);
assert!(cap.thinking_supported);
assert!(!cap.cache_telemetry_supported);
assert_eq!(
cap.request_payload_mode,
RequestPayloadMode::ChatCompletions
);
}
#[test]
fn provider_capability_sglang_v4_pro_has_thinking_no_cache() {
let cap = provider_capability(ApiProvider::Sglang, DEFAULT_SGLANG_MODEL);
+1
View File
@@ -405,6 +405,7 @@ impl Engine {
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY/MIMO_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Siliconflow => "SILICONFLOW_API_KEY",
ApiProvider::Moonshot => "MOONSHOT_API_KEY/KIMI_API_KEY",
ApiProvider::Sglang => "SGLANG_API_KEY",
ApiProvider::Vllm => "VLLM_API_KEY",
+10
View File
@@ -1933,6 +1933,10 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
"FIREWORKS_API_KEY",
"codewhale auth set --provider fireworks --api-key \"...\"",
),
crate::config::ApiProvider::Siliconflow => (
"SILICONFLOW_API_KEY",
"codewhale auth set --provider siliconflow --api-key \"...\"",
),
crate::config::ApiProvider::Moonshot => (
"MOONSHOT_API_KEY/KIMI_API_KEY",
"codewhale auth set --provider moonshot --api-key \"...\"",
@@ -1969,6 +1973,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
crate::config::ApiProvider::XiaomiMimo => "xiaomi_mimo",
crate::config::ApiProvider::Novita => "novita",
crate::config::ApiProvider::Fireworks => "fireworks",
crate::config::ApiProvider::Siliconflow => "siliconflow",
crate::config::ApiProvider::Moonshot => "moonshot",
crate::config::ApiProvider::Sglang => "sglang",
crate::config::ApiProvider::Vllm => "vllm",
@@ -2288,6 +2293,11 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
"fireworks",
&["FIREWORKS_API_KEY"][..],
),
(
crate::config::ApiProvider::Siliconflow,
"siliconflow",
&["SILICONFLOW_API_KEY"][..],
),
(
crate::config::ApiProvider::Moonshot,
"moonshot",
+31 -2
View File
@@ -5400,8 +5400,37 @@ mod tests {
#[test]
fn app_new_detects_missing_api_key_with_default_config() {
// Config::default() carries no api_key and the test runner
// should not have DEEPSEEK_API_KEY in its environment.
let _lock = lock_test_env();
let tmp = tempfile::TempDir::new().expect("tempdir");
let config_path = tmp.path().join("config.toml");
let _config_path = EnvVarGuard::set("DEEPSEEK_CONFIG_PATH", &config_path);
let _provider_env = EnvVarGuard::remove("CODEWHALE_PROVIDER");
let _legacy_provider_env = EnvVarGuard::remove("DEEPSEEK_PROVIDER");
let _api_key_envs: Vec<_> = [
"DEEPSEEK_API_KEY",
"NVIDIA_API_KEY",
"NVIDIA_NIM_API_KEY",
"OPENAI_API_KEY",
"ATLASCLOUD_API_KEY",
"WANJIE_ARK_API_KEY",
"WANJIE_API_KEY",
"WANJIE_MAAS_API_KEY",
"OPENROUTER_API_KEY",
"NOVITA_API_KEY",
"FIREWORKS_API_KEY",
"SILICONFLOW_API_KEY",
"MOONSHOT_API_KEY",
"KIMI_API_KEY",
"SGLANG_API_KEY",
"VLLM_API_KEY",
"OLLAMA_API_KEY",
]
.into_iter()
.map(EnvVarGuard::remove)
.collect();
// Config::default() carries no api_key, and this test isolates process
// env/settings so previous tests or developer shells cannot satisfy it.
let app = App::new(test_options(false), &Config::default());
assert!(
app.onboarding_needs_api_key,
+2
View File
@@ -106,6 +106,7 @@ impl ProviderPickerView {
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY / MIMO_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Siliconflow => "SILICONFLOW_API_KEY",
ApiProvider::Moonshot => "MOONSHOT_API_KEY / KIMI_API_KEY",
ApiProvider::Sglang => "SGLANG_API_KEY",
ApiProvider::Vllm => "VLLM_API_KEY",
@@ -478,6 +479,7 @@ mod tests {
"Xiaomi MiMo",
"Novita AI",
"Fireworks AI",
"SiliconFlow",
"Moonshot/Kimi",
"SGLang",
"vLLM",
+3
View File
@@ -6028,6 +6028,7 @@ fn render(f: &mut Frame, app: &mut App) {
crate::config::ApiProvider::XiaomiMimo => Some("MiMo"),
crate::config::ApiProvider::Novita => Some("Novita"),
crate::config::ApiProvider::Fireworks => Some("Fireworks"),
crate::config::ApiProvider::Siliconflow => Some("SiliconFlow"),
crate::config::ApiProvider::Moonshot => Some("Kimi"),
crate::config::ApiProvider::Sglang => Some("SGLang"),
crate::config::ApiProvider::Vllm => Some("vLLM"),
@@ -6947,6 +6948,7 @@ async fn apply_provider_picker_api_key(
ApiProvider::XiaomiMimo => &mut providers.xiaomi_mimo,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
@@ -7001,6 +7003,7 @@ fn set_provider_auth_mode_in_memory(config: &mut Config, provider: ApiProvider,
ApiProvider::XiaomiMimo => &mut providers.xiaomi_mimo,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
+21 -6
View File
@@ -63,7 +63,7 @@ provider's keyring entry.
For hosted, generic OpenAI-compatible, or self-hosted providers, set
`provider = "nvidia-nim"`, `"openai"`, `"atlascloud"`, `"wanjie-ark"`,
`"openrouter"`, `"xiaomi-mimo"`, `"novita"`, `"fireworks"`, `"moonshot"`,
`"openrouter"`, `"xiaomi-mimo"`, `"novita"`, `"fireworks"`, `"siliconflow"`, `"moonshot"`,
`"sglang"`, `"vllm"`, or `"ollama"` or pass `codewhale --provider <name>`.
For the provider-by-provider registry, including auth variables, default base
URLs, model IDs, and capability metadata, see [PROVIDERS.md](PROVIDERS.md).
@@ -74,7 +74,8 @@ the resolved key, base URL, provider, and model to the TUI process. Use
`codewhale auth set --provider atlascloud --api-key "YOUR_ATLASCLOUD_API_KEY"` or
`codewhale auth set --provider wanjie-ark --api-key "YOUR_WANJIE_API_KEY"` or
`codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_MIMO_API_KEY"` or
`codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"`
`codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"` or
`codewhale auth set --provider siliconflow --api-key "YOUR_SILICONFLOW_API_KEY"`
to save provider keys through the facade. The generic `openai` provider defaults
to `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to
`deepseek-v4-pro` for OpenAI-compatible gateways. `atlascloud` defaults to
@@ -90,6 +91,10 @@ or `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom
URLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store
unless API-key auth is explicitly requested; use an env var or config-file key
when a local server does require bearer auth.
SiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts
`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.
`https://api.siliconflow.cn/v1` can still be configured explicitly when a user
needs the regional endpoint.
### Custom OpenAI-Compatible Gateways
@@ -180,6 +185,13 @@ default_text_model = "deepseek-ai/deepseek-v4-pro"
provider = "fireworks"
default_text_model = "accounts/fireworks/models/deepseek-v4-pro"
[profiles.siliconflow]
provider = "siliconflow"
default_text_model = "deepseek-ai/DeepSeek-V4-Pro"
[profiles.siliconflow.providers.siliconflow]
base_url = "https://api.siliconflow.com/v1"
[profiles.openai-compatible]
provider = "openai"
@@ -227,7 +239,7 @@ aliases. When both forms are set the `CODEWHALE_*` value wins; the
`DEEPSEEK_*` form is kept for older shells:
- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —
`deepseek|nvidia-nim|openai|atlascloud|wanjie-ark|openrouter|xiaomi-mimo|novita|fireworks|moonshot|sglang|vllm|ollama`
`deepseek|nvidia-nim|openai|atlascloud|wanjie-ark|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|moonshot|sglang|vllm|ollama`
- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider
- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider
@@ -259,6 +271,9 @@ Remaining variables:
- `NOVITA_BASE_URL`
- `FIREWORKS_API_KEY`
- `FIREWORKS_BASE_URL`
- `SILICONFLOW_API_KEY`
- `SILICONFLOW_BASE_URL`
- `SILICONFLOW_MODEL`
- `MOONSHOT_API_KEY` or `KIMI_API_KEY`
- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`
- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`
@@ -464,10 +479,10 @@ If you are upgrading from older releases:
### Core keys (used by the TUI/engine)
- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, or `ollama`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint at `https://api.xiaomimimo.com/v1`; `novita` targets `https://api.novita.ai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`.
- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `moonshot`, `sglang`, `vllm`, or `ollama`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint at `https://api.xiaomimimo.com/v1`; `novita` targets `https://api.novita.ai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`.
- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and Ollama can omit it.
- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs. Other defaults are `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://openrouter.ai/api/v1` for `openrouter`, `https://api.xiaomimimo.com/v1` for `xiaomi-mimo`, `https://api.novita.ai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.moonshot.ai/v1` for `moonshot`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.5-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `kimi-k2.6` for Moonshot, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, and Ollama model IDs are passed through unchanged. OpenRouter provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.
- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs. Other defaults are `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://openrouter.ai/api/v1` for `openrouter`, `https://api.xiaomimimo.com/v1` for `xiaomi-mimo`, `https://api.novita.ai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.moonshot.ai/v1` for `moonshot`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.5-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow, `kimi-k2.6` for Moonshot, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026, except SiliconFlow maps `deepseek-reasoner` and `deepseek-r1` to its Pro model while `deepseek-chat` and `deepseek-v3` map to Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, and Ollama model IDs are passed through unchanged. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.
- `reasoning_effort` (string, optional): `off`, `low`, `medium`, `high`, or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.
- `allow_shell` (bool, optional): defaults to `true` (sandboxed).
- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.
+11 -8
View File
@@ -5,12 +5,12 @@ CodeWhale codebase. It is intentionally conservative: shipped entries are
limited to provider IDs, config keys, auth paths, base URLs, model resolution,
and capability metadata that the code already knows about.
DeepSeek remains the first-class default provider. NVIDIA NIM, Volcengine Ark,
OpenRouter, Xiaomi MiMo, Novita, Fireworks, generic OpenAI-compatible endpoints,
self-hosted runtimes, and Moonshot/Kimi are additive routes for running the
same terminal harness against other hosted or local model endpoints. Hugging
Face Inference Providers are a planned additive open-model routing layer; they
are not a native provider in this checkout yet.
DeepSeek remains the first-class default provider. NVIDIA NIM, OpenRouter,
Volcengine Ark, Xiaomi MiMo, Novita, Fireworks, SiliconFlow, generic
OpenAI-compatible endpoints, self-hosted runtimes, and Moonshot/Kimi are
additive routes for running the same terminal harness against other hosted or
local model endpoints. Hugging Face Inference Providers are a planned additive
open-model routing layer; they are not a native provider in this checkout yet.
Sources to keep in sync:
@@ -30,7 +30,7 @@ Sources to keep in sync:
The canonical provider IDs are:
`deepseek`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,
`openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `moonshot`, `sglang`,
`xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `moonshot`, `sglang`,
`vllm`, and `ollama`.
Use any of these surfaces to select a provider:
@@ -121,6 +121,7 @@ endpoint.
| `xiaomi-mimo` | `[providers.xiaomi_mimo]` | `XIAOMI_MIMO_API_KEY`, `MIMO_API_KEY` | `XIAOMI_MIMO_BASE_URL`, `MIMO_BASE_URL`; default `https://api.xiaomimimo.com/v1` | `mimo-v2.5-pro`, `mimo-v2.5` | Xiaomi MiMo OpenAI-compatible chat completions route. It sends `max_completion_tokens` and uses MiMo's `thinking` field for reasoning control. |
| `novita` | `[providers.novita]` | `NOVITA_API_KEY` | `NOVITA_BASE_URL`; default `https://api.novita.ai/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | OpenAI-compatible hosted route for DeepSeek model IDs. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |
| `fireworks` | `[providers.fireworks]` | `FIREWORKS_API_KEY` | `FIREWORKS_BASE_URL`; default `https://api.fireworks.ai/inference/v1` | `accounts/fireworks/models/deepseek-v4-pro` | OpenAI-compatible hosted route. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |
| `siliconflow` | `[providers.siliconflow]` | `SILICONFLOW_API_KEY` | `SILICONFLOW_BASE_URL`; default `https://api.siliconflow.com/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | OpenAI-compatible hosted route. Official docs use the `.com` endpoint; users who need the regional endpoint can set `https://api.siliconflow.cn/v1` explicitly. `SILICONFLOW_MODEL` is accepted. Reasoning aliases `deepseek-reasoner` and `deepseek-r1` map to Pro; `deepseek-chat` and `deepseek-v3` map to Flash. |
| `moonshot` | `[providers.moonshot]` | `MOONSHOT_API_KEY`, `KIMI_API_KEY` | `MOONSHOT_BASE_URL`, `KIMI_BASE_URL`; default `https://api.moonshot.ai/v1` | `kimi-k2.6`; Kimi Code path uses `kimi-for-coding` at `https://api.kimi.com/coding/v1` | Moonshot/Kimi route. `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, and `KIMI_MODEL` are accepted. `[providers.moonshot] auth_mode = "kimi_oauth"` reads Kimi CLI OAuth credentials when present. |
| `sglang` | `[providers.sglang]` | Optional `SGLANG_API_KEY` | `SGLANG_BASE_URL`; default `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted OpenAI-compatible route. Localhost deployments commonly omit auth. `SGLANG_MODEL` is accepted. |
| `vllm` | `[providers.vllm]` | Optional `VLLM_API_KEY` | `VLLM_BASE_URL`; default `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted vLLM OpenAI-compatible route. Localhost deployments commonly omit auth. `VLLM_MODEL` is accepted. |
@@ -153,6 +154,7 @@ endpoint when the endpoint supports model listing.
| `xiaomi-mimo` | `mimo-v2.5-pro`, `mimo-v2.5` | yes | yes |
| `novita` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | yes | yes |
| `fireworks` | `accounts/fireworks/models/deepseek-v4-pro` | yes | yes |
| `siliconflow` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes |
| `moonshot` | `kimi-k2.6` | yes | yes |
| `sglang` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes |
| `vllm` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes |
@@ -177,7 +179,8 @@ All shipped providers use the Chat Completions request payload mode today.
| DeepSeek V4 (`deepseek-v4-pro`, `deepseek-v4-flash`) | 1,000,000 | 384,000 | yes | yes | DeepSeek beta only |
| DeepSeek compatibility aliases (`deepseek-chat`, `deepseek-reasoner`) | 1,000,000 | 384,000 | yes | yes | DeepSeek beta only |
| NVIDIA NIM V4 registry models | 1,000,000 | 384,000 | yes | yes | not documented in code |
| OpenRouter, Novita, Fireworks, Volcengine Ark, SGLang, and vLLM V4 model IDs | 1,000,000 | 384,000 | yes | Volcengine only | not documented in code |
| Volcengine Ark V4 model IDs | 1,000,000 | 384,000 | yes | yes | not documented in code |
| OpenRouter, Novita, Fireworks, SiliconFlow, SGLang, and vLLM V4 model IDs | 1,000,000 | 384,000 | yes | no | not documented in code |
| Xiaomi MiMo models | 1,000,000 | 128,000 | yes | no | not documented in code |
| Wanjie Ark `reasoner` / `r1` model IDs | 128,000 | 4,096 | yes | no | not documented in code |
| Generic `openai`, AtlasCloud, and Moonshot/Kimi | 128,000 | 4,096 | no in doctor capability metadata | no | not documented in code |