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:
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1745,6 +1745,7 @@ fn provider_accepts_reasoning_content(provider: ApiProvider) -> bool {
|
||||
| ApiProvider::XiaomiMimo
|
||||
| ApiProvider::Novita
|
||||
| ApiProvider::Fireworks
|
||||
| ApiProvider::Siliconflow
|
||||
| ApiProvider::Sglang
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user