fix(config): split SiliconFlow CN provider config
Harvest #2895 for the v0.8.59 release lane. SiliconFlow CN now reads its own providers.siliconflow_cn / providers.siliconflow-CN table and falls back to providers.siliconflow only when api_key, base_url, or model are unset. Maintainer amendments wire the TUI fallback paths, provider config get/set/unset/redaction surfaces, env override routing, model normalization, tests, and changelog credit. Fixes #2893. Reported-by: Artenx <18120598+Artenx@users.noreply.github.com>
This commit is contained in:
@@ -31,6 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- **SiliconFlow China provider config (#2893/#2895).** `siliconflow-CN`
|
||||
now reads its own `[providers.siliconflow_cn]` / `[providers.siliconflow-CN]`
|
||||
table and falls back to `[providers.siliconflow]` only for unset
|
||||
`api_key`/`base_url`/`model` fields. Thanks @Artenx for the report and
|
||||
@idling11 for the PR.
|
||||
- **TUI mouse-report leak (#3063/#3067).** Strip raw SGR mouse coordinate
|
||||
tails from the composer even when `use_mouse_capture` is false, covering
|
||||
orphaned terminal reporting state after crashes or focus races.
|
||||
|
||||
@@ -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"
|
||||
|
||||
+141
-7
@@ -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,
|
||||
@@ -1121,6 +1125,10 @@ impl ConfigToml {
|
||||
&mut self.providers.siliconflow,
|
||||
&project.providers.siliconflow,
|
||||
);
|
||||
merge_project_provider_config(
|
||||
&mut self.providers.siliconflow_cn,
|
||||
&project.providers.siliconflow_cn,
|
||||
);
|
||||
merge_project_provider_config(&mut self.providers.arcee, &project.providers.arcee);
|
||||
merge_project_provider_config(&mut self.providers.moonshot, &project.providers.moonshot);
|
||||
merge_project_provider_config(&mut self.providers.sglang, &project.providers.sglang);
|
||||
@@ -1220,6 +1228,12 @@ impl ConfigToml {
|
||||
"providers.siliconflow.http_headers" => {
|
||||
serialize_http_headers(&self.providers.siliconflow.http_headers)
|
||||
}
|
||||
"providers.siliconflow_cn.api_key" => self.providers.siliconflow_cn.api_key.clone(),
|
||||
"providers.siliconflow_cn.base_url" => self.providers.siliconflow_cn.base_url.clone(),
|
||||
"providers.siliconflow_cn.model" => self.providers.siliconflow_cn.model.clone(),
|
||||
"providers.siliconflow_cn.http_headers" => {
|
||||
serialize_http_headers(&self.providers.siliconflow_cn.http_headers)
|
||||
}
|
||||
"providers.arcee.api_key" => self.providers.arcee.api_key.clone(),
|
||||
"providers.arcee.base_url" => self.providers.arcee.base_url.clone(),
|
||||
"providers.arcee.model" => self.providers.arcee.model.clone(),
|
||||
@@ -1439,6 +1453,18 @@ impl ConfigToml {
|
||||
"providers.siliconflow.http_headers" => {
|
||||
self.providers.siliconflow.http_headers = parse_http_headers(value)?;
|
||||
}
|
||||
"providers.siliconflow_cn.api_key" => {
|
||||
self.providers.siliconflow_cn.api_key = Some(value.to_string());
|
||||
}
|
||||
"providers.siliconflow_cn.base_url" => {
|
||||
self.providers.siliconflow_cn.base_url = Some(value.to_string());
|
||||
}
|
||||
"providers.siliconflow_cn.model" => {
|
||||
self.providers.siliconflow_cn.model = Some(value.to_string());
|
||||
}
|
||||
"providers.siliconflow_cn.http_headers" => {
|
||||
self.providers.siliconflow_cn.http_headers = parse_http_headers(value)?;
|
||||
}
|
||||
"providers.arcee.api_key" => {
|
||||
self.providers.arcee.api_key = Some(value.to_string());
|
||||
}
|
||||
@@ -1618,6 +1644,12 @@ impl ConfigToml {
|
||||
"providers.siliconflow.http_headers" => {
|
||||
self.providers.siliconflow.http_headers.clear();
|
||||
}
|
||||
"providers.siliconflow_cn.api_key" => self.providers.siliconflow_cn.api_key = None,
|
||||
"providers.siliconflow_cn.base_url" => self.providers.siliconflow_cn.base_url = None,
|
||||
"providers.siliconflow_cn.model" => self.providers.siliconflow_cn.model = None,
|
||||
"providers.siliconflow_cn.http_headers" => {
|
||||
self.providers.siliconflow_cn.http_headers.clear();
|
||||
}
|
||||
"providers.arcee.api_key" => self.providers.arcee.api_key = None,
|
||||
"providers.arcee.base_url" => self.providers.arcee.base_url = None,
|
||||
"providers.arcee.model" => self.providers.arcee.model = None,
|
||||
@@ -1845,6 +1877,21 @@ impl ConfigToml {
|
||||
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.siliconflow_cn.api_key.as_ref() {
|
||||
out.insert(
|
||||
"providers.siliconflow_cn.api_key".to_string(),
|
||||
redact_secret(v),
|
||||
);
|
||||
}
|
||||
if let Some(v) = self.providers.siliconflow_cn.base_url.as_ref() {
|
||||
out.insert("providers.siliconflow_cn.base_url".to_string(), v.clone());
|
||||
}
|
||||
if let Some(v) = self.providers.siliconflow_cn.model.as_ref() {
|
||||
out.insert("providers.siliconflow_cn.model".to_string(), v.clone());
|
||||
}
|
||||
if let Some(v) = serialize_http_headers(&self.providers.siliconflow_cn.http_headers) {
|
||||
out.insert("providers.siliconflow_cn.http_headers".to_string(), v);
|
||||
}
|
||||
if let Some(v) = self.providers.arcee.api_key.as_ref() {
|
||||
out.insert("providers.arcee.api_key".to_string(), redact_secret(v));
|
||||
}
|
||||
@@ -1956,7 +2003,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();
|
||||
@@ -2284,11 +2343,11 @@ fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String {
|
||||
DEFAULT_FIREWORKS_MODEL.to_string()
|
||||
}
|
||||
(
|
||||
ProviderKind::Siliconflow,
|
||||
ProviderKind::Siliconflow | ProviderKind::SiliconflowCN,
|
||||
"deepseek-v4-pro" | "deepseek-v4pro" | "deepseek-reasoner" | "deepseek-r1",
|
||||
) => DEFAULT_SILICONFLOW_MODEL.to_string(),
|
||||
(
|
||||
ProviderKind::Siliconflow,
|
||||
ProviderKind::Siliconflow | ProviderKind::SiliconflowCN,
|
||||
"deepseek-v4-flash" | "deepseek-v4flash" | "deepseek-chat" | "deepseek-v3",
|
||||
) => DEFAULT_SILICONFLOW_FLASH_MODEL.to_string(),
|
||||
(
|
||||
@@ -4632,6 +4691,59 @@ unix_socket_path = "/tmp/cw-hooks.sock"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn siliconflow_cn_provider_config_values_round_trip() -> Result<()> {
|
||||
let mut config = ConfigToml::default();
|
||||
|
||||
config.set_value("providers.siliconflow_cn.api_key", "sf-cn-secret-value")?;
|
||||
config.set_value(
|
||||
"providers.siliconflow_cn.base_url",
|
||||
DEFAULT_SILICONFLOW_CN_BASE_URL,
|
||||
)?;
|
||||
config.set_value("providers.siliconflow_cn.model", DEFAULT_SILICONFLOW_MODEL)?;
|
||||
config.set_value("providers.siliconflow_cn.http_headers", "X-Test=ok")?;
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.get_display_value("providers.siliconflow_cn.api_key")
|
||||
.as_deref(),
|
||||
Some("sf-c***alue")
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.get_value("providers.siliconflow_cn.base_url")
|
||||
.as_deref(),
|
||||
Some(DEFAULT_SILICONFLOW_CN_BASE_URL)
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.get_value("providers.siliconflow_cn.model")
|
||||
.as_deref(),
|
||||
Some(DEFAULT_SILICONFLOW_MODEL)
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.list_values()
|
||||
.get("providers.siliconflow_cn.api_key")
|
||||
.map(String::as_str),
|
||||
Some("sf-c***alue")
|
||||
);
|
||||
|
||||
config.unset_value("providers.siliconflow_cn.api_key")?;
|
||||
config.unset_value("providers.siliconflow_cn.base_url")?;
|
||||
config.unset_value("providers.siliconflow_cn.model")?;
|
||||
config.unset_value("providers.siliconflow_cn.http_headers")?;
|
||||
|
||||
assert_eq!(config.get_value("providers.siliconflow_cn.api_key"), None);
|
||||
assert_eq!(config.get_value("providers.siliconflow_cn.base_url"), None);
|
||||
assert_eq!(config.get_value("providers.siliconflow_cn.model"), None);
|
||||
assert_eq!(
|
||||
config.get_value("providers.siliconflow_cn.http_headers"),
|
||||
None
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volcengine_provider_config_values_round_trip() -> Result<()> {
|
||||
let mut config = ConfigToml::default();
|
||||
@@ -5135,11 +5247,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)
|
||||
));
|
||||
@@ -5363,6 +5475,28 @@ mode = "token-plan-usa"
|
||||
assert_eq!(resolved.model, DEFAULT_SILICONFLOW_MODEL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn siliconflow_cn_config_falls_back_to_shared_table_when_unset() {
|
||||
let _lock = env_lock();
|
||||
let _env = EnvGuard::without_deepseek_runtime_overrides();
|
||||
let mut config = ConfigToml {
|
||||
provider: ProviderKind::SiliconflowCN,
|
||||
..ConfigToml::default()
|
||||
};
|
||||
config.providers.siliconflow.api_key = Some("sf-shared-key".to_string());
|
||||
config.providers.siliconflow.base_url = Some(DEFAULT_SILICONFLOW_BASE_URL.to_string());
|
||||
config.providers.siliconflow.model = Some("deepseek-chat".to_string());
|
||||
config.providers.siliconflow_cn.base_url =
|
||||
Some(DEFAULT_SILICONFLOW_CN_BASE_URL.to_string());
|
||||
|
||||
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
|
||||
|
||||
assert_eq!(resolved.provider, ProviderKind::SiliconflowCN);
|
||||
assert_eq!(resolved.api_key.as_deref(), Some("sf-shared-key"));
|
||||
assert_eq!(resolved.base_url, DEFAULT_SILICONFLOW_CN_BASE_URL);
|
||||
assert_eq!(resolved.model, DEFAULT_SILICONFLOW_FLASH_MODEL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moonshot_provider_defaults_to_kimi_k2() {
|
||||
let _lock = env_lock();
|
||||
|
||||
@@ -223,7 +223,7 @@ provider!(
|
||||
DEFAULT_SILICONFLOW_CN_BASE_URL,
|
||||
DEFAULT_SILICONFLOW_MODEL,
|
||||
["SILICONFLOW_API_KEY"],
|
||||
"siliconflow"
|
||||
"siliconflow_cn"
|
||||
);
|
||||
provider!(
|
||||
Arcee,
|
||||
|
||||
@@ -31,6 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- **SiliconFlow China provider config (#2893/#2895).** `siliconflow-CN`
|
||||
now reads its own `[providers.siliconflow_cn]` / `[providers.siliconflow-CN]`
|
||||
table and falls back to `[providers.siliconflow]` only for unset
|
||||
`api_key`/`base_url`/`model` fields. Thanks @Artenx for the report and
|
||||
@idling11 for the PR.
|
||||
- **TUI mouse-report leak (#3063/#3067).** Strip raw SGR mouse coordinate
|
||||
tails from the composer even when `use_mouse_capture` is false, covering
|
||||
orphaned terminal reporting state after crashes or focus races.
|
||||
|
||||
+217
-41
@@ -2012,6 +2012,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)]
|
||||
@@ -2184,7 +2186,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",
|
||||
@@ -2335,7 +2338,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,
|
||||
@@ -2362,7 +2366,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,
|
||||
@@ -2380,6 +2385,28 @@ impl Config {
|
||||
self.provider_config_for(self.api_provider())
|
||||
}
|
||||
|
||||
fn provider_config_string_with_runtime_fallback<F>(
|
||||
&self,
|
||||
provider: ApiProvider,
|
||||
get: F,
|
||||
) -> Option<String>
|
||||
where
|
||||
F: Fn(&ProviderConfig) -> Option<String>,
|
||||
{
|
||||
if let Some(value) = self
|
||||
.provider_config_for(provider)
|
||||
.and_then(|entry| get(entry))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
if provider == ApiProvider::SiliconflowCn {
|
||||
return self
|
||||
.provider_config_for(ApiProvider::Siliconflow)
|
||||
.and_then(|entry| get(entry));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn insecure_skip_tls_verify(&self) -> bool {
|
||||
self.provider_config()
|
||||
@@ -2403,14 +2430,14 @@ impl Config {
|
||||
#[must_use]
|
||||
pub fn default_model(&self) -> String {
|
||||
let provider = self.api_provider();
|
||||
if let Some(model) = self
|
||||
.provider_config()
|
||||
.and_then(|provider| provider.model.as_deref())
|
||||
if let Some(model) =
|
||||
self.provider_config_string_with_runtime_fallback(provider, |entry| entry.model.clone())
|
||||
{
|
||||
let model = model.trim();
|
||||
if provider_passes_model_through(provider)
|
||||
|| self.active_provider_preserves_custom_base_url_model()
|
||||
{
|
||||
return model.trim().to_string();
|
||||
return model.to_string();
|
||||
}
|
||||
if let Some(normalized) = normalize_model_for_provider(provider, model) {
|
||||
return normalized;
|
||||
@@ -2421,9 +2448,8 @@ impl Config {
|
||||
// It must pass through verbatim rather than fall back to a
|
||||
// DeepSeek/provider default (issue #1714).
|
||||
if !matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
|
||||
let trimmed = model.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return trimmed.to_string();
|
||||
if !model.is_empty() {
|
||||
return model.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2506,8 +2532,7 @@ impl Config {
|
||||
pub fn deepseek_base_url(&self) -> String {
|
||||
let provider = self.api_provider();
|
||||
let provider_base = self
|
||||
.provider_config_for(provider)
|
||||
.and_then(|provider| provider.base_url.clone());
|
||||
.provider_config_string_with_runtime_fallback(provider, |entry| entry.base_url.clone());
|
||||
// Root `base_url` is the legacy DeepSeek field; only NvidiaNim has a
|
||||
// back-compat sniff (integrate.api.nvidia.com). OpenRouter / Novita
|
||||
// were added in v0.6.7 and require explicit `[providers.<name>]`
|
||||
@@ -2682,8 +2707,7 @@ impl Config {
|
||||
// 1. Config file (provider-scoped slot). This intentionally wins
|
||||
// over ambient env so `codewhale auth set` fixes stale shell exports.
|
||||
if let Some(configured) = self
|
||||
.provider_config_for(provider)
|
||||
.and_then(|provider| provider.api_key.clone())
|
||||
.provider_config_string_with_runtime_fallback(provider, |entry| entry.api_key.clone())
|
||||
&& !configured.trim().is_empty()
|
||||
{
|
||||
return Ok(configured);
|
||||
@@ -2779,10 +2803,15 @@ 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 ~/.codewhale/config.toml."
|
||||
),
|
||||
ApiProvider::Siliconflow | ApiProvider::SiliconflowCn => anyhow::bail!(
|
||||
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 ~/.codewhale/config.toml."
|
||||
),
|
||||
ApiProvider::SiliconflowCn => anyhow::bail!(
|
||||
"SiliconFlow China API key not found. Run 'codewhale auth set --provider siliconflow-CN', \
|
||||
set SILICONFLOW_API_KEY, or add [providers.siliconflow_cn] api_key in ~/.codewhale/config.toml. \
|
||||
[providers.siliconflow] remains a fallback when the CN table omits api_key."
|
||||
),
|
||||
ApiProvider::Arcee => anyhow::bail!(
|
||||
"Arcee AI API key not found. Run 'codewhale auth set --provider arcee', \
|
||||
set ARCEE_API_KEY, or add [providers.arcee] api_key in ~/.codewhale/config.toml."
|
||||
@@ -3573,13 +3602,20 @@ fn apply_env_overrides(config: &mut Config) {
|
||||
.fireworks
|
||||
.base_url = Some(value);
|
||||
}
|
||||
ApiProvider::Siliconflow | ApiProvider::SiliconflowCn => {
|
||||
ApiProvider::Siliconflow => {
|
||||
config
|
||||
.providers
|
||||
.get_or_insert_with(ProvidersConfig::default)
|
||||
.siliconflow
|
||||
.base_url = Some(value);
|
||||
}
|
||||
ApiProvider::SiliconflowCn => {
|
||||
config
|
||||
.providers
|
||||
.get_or_insert_with(ProvidersConfig::default)
|
||||
.siliconflow_cn
|
||||
.base_url = Some(value);
|
||||
}
|
||||
ApiProvider::Arcee => {
|
||||
config
|
||||
.providers
|
||||
@@ -3761,17 +3797,14 @@ fn apply_env_overrides(config: &mut Config) {
|
||||
.fireworks
|
||||
.base_url = Some(value);
|
||||
}
|
||||
let active_provider = config.api_provider();
|
||||
if matches!(
|
||||
config.api_provider(),
|
||||
active_provider,
|
||||
ApiProvider::Siliconflow | ApiProvider::SiliconflowCn
|
||||
) && 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);
|
||||
config.provider_config_for_mut(active_provider).base_url = Some(value);
|
||||
}
|
||||
if matches!(config.api_provider(), ApiProvider::Arcee)
|
||||
&& let Ok(value) = std::env::var("ARCEE_BASE_URL")
|
||||
@@ -3848,7 +3881,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,
|
||||
@@ -3978,17 +4012,14 @@ fn apply_env_overrides(config: &mut Config) {
|
||||
.moonshot
|
||||
.model = Some(value);
|
||||
}
|
||||
let active_provider = config.api_provider();
|
||||
if matches!(
|
||||
config.api_provider(),
|
||||
active_provider,
|
||||
ApiProvider::Siliconflow | ApiProvider::SiliconflowCn
|
||||
) && let Ok(value) = std::env::var("SILICONFLOW_MODEL")
|
||||
&& !value.trim().is_empty()
|
||||
{
|
||||
config
|
||||
.providers
|
||||
.get_or_insert_with(ProvidersConfig::default)
|
||||
.siliconflow
|
||||
.model = Some(value);
|
||||
config.provider_config_for_mut(active_provider).model = Some(value);
|
||||
}
|
||||
if matches!(config.api_provider(), ApiProvider::Arcee)
|
||||
&& let Ok(value) = std::env::var("ARCEE_MODEL")
|
||||
@@ -4045,7 +4076,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,
|
||||
@@ -4303,6 +4335,16 @@ fn normalize_model_config(config: &mut Config) {
|
||||
{
|
||||
providers.siliconflow.model = Some(normalized);
|
||||
}
|
||||
if let Some(model) = providers.siliconflow_cn.model.as_deref()
|
||||
&& !provider_entry_uses_custom_base_url(
|
||||
ApiProvider::SiliconflowCn,
|
||||
&providers.siliconflow_cn,
|
||||
)
|
||||
&& let Some(normalized) =
|
||||
normalize_model_for_provider(ApiProvider::SiliconflowCn, model)
|
||||
{
|
||||
providers.siliconflow_cn.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)
|
||||
@@ -4796,6 +4838,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),
|
||||
@@ -5232,8 +5275,7 @@ pub fn active_provider_has_config_api_key(config: &Config) -> bool {
|
||||
}
|
||||
|
||||
if config
|
||||
.provider_config_for(provider)
|
||||
.and_then(|entry| entry.api_key.as_ref())
|
||||
.provider_config_string_with_runtime_fallback(provider, |entry| entry.api_key.clone())
|
||||
.is_some_and(|k| !k.trim().is_empty() && k != API_KEYRING_SENTINEL)
|
||||
{
|
||||
return true;
|
||||
@@ -5408,8 +5450,7 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
|
||||
}
|
||||
|
||||
if config
|
||||
.provider_config_for(provider)
|
||||
.and_then(|entry| entry.api_key.as_ref())
|
||||
.provider_config_string_with_runtime_fallback(provider, |entry| entry.api_key.clone())
|
||||
.is_some_and(|k| !k.trim().is_empty() && k != API_KEYRING_SENTINEL)
|
||||
{
|
||||
return true;
|
||||
@@ -5458,7 +5499,8 @@ 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 | ApiProvider::SiliconflowCn => "providers.siliconflow",
|
||||
ApiProvider::Siliconflow => "providers.siliconflow",
|
||||
ApiProvider::SiliconflowCn => "providers.siliconflow_cn",
|
||||
ApiProvider::Arcee => "providers.arcee",
|
||||
ApiProvider::Huggingface => "providers.huggingface",
|
||||
ApiProvider::Moonshot => "providers.moonshot",
|
||||
@@ -5504,7 +5546,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",
|
||||
@@ -5603,7 +5645,7 @@ fn provider_config_key(provider: ApiProvider) -> Result<&'static str> {
|
||||
ApiProvider::Novita => Ok("novita"),
|
||||
ApiProvider::Fireworks => Ok("fireworks"),
|
||||
ApiProvider::Siliconflow => Ok("siliconflow"),
|
||||
ApiProvider::SiliconflowCn => Ok("siliconflow"),
|
||||
ApiProvider::SiliconflowCn => Ok("siliconflow_cn"),
|
||||
ApiProvider::Arcee => Ok("arcee"),
|
||||
ApiProvider::Huggingface => Ok("huggingface"),
|
||||
ApiProvider::Moonshot => Ok("moonshot"),
|
||||
@@ -9970,6 +10012,138 @@ model = "deepseek-v4-flash"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn siliconflow_cn_reads_hyphenated_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-cn-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-CN"
|
||||
|
||||
[providers.siliconflow-CN]
|
||||
api_key = "sf-cn-table-key"
|
||||
base_url = "https://api.siliconflow.cn/v1"
|
||||
model = "deepseek-reasoner"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = Config::load(None, None)?;
|
||||
assert_eq!(config.api_provider(), ApiProvider::SiliconflowCn);
|
||||
assert_eq!(config.deepseek_api_key()?, "sf-cn-table-key");
|
||||
assert_eq!(config.deepseek_base_url(), DEFAULT_SILICONFLOW_CN_BASE_URL);
|
||||
assert_eq!(config.default_model(), DEFAULT_SILICONFLOW_MODEL);
|
||||
assert!(has_api_key_for(&config, ApiProvider::SiliconflowCn));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn siliconflow_cn_falls_back_to_shared_siliconflow_table_when_unset() -> 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-fallback-{}-{}",
|
||||
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-CN"
|
||||
|
||||
[providers.siliconflow]
|
||||
api_key = "sf-shared-key"
|
||||
base_url = "https://api.siliconflow.com/v1"
|
||||
model = "deepseek-chat"
|
||||
|
||||
[providers.siliconflow_cn]
|
||||
base_url = "https://api.siliconflow.cn/v1"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = Config::load(None, None)?;
|
||||
assert_eq!(config.api_provider(), ApiProvider::SiliconflowCn);
|
||||
assert_eq!(config.deepseek_api_key()?, "sf-shared-key");
|
||||
assert_eq!(config.deepseek_base_url(), DEFAULT_SILICONFLOW_CN_BASE_URL);
|
||||
assert_eq!(config.default_model(), DEFAULT_SILICONFLOW_FLASH_MODEL);
|
||||
assert!(active_provider_has_config_api_key(&config));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn siliconflow_cn_env_overrides_write_cn_table_only() -> 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-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-CN"
|
||||
|
||||
[providers.siliconflow]
|
||||
api_key = "sf-shared-key"
|
||||
base_url = "https://api.siliconflow.com/v1"
|
||||
model = "deepseek-reasoner"
|
||||
"#,
|
||||
)?;
|
||||
unsafe {
|
||||
env::set_var("SILICONFLOW_BASE_URL", "https://api.siliconflow.cn/v1");
|
||||
env::set_var("SILICONFLOW_MODEL", "deepseek-chat");
|
||||
}
|
||||
|
||||
let config = Config::load(None, None)?;
|
||||
let providers = config.providers.as_ref().expect("providers");
|
||||
assert_eq!(
|
||||
providers.siliconflow.base_url.as_deref(),
|
||||
Some(DEFAULT_SILICONFLOW_BASE_URL)
|
||||
);
|
||||
assert_eq!(
|
||||
providers.siliconflow.model.as_deref(),
|
||||
Some(DEFAULT_SILICONFLOW_MODEL)
|
||||
);
|
||||
assert_eq!(
|
||||
providers.siliconflow_cn.base_url.as_deref(),
|
||||
Some(DEFAULT_SILICONFLOW_CN_BASE_URL)
|
||||
);
|
||||
assert_eq!(
|
||||
providers.siliconflow_cn.model.as_deref(),
|
||||
Some(DEFAULT_SILICONFLOW_FLASH_MODEL)
|
||||
);
|
||||
assert_eq!(config.deepseek_api_key()?, "sf-shared-key");
|
||||
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();
|
||||
@@ -10549,16 +10723,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
@@ -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. |
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user