fix(config): add separate siliconflow_cn provider config field with fallback (#2893) (#2895)

Split SiliconflowCN into its own [providers.siliconflow_cn] TOML section
instead of silently ignoring [providers.siliconflow-CN] config.

- ProvidersToml / ProvidersConfig: add siliconflow_cn field with serde alias
- for_provider / for_provider_mut / provider_config_for: route SiliconflowCN
  to the new field
- resolve_runtime_options_with_secrets: fallback siliconflow_cn → siliconflow
  for api_key / base_url / model when unset
- deepseek_api_key: add config-file fallback for SiliconflowCn
- provider_config_key: update metadata to "siliconflow_cn"
- save_api_key_for: write SiliconflowCn keys to providers.siliconflow_cn
- docs/PROVIDERS.md, config.example.toml, scripts/check-provider-registry.py
This commit is contained in:
Hanmiao Li
2026-06-13 01:53:11 +08:00
committed by GitHub
parent 5e22753ee1
commit b291214cd2
6 changed files with 49 additions and 17 deletions
+7
View File
@@ -378,6 +378,13 @@ max_subagents = 10 # optional (1-20)
# base_url = "https://api.siliconflow.com/v1"
# model = "deepseek-ai/DeepSeek-V4-Pro" # or deepseek-ai/DeepSeek-V4-Flash
# SiliconFlow China-hosted DeepSeek V4 (https://siliconflow.cn)
# Falls back to [providers.siliconflow] for api_key / base_url / model when unset.
[providers.siliconflow-CN]
# api_key = "YOUR_SILICONFLOW_API_KEY"
# base_url = "https://api.siliconflow.cn/v1"
# model = "deepseek-ai/DeepSeek-V4-Pro"
# Arcee AI direct OpenAI-compatible endpoint (https://docs.arcee.ai)
[providers.arcee]
# api_key = "YOUR_ARCEE_API_KEY"
+21 -5
View File
@@ -296,6 +296,8 @@ pub struct ProvidersToml {
pub fireworks: ProviderConfigToml,
#[serde(default)]
pub siliconflow: ProviderConfigToml,
#[serde(default, alias = "siliconflow-CN", alias = "siliconflow-cn")]
pub siliconflow_cn: ProviderConfigToml,
#[serde(default)]
pub arcee: ProviderConfigToml,
#[serde(default)]
@@ -361,7 +363,8 @@ impl ProvidersToml {
ProviderKind::XiaomiMimo => &self.xiaomi_mimo,
ProviderKind::Novita => &self.novita,
ProviderKind::Fireworks => &self.fireworks,
ProviderKind::Siliconflow | ProviderKind::SiliconflowCN => &self.siliconflow,
ProviderKind::Siliconflow => &self.siliconflow,
ProviderKind::SiliconflowCN => &self.siliconflow_cn,
ProviderKind::Arcee => &self.arcee,
ProviderKind::Moonshot => &self.moonshot,
ProviderKind::Sglang => &self.sglang,
@@ -386,7 +389,8 @@ impl ProvidersToml {
ProviderKind::XiaomiMimo => &mut self.xiaomi_mimo,
ProviderKind::Novita => &mut self.novita,
ProviderKind::Fireworks => &mut self.fireworks,
ProviderKind::Siliconflow | ProviderKind::SiliconflowCN => &mut self.siliconflow,
ProviderKind::Siliconflow => &mut self.siliconflow,
ProviderKind::SiliconflowCN => &mut self.siliconflow_cn,
ProviderKind::Arcee => &mut self.arcee,
ProviderKind::Moonshot => &mut self.moonshot,
ProviderKind::Sglang => &mut self.sglang,
@@ -1956,7 +1960,19 @@ impl ConfigToml {
let env = EnvRuntimeOverrides::load();
let provider = cli.provider.or(env.provider).unwrap_or(self.provider);
let provider_cfg = self.providers.for_provider(provider);
let mut provider_cfg = self.providers.for_provider(provider).clone();
if provider == ProviderKind::SiliconflowCN {
let fb = &self.providers.siliconflow;
if provider_cfg.api_key.is_none() {
provider_cfg.api_key = fb.api_key.clone();
}
if provider_cfg.base_url.is_none() {
provider_cfg.base_url = fb.base_url.clone();
}
if provider_cfg.model.is_none() {
provider_cfg.model = fb.model.clone();
}
}
let root_deepseek_api_key = (provider == ProviderKind::Deepseek)
.then(|| self.api_key.clone())
.flatten();
@@ -5135,11 +5151,11 @@ unix_socket_path = "/tmp/cw-hooks.sock"
provider::resolve_provider("siliconflow-cn").expect("siliconflow-cn alias resolves");
assert_eq!(siliconflow_cn.kind(), ProviderKind::SiliconflowCN);
assert_eq!(siliconflow_cn.id(), "siliconflow-CN");
assert_eq!(siliconflow_cn.provider_config_key(), "siliconflow");
assert_eq!(siliconflow_cn.provider_config_key(), "siliconflow_cn");
let config = ProvidersToml::default();
let shared_table = config.for_provider(ProviderKind::SiliconflowCN);
assert!(std::ptr::eq(
assert!(!std::ptr::eq(
shared_table,
config.for_provider(ProviderKind::Siliconflow)
));
+1 -1
View File
@@ -223,7 +223,7 @@ provider!(
DEFAULT_SILICONFLOW_CN_BASE_URL,
DEFAULT_SILICONFLOW_MODEL,
["SILICONFLOW_API_KEY"],
"siliconflow"
"siliconflow_cn"
);
provider!(
Arcee,
+18 -9
View File
@@ -1979,6 +1979,8 @@ pub struct ProvidersConfig {
pub fireworks: ProviderConfig,
#[serde(default)]
pub siliconflow: ProviderConfig,
#[serde(default, alias = "siliconflow-CN", alias = "siliconflow-cn")]
pub siliconflow_cn: ProviderConfig,
#[serde(default)]
pub arcee: ProviderConfig,
#[serde(default)]
@@ -2302,7 +2304,8 @@ impl Config {
ApiProvider::XiaomiMimo => &providers.xiaomi_mimo,
ApiProvider::Novita => &providers.novita,
ApiProvider::Fireworks => &providers.fireworks,
ApiProvider::Siliconflow | ApiProvider::SiliconflowCn => &providers.siliconflow,
ApiProvider::Siliconflow => &providers.siliconflow,
ApiProvider::SiliconflowCn => &providers.siliconflow_cn,
ApiProvider::Arcee => &providers.arcee,
ApiProvider::Moonshot => &providers.moonshot,
ApiProvider::Sglang => &providers.sglang,
@@ -2329,7 +2332,8 @@ impl Config {
ApiProvider::XiaomiMimo => &mut providers.xiaomi_mimo,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Siliconflow | ApiProvider::SiliconflowCn => &mut providers.siliconflow,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::SiliconflowCn => &mut providers.siliconflow_cn,
ApiProvider::Arcee => &mut providers.arcee,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
@@ -3801,7 +3805,8 @@ 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 | ApiProvider::SiliconflowCn => &mut providers.siliconflow,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::SiliconflowCn => &mut providers.siliconflow_cn,
ApiProvider::Arcee => &mut providers.arcee,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
@@ -3998,7 +4003,8 @@ 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 | ApiProvider::SiliconflowCn => &mut providers.siliconflow,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::SiliconflowCn => &mut providers.siliconflow_cn,
ApiProvider::Arcee => &mut providers.arcee,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
@@ -4749,6 +4755,7 @@ fn merge_providers(
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),
siliconflow_cn: merge_provider_config(base.siliconflow_cn, override_cfg.siliconflow_cn),
arcee: merge_provider_config(base.arcee, override_cfg.arcee),
moonshot: merge_provider_config(base.moonshot, override_cfg.moonshot),
sglang: merge_provider_config(base.sglang, override_cfg.sglang),
@@ -5457,7 +5464,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
ApiProvider::Siliconflow => "siliconflow",
ApiProvider::SiliconflowCn => "siliconflow",
ApiProvider::SiliconflowCn => "siliconflow_cn",
ApiProvider::Arcee => "arcee",
ApiProvider::Huggingface => "huggingface",
ApiProvider::Moonshot => "moonshot",
@@ -10473,16 +10480,18 @@ api_key = "moonshot-platform-key"
assert_eq!(
parsed
.get("providers")
.and_then(|p| p.get("siliconflow"))
.and_then(|p| p.get("siliconflow_cn"))
.and_then(|t| t.get("api_key"))
.and_then(toml::Value::as_str),
Some("sf-cn-saved-key")
);
assert!(
assert_eq!(
parsed
.get("providers")
.and_then(|p| p.get("siliconflow-CN"))
.is_none()
.and_then(|p| p.get("siliconflow"))
.and_then(|t| t.get("api_key"))
.and_then(toml::Value::as_str),
Some("sf-saved-key")
);
Ok(())
}
+1 -1
View File
@@ -128,7 +128,7 @@ endpoint.
| `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. `SILICONFLOW_MODEL` is accepted. Reasoning aliases `deepseek-reasoner` and `deepseek-r1` map to Pro; `deepseek-chat` and `deepseek-v3` map to Flash. |
| `siliconflow-CN` | `[providers.siliconflow]` | `SILICONFLOW_API_KEY` | `SILICONFLOW_BASE_URL`; default `https://api.siliconflow.cn/v1` | Uses the SiliconFlow model set | China regional SiliconFlow route. This intentionally shares `[providers.siliconflow]` and `SILICONFLOW_API_KEY`; do not create `[providers.siliconflow_CN]`. Select it with `provider = "siliconflow-CN"` or `CODEWHALE_PROVIDER=siliconflow-CN`. |
| `siliconflow-CN` | `[providers.siliconflow_cn]` | `SILICONFLOW_API_KEY` | `SILICONFLOW_BASE_URL`; default `https://api.siliconflow.cn/v1` | Uses the SiliconFlow model set | China regional SiliconFlow route. Falls back to `[providers.siliconflow]` for api_key / base_url / model when unset. Select it with `provider = "siliconflow-CN"` or `CODEWHALE_PROVIDER=siliconflow-CN`. |
| `arcee` | `[providers.arcee]` | `ARCEE_API_KEY` | `ARCEE_BASE_URL`; default `https://api.arcee.ai/api/v1` | `trinity-large-thinking`, `trinity-large-preview` | Arcee AI direct OpenAI-compatible route, tracked as 256K-context BF16 serving. `ARCEE_MODEL` is accepted. OpenRouter's `arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced model ID; direct Arcee uses the bare `trinity-large-thinking` ID. |
| `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. |
+1 -1
View File
@@ -28,7 +28,7 @@ PROVIDERS_MD = ROOT / "docs" / "PROVIDERS.md"
API_PROVIDER_ONLY_IDS = {"deepseek-cn"}
SHARED_PROVIDER_TABLES = {
"siliconflow-CN": "siliconflow",
"siliconflow-CN": "siliconflow_cn",
}