mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
feat(providers): add CSDN 星图 with Coding Plan billing
CSDN's hosted OpenAI-compatible platform (ai.csdn.net/api/model/v1) serves the Coding Plan's dedicated model glm_for_coding — the shipped default — plus pass-through marketplace model ids, all over the one endpoint and one CSDN_API_KEY slot. Since the URL proves neither product, billing follows captured credential product MiniMax-style: plan mode or the plan model id bills as CSDN Coding Plan quota with no dollar estimates, other models or explicit payg bill metered, and unrecognized modes or off-platform endpoints stay cost: unknown. Covers the full spine: ProviderKind + serde aliases (csdn-ai, csdn-coding-plan, starmap), config/env overrides (CSDN_BASE_URL, CSDN_MODEL), secrets env lookup, TUI provider/config/runtime surfaces, agent registry entry for glm_for_coding, route export goldens, pricing/billing surfaces, EN+ZH provider docs, website facts maps, and config.example.toml. No invented reasoning dialect — CSDN documents no provider-owned reasoning controls.
This commit is contained in:
1 parent
77f7582983
commit
e2470e9f3f
26 files changed
+418
-42
No files matched your search
+1
-1
@@ -21,7 +21,7 @@
|
||||
# `api_key` / `base_url` are
|
||||
# still read as DeepSeek defaults when `[providers.deepseek]` is absent
|
||||
# (backward compatibility).
|
||||
provider = "deepseek" # deepseek | deepseek-cn | deepseek-anthropic | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | orcarouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | zai | stepfun | minimax | sglang | vllm | ollama | huggingface | modelscope | together | qianfan | openai-codex | anthropic | openmodel | deepinfra | sakana | longcat | opencode-go | opencode-zen | meta | xai | modelstudio-token-plan | modelstudio-coding-plan | codewhale
|
||||
provider = "deepseek" # deepseek | deepseek-cn | deepseek-anthropic | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | orcarouter | xiaomi-mimo | novita | fireworks | siliconflow | siliconflow-CN | arcee | moonshot | zai | stepfun | minimax | sglang | vllm | ollama | ollama-cloud | huggingface | modelscope | together | qianfan | openai-codex | anthropic | openmodel | deepinfra | sakana | longcat | opencode-go | opencode-zen | meta | xai | mistral | telecomjs | google | edenai | zenmux | csdn | concentrate | modelstudio-token-plan | modelstudio-coding-plan | codewhale
|
||||
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)
|
||||
|
||||
@@ -1006,6 +1006,18 @@ impl Default for ModelRegistry {
|
||||
supports_tools: true,
|
||||
supports_reasoning: true,
|
||||
},
|
||||
// CSDN 星图 (Starmap) — the Coding Plan's dedicated model id.
|
||||
// Other CSDN marketplace models resolve through pass-through.
|
||||
ModelInfo {
|
||||
id: "glm_for_coding".to_string(),
|
||||
provider: ProviderKind::Csdn,
|
||||
aliases: vec![
|
||||
"glm-for-coding".to_string(),
|
||||
"csdn-glm-for-coding".to_string(),
|
||||
],
|
||||
supports_tools: true,
|
||||
supports_reasoning: true,
|
||||
},
|
||||
// Together AI provider models
|
||||
ModelInfo {
|
||||
id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
|
||||
|
||||
@@ -10150,11 +10150,11 @@ verbosity = "project-imported"
|
||||
.map(|provider| provider.kind())
|
||||
.collect();
|
||||
// Full registry keeps legacy dialect/plan kinds; ALL is the catalog surface.
|
||||
assert_eq!(registry_kinds.len(), 51);
|
||||
assert_eq!(registry_kinds.len(), 52);
|
||||
// The tombstone stays in the registry (old config must still parse
|
||||
// and clear) and left the catalog surface when it stopped being
|
||||
// selectable.
|
||||
assert_eq!(ProviderKind::ALL.len(), 45);
|
||||
assert_eq!(ProviderKind::ALL.len(), 46);
|
||||
for kind in ProviderKind::ALL {
|
||||
assert!(
|
||||
registry_kinds.contains(&kind),
|
||||
|
||||
@@ -532,6 +532,17 @@ pub struct ProvidersToml {
|
||||
alias = "zen_mux"
|
||||
)]
|
||||
pub zenmux: ProviderConfigToml,
|
||||
/// CSDN 星图 — hosted OpenAI-compatible platform and Coding Plan.
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "ProviderConfigToml::is_empty",
|
||||
alias = "csdn-ai",
|
||||
alias = "csdn_ai",
|
||||
alias = "csdn-coding-plan",
|
||||
alias = "csdn_coding_plan",
|
||||
alias = "starmap"
|
||||
)]
|
||||
pub csdn: ProviderConfigToml,
|
||||
/// Concentrate — OpenAI Responses-compatible AI gateway (aggregator).
|
||||
#[serde(
|
||||
default,
|
||||
@@ -778,6 +789,7 @@ impl ProvidersToml {
|
||||
ProviderKind::Telecomjs => &self.telecomjs,
|
||||
ProviderKind::Edenai => &self.edenai,
|
||||
ProviderKind::Zenmux => &self.zenmux,
|
||||
ProviderKind::Csdn => &self.csdn,
|
||||
ProviderKind::Concentrate => &self.concentrate,
|
||||
ProviderKind::Codewhale => &self.codewhale,
|
||||
ProviderKind::ModelstudioTokenPlan => &self.modelstudio_token_plan,
|
||||
@@ -834,6 +846,7 @@ impl ProvidersToml {
|
||||
ProviderKind::Telecomjs => &mut self.telecomjs,
|
||||
ProviderKind::Edenai => &mut self.edenai,
|
||||
ProviderKind::Zenmux => &mut self.zenmux,
|
||||
ProviderKind::Csdn => &mut self.csdn,
|
||||
ProviderKind::Concentrate => &mut self.concentrate,
|
||||
ProviderKind::Codewhale => &mut self.codewhale,
|
||||
ProviderKind::ModelstudioTokenPlan => &mut self.modelstudio_token_plan,
|
||||
@@ -3929,6 +3942,7 @@ fn provider_passes_model_through(provider: ProviderKind) -> bool {
|
||||
| ProviderKind::Telecomjs
|
||||
| ProviderKind::Edenai
|
||||
| ProviderKind::Zenmux
|
||||
| ProviderKind::Csdn
|
||||
| ProviderKind::Concentrate
|
||||
| ProviderKind::ModelstudioTokenPlan
|
||||
| ProviderKind::ModelstudioTokenPlanAnthropic
|
||||
@@ -4467,6 +4481,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
|
||||
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_MODEL,
|
||||
ProviderKind::Edenai => DEFAULT_EDENAI_MODEL,
|
||||
ProviderKind::Zenmux => DEFAULT_ZENMUX_MODEL,
|
||||
ProviderKind::Csdn => DEFAULT_CSDN_MODEL,
|
||||
ProviderKind::Concentrate => DEFAULT_CONCENTRATE_MODEL,
|
||||
ProviderKind::Codewhale => DEFAULT_CODEWHALE_MODEL,
|
||||
ProviderKind::ModelstudioTokenPlan
|
||||
@@ -4524,6 +4539,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
|
||||
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
|
||||
ProviderKind::Edenai => DEFAULT_EDENAI_BASE_URL,
|
||||
ProviderKind::Zenmux => DEFAULT_ZENMUX_BASE_URL,
|
||||
ProviderKind::Csdn => DEFAULT_CSDN_BASE_URL,
|
||||
ProviderKind::Concentrate => DEFAULT_CONCENTRATE_BASE_URL,
|
||||
ProviderKind::Codewhale => DEFAULT_CODEWHALE_BASE_URL,
|
||||
ProviderKind::ModelstudioTokenPlan => DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
|
||||
@@ -4825,6 +4841,7 @@ pub fn provider_base_url_is_official(provider: ProviderKind, base_url: &str) ->
|
||||
"https://api.edenai.run/v3" | "https://api.eu.edenai.run/v3"
|
||||
),
|
||||
ProviderKind::Zenmux => normalized == DEFAULT_ZENMUX_BASE_URL,
|
||||
ProviderKind::Csdn => normalized == DEFAULT_CSDN_BASE_URL,
|
||||
ProviderKind::Concentrate => normalized == DEFAULT_CONCENTRATE_BASE_URL,
|
||||
// The Codewhale API's official endpoint family is its default base
|
||||
// plus whatever the operator declared in `CODEWHALE_API_BASE` — the
|
||||
@@ -6997,6 +7014,8 @@ struct EnvRuntimeOverrides {
|
||||
edenai_model: Option<String>,
|
||||
zenmux_base_url: Option<String>,
|
||||
zenmux_model: Option<String>,
|
||||
csdn_base_url: Option<String>,
|
||||
csdn_model: Option<String>,
|
||||
concentrate_base_url: Option<String>,
|
||||
concentrate_model: Option<String>,
|
||||
codewhale_base_url: Option<String>,
|
||||
@@ -7365,6 +7384,12 @@ impl EnvRuntimeOverrides {
|
||||
zenmux_model: std::env::var("ZENMUX_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
csdn_base_url: std::env::var("CSDN_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
csdn_model: std::env::var("CSDN_MODEL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
concentrate_base_url: std::env::var("CONCENTRATE_BASE_URL")
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty()),
|
||||
@@ -7462,6 +7487,7 @@ impl EnvRuntimeOverrides {
|
||||
ProviderKind::Telecomjs => self.telecomjs_base_url.clone(),
|
||||
ProviderKind::Edenai => self.edenai_base_url.clone(),
|
||||
ProviderKind::Zenmux => self.zenmux_base_url.clone(),
|
||||
ProviderKind::Csdn => self.csdn_base_url.clone(),
|
||||
ProviderKind::Concentrate => self.concentrate_base_url.clone(),
|
||||
ProviderKind::Codewhale => self.codewhale_base_url.clone(),
|
||||
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
|
||||
@@ -7513,6 +7539,7 @@ impl EnvRuntimeOverrides {
|
||||
ProviderKind::Telecomjs => self.telecomjs_model.clone(),
|
||||
ProviderKind::Edenai => self.edenai_model.clone(),
|
||||
ProviderKind::Zenmux => self.zenmux_model.clone(),
|
||||
ProviderKind::Csdn => self.csdn_model.clone(),
|
||||
ProviderKind::Concentrate => self.concentrate_model.clone(),
|
||||
ProviderKind::Codewhale => self.codewhale_model.clone(),
|
||||
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
|
||||
|
||||
@@ -9,35 +9,36 @@ use super::{
|
||||
DEFAULT_ANTIGRAVITY_BASE_URL, DEFAULT_ANTIGRAVITY_MODEL, DEFAULT_ARCEE_BASE_URL,
|
||||
DEFAULT_ARCEE_MODEL, DEFAULT_ATLASCLOUD_BASE_URL, DEFAULT_ATLASCLOUD_MODEL,
|
||||
DEFAULT_CODEWHALE_BASE_URL, DEFAULT_CODEWHALE_MODEL, DEFAULT_CONCENTRATE_BASE_URL,
|
||||
DEFAULT_CONCENTRATE_MODEL, DEFAULT_DEEPINFRA_BASE_URL, DEFAULT_DEEPINFRA_MODEL,
|
||||
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL, DEFAULT_DEEPSEEK_ANTHROPIC_MODEL,
|
||||
DEFAULT_DEEPSEEK_BASE_URL, DEFAULT_DEEPSEEK_MODEL, DEFAULT_EDENAI_BASE_URL,
|
||||
DEFAULT_EDENAI_MODEL, DEFAULT_FIREWORKS_BASE_URL, DEFAULT_FIREWORKS_MODEL,
|
||||
DEFAULT_GOOGLE_BASE_URL, DEFAULT_GOOGLE_MODEL, DEFAULT_HUGGINGFACE_BASE_URL,
|
||||
DEFAULT_HUGGINGFACE_MODEL, DEFAULT_LONGCAT_BASE_URL, DEFAULT_LONGCAT_MODEL,
|
||||
DEFAULT_META_BASE_URL, DEFAULT_META_MODEL, DEFAULT_MINIMAX_ANTHROPIC_BASE_URL,
|
||||
DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL, DEFAULT_MISTRAL_BASE_URL,
|
||||
DEFAULT_MISTRAL_MODEL, DEFAULT_MODELSCOPE_BASE_URL, DEFAULT_MODELSCOPE_MODEL,
|
||||
DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
|
||||
DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL, DEFAULT_MOONSHOT_BASE_URL, DEFAULT_MOONSHOT_MODEL,
|
||||
DEFAULT_NOVITA_BASE_URL, DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL,
|
||||
DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_CLOUD_BASE_URL,
|
||||
DEFAULT_OLLAMA_CLOUD_MODEL, DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL,
|
||||
DEFAULT_OPENAI_CODEX_BASE_URL, DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL,
|
||||
DEFAULT_OPENCODE_GO_BASE_URL, DEFAULT_OPENCODE_GO_MODEL, DEFAULT_OPENCODE_ZEN_BASE_URL,
|
||||
DEFAULT_OPENCODE_ZEN_MODEL, DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL,
|
||||
DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL, DEFAULT_ORCAROUTER_BASE_URL,
|
||||
DEFAULT_ORCAROUTER_MODEL, DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL,
|
||||
DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL,
|
||||
DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL,
|
||||
DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL, DEFAULT_TELECOMJS_BASE_URL,
|
||||
DEFAULT_TELECOMJS_MODEL, DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL,
|
||||
DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL,
|
||||
DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL,
|
||||
DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL,
|
||||
DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL, DEFAULT_ZAI_MODEL, DEFAULT_ZENMUX_BASE_URL,
|
||||
DEFAULT_ZENMUX_MODEL, MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL,
|
||||
MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL, ProviderKind,
|
||||
DEFAULT_CONCENTRATE_MODEL, DEFAULT_CSDN_BASE_URL, DEFAULT_CSDN_MODEL,
|
||||
DEFAULT_DEEPINFRA_BASE_URL, DEFAULT_DEEPINFRA_MODEL, DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL,
|
||||
DEFAULT_DEEPSEEK_ANTHROPIC_MODEL, DEFAULT_DEEPSEEK_BASE_URL, DEFAULT_DEEPSEEK_MODEL,
|
||||
DEFAULT_EDENAI_BASE_URL, DEFAULT_EDENAI_MODEL, DEFAULT_FIREWORKS_BASE_URL,
|
||||
DEFAULT_FIREWORKS_MODEL, DEFAULT_GOOGLE_BASE_URL, DEFAULT_GOOGLE_MODEL,
|
||||
DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL, DEFAULT_LONGCAT_BASE_URL,
|
||||
DEFAULT_LONGCAT_MODEL, DEFAULT_META_BASE_URL, DEFAULT_META_MODEL,
|
||||
DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL,
|
||||
DEFAULT_MISTRAL_BASE_URL, DEFAULT_MISTRAL_MODEL, DEFAULT_MODELSCOPE_BASE_URL,
|
||||
DEFAULT_MODELSCOPE_MODEL, DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL,
|
||||
DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL,
|
||||
DEFAULT_MOONSHOT_BASE_URL, DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL,
|
||||
DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL,
|
||||
DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_CLOUD_BASE_URL, DEFAULT_OLLAMA_CLOUD_MODEL,
|
||||
DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_CODEX_BASE_URL,
|
||||
DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_OPENCODE_GO_BASE_URL,
|
||||
DEFAULT_OPENCODE_GO_MODEL, DEFAULT_OPENCODE_ZEN_BASE_URL, DEFAULT_OPENCODE_ZEN_MODEL,
|
||||
DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL, DEFAULT_OPENROUTER_BASE_URL,
|
||||
DEFAULT_OPENROUTER_MODEL, DEFAULT_ORCAROUTER_BASE_URL, DEFAULT_ORCAROUTER_MODEL,
|
||||
DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL,
|
||||
DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL,
|
||||
DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL,
|
||||
DEFAULT_STEPFUN_MODEL, DEFAULT_TELECOMJS_BASE_URL, DEFAULT_TELECOMJS_MODEL,
|
||||
DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL,
|
||||
DEFAULT_VOLCENGINE_BASE_URL, DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL,
|
||||
DEFAULT_WANJIE_ARK_MODEL, DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL,
|
||||
DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL,
|
||||
DEFAULT_ZAI_MODEL, DEFAULT_ZENMUX_BASE_URL, DEFAULT_ZENMUX_MODEL,
|
||||
MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL, MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL,
|
||||
ProviderKind,
|
||||
};
|
||||
|
||||
/// Wire protocol spoken by a provider.
|
||||
@@ -494,6 +495,12 @@ pub const fn credential_help(kind: ProviderKind) -> CredentialHelp {
|
||||
docs_url: Some("https://zenmux.ai/docs/"),
|
||||
guidance: "Create a ZenMux API key from the Pay As You Go management page, then select models by their provider/model namespaced id. The catalog at https://zenmux.ai/api/v1/models is keyless-readable.",
|
||||
},
|
||||
ProviderKind::Csdn => CredentialHelp {
|
||||
acquisition: ApiKey,
|
||||
credential_url: Some("https://ai.csdn.net/workbench/api-key"),
|
||||
docs_url: Some("https://ai.csdn.net/coding-plan"),
|
||||
guidance: "Create an API key in the CSDN console — choose the Coding Plan key type so glm_for_coding calls bill against plan quota; a general key bills metered.",
|
||||
},
|
||||
ProviderKind::Codewhale => CredentialHelp {
|
||||
acquisition: ApiKey,
|
||||
credential_url: Some(CODEWHALE_API_KEY_URL),
|
||||
@@ -658,6 +665,18 @@ pub fn is_exact_minimax_anthropic_route(kind: ProviderKind, base_url: &str) -> b
|
||||
|| is_exact_https_route(base_url, "api.minimaxi.com", "anthropic"))
|
||||
}
|
||||
|
||||
/// Whether a configured route is exactly CSDN 星图's official OpenAI-compatible
|
||||
/// platform endpoint.
|
||||
///
|
||||
/// Coding Plan keys and general marketplace keys share this one endpoint, so
|
||||
/// the URL proves neither product — only that the route is first-party.
|
||||
/// Neighboring paths, HTTP downgrades, and lookalike hosts must not inherit
|
||||
/// CSDN billing or wire semantics.
|
||||
#[must_use]
|
||||
pub fn is_exact_csdn_platform_route(kind: ProviderKind, base_url: &str) -> bool {
|
||||
kind == ProviderKind::Csdn && is_exact_https_route(base_url, "ai.csdn.net", "api/model/v1")
|
||||
}
|
||||
|
||||
/// Return credential help for one concrete provider route.
|
||||
///
|
||||
/// This protects non-UI callers such as diagnostics and command surfaces from
|
||||
@@ -1568,6 +1587,23 @@ provider!(
|
||||
"zenmux",
|
||||
aliases: ["zen-mux", "zen_mux"]
|
||||
);
|
||||
provider!(
|
||||
Csdn,
|
||||
Csdn,
|
||||
"csdn",
|
||||
"CSDN",
|
||||
DEFAULT_CSDN_BASE_URL,
|
||||
DEFAULT_CSDN_MODEL,
|
||||
["CSDN_API_KEY"],
|
||||
"csdn",
|
||||
aliases: [
|
||||
"csdn-ai",
|
||||
"csdn_ai",
|
||||
"csdn-coding-plan",
|
||||
"csdn_coding_plan",
|
||||
"starmap"
|
||||
]
|
||||
);
|
||||
|
||||
/// Concentrate — OpenAI Responses-compatible AI gateway (aggregator).
|
||||
///
|
||||
@@ -1913,6 +1949,7 @@ static ANTIGRAVITY: Antigravity = Antigravity;
|
||||
static TELECOMJS: Telecomjs = Telecomjs;
|
||||
static EDENAI: Edenai = Edenai;
|
||||
static ZENMUX: Zenmux = Zenmux;
|
||||
static CSDN: Csdn = Csdn;
|
||||
static CONCENTRATE: Concentrate = Concentrate;
|
||||
static CODEWHALE: Codewhale = Codewhale;
|
||||
static MODELSTUDIO_TOKEN_PLAN: ModelstudioTokenPlan = ModelstudioTokenPlan;
|
||||
@@ -1923,7 +1960,7 @@ static MODELSTUDIO_CODING_PLAN_ANTHROPIC: ModelstudioCodingPlanAnthropic =
|
||||
ModelstudioCodingPlanAnthropic;
|
||||
static CUSTOM: Custom = Custom;
|
||||
|
||||
static PROVIDER_REGISTRY: [&dyn Provider; 51] = [
|
||||
static PROVIDER_REGISTRY: [&dyn Provider; 52] = [
|
||||
&DEEPSEEK,
|
||||
&DEEPSEEK_ANTHROPIC,
|
||||
&NVIDIA_NIM,
|
||||
@@ -1966,6 +2003,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 51] = [
|
||||
&TELECOMJS,
|
||||
&EDENAI,
|
||||
&ZENMUX,
|
||||
&CSDN,
|
||||
&CONCENTRATE,
|
||||
&CODEWHALE,
|
||||
&MODELSTUDIO_TOKEN_PLAN,
|
||||
|
||||
@@ -213,6 +213,8 @@ pub(crate) const DEFAULT_ZENMUX_BASE_URL: &str = "https://zenmux.ai/api/v1";
|
||||
// `provider/model` pins a provider and `concentrate/auto` is the gateway router.
|
||||
pub(crate) const DEFAULT_CONCENTRATE_MODEL: &str = "deepseek-v4-pro";
|
||||
pub(crate) const DEFAULT_CONCENTRATE_BASE_URL: &str = "https://api.concentrate.ai/v1";
|
||||
pub(crate) const DEFAULT_CSDN_MODEL: &str = "glm_for_coding";
|
||||
pub(crate) const DEFAULT_CSDN_BASE_URL: &str = "https://ai.csdn.net/api/model/v1";
|
||||
// Codewhale API (account-backed model access) defaults. The account control
|
||||
// plane serves an OpenAI-shaped `GET /v1/models` whose rows carry the wire
|
||||
// protocol per model, so this route is model-aware and its catalog is the
|
||||
|
||||
@@ -233,6 +233,21 @@ pub enum ProviderKind {
|
||||
/// `https://zenmux.ai/api/v1`. The `/models` catalog is keyless-readable.
|
||||
#[serde(alias = "zen-mux", alias = "zen_mux")]
|
||||
Zenmux,
|
||||
/// CSDN 星图 (Starmap) — CSDN's hosted OpenAI-compatible model platform
|
||||
/// and Coding Plan subscription.
|
||||
///
|
||||
/// The plan model id `glm_for_coding` (GLM-5.2 underneath, 200k context
|
||||
/// cap) bills against plan quota and requires the dedicated Coding Plan
|
||||
/// key type; other marketplace model ids and general keys bill metered
|
||||
/// through the same endpoint.
|
||||
#[serde(
|
||||
alias = "csdn-ai",
|
||||
alias = "csdn_ai",
|
||||
alias = "csdn-coding-plan",
|
||||
alias = "csdn_coding_plan",
|
||||
alias = "starmap"
|
||||
)]
|
||||
Csdn,
|
||||
/// Concentrate — OpenAI Responses-compatible AI gateway (aggregator).
|
||||
///
|
||||
/// Serves a broad catalog of upstream models over the OpenAI Responses
|
||||
@@ -281,7 +296,7 @@ impl ProviderKind {
|
||||
/// stay on the enum for serde and `provider_for_kind`, but they are not
|
||||
/// first-class catalog rows. Plan is `mode` / base_url; dialect is
|
||||
/// `wire = openai|anthropic` on the primary provider config.
|
||||
pub const ALL: [Self; 45] = [
|
||||
pub const ALL: [Self; 46] = [
|
||||
Self::Deepseek,
|
||||
Self::NvidiaNim,
|
||||
Self::Openai,
|
||||
@@ -324,6 +339,7 @@ impl ProviderKind {
|
||||
Self::Google,
|
||||
Self::Edenai,
|
||||
Self::Zenmux,
|
||||
Self::Csdn,
|
||||
Self::Concentrate,
|
||||
Self::Codewhale,
|
||||
Self::Custom,
|
||||
|
||||
@@ -6,6 +6,7 @@ arcee
|
||||
atlascloud
|
||||
codewhale
|
||||
concentrate
|
||||
csdn
|
||||
custom
|
||||
deepinfra
|
||||
deepseek
|
||||
|
||||
@@ -122,6 +122,24 @@
|
||||
],
|
||||
"transport": "open-ai-responses"
|
||||
},
|
||||
{
|
||||
"id": "csdn",
|
||||
"family": "csdn",
|
||||
"label": "CSDN",
|
||||
"endpoint": "https://ai.csdn.net/api/model/v1",
|
||||
"wire": "chat-completions",
|
||||
"defaultModel": "glm_for_coding",
|
||||
"envVars": [
|
||||
"CSDN_API_KEY"
|
||||
],
|
||||
"auth": [
|
||||
{
|
||||
"kind": "api-key",
|
||||
"label": "API key"
|
||||
}
|
||||
],
|
||||
"transport": "chat-completions"
|
||||
},
|
||||
{
|
||||
"id": "custom",
|
||||
"family": "custom",
|
||||
|
||||
@@ -5230,6 +5230,78 @@ model = "z-ai/glm-5.3"
|
||||
assert_eq!(resolved.api_key_source, Some(RuntimeApiKeySource::Env));
|
||||
}
|
||||
|
||||
/// CSDN 星图 (Starmap) is an OpenAI-compatible hosted platform whose default
|
||||
/// route is the Coding Plan model `glm_for_coding`: aliases collapse onto one
|
||||
/// catalog identity, the metadata names the official base URL and a distinct
|
||||
/// `CSDN_API_KEY` slot, the wire policy is fixed on Chat Completions, and
|
||||
/// env/config overrides resolve exactly like every other provider table.
|
||||
#[test]
|
||||
fn csdn_resolves_named_chat_provider_and_environment_overrides() {
|
||||
let _lock = env_lock();
|
||||
let _env = EnvGuard::without_deepseek_runtime_overrides();
|
||||
|
||||
for alias in [
|
||||
"csdn",
|
||||
"csdn-ai",
|
||||
"csdn_ai",
|
||||
"csdn-coding-plan",
|
||||
"csdn_coding_plan",
|
||||
"starmap",
|
||||
] {
|
||||
assert_eq!(ProviderKind::parse(alias), Some(ProviderKind::Csdn));
|
||||
let parsed: ConfigToml =
|
||||
toml::from_str(&format!("provider = \"{alias}\"")).expect("CSDN alias");
|
||||
assert_eq!(parsed.provider, ProviderKind::Csdn);
|
||||
}
|
||||
|
||||
let metadata = provider::resolve_provider("starmap").expect("CSDN metadata");
|
||||
assert_eq!(metadata.id(), "csdn");
|
||||
assert_eq!(metadata.display_name(), "CSDN");
|
||||
assert_eq!(metadata.provider_config_key(), "csdn");
|
||||
assert_eq!(metadata.default_base_url(), DEFAULT_CSDN_BASE_URL);
|
||||
assert_eq!(metadata.default_model(), DEFAULT_CSDN_MODEL);
|
||||
assert_eq!(metadata.env_vars(), &["CSDN_API_KEY"]);
|
||||
assert_eq!(
|
||||
metadata.wire_policy(),
|
||||
provider::WirePolicy::Fixed(provider::WireFormat::ChatCompletions)
|
||||
);
|
||||
|
||||
let config: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
provider = "csdn"
|
||||
|
||||
[providers.csdn]
|
||||
api_key = "csdn-config-key"
|
||||
model = "glm_for_coding"
|
||||
"#,
|
||||
)
|
||||
.expect("CSDN provider table");
|
||||
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
|
||||
assert_eq!(resolved.provider, ProviderKind::Csdn);
|
||||
assert_eq!(resolved.base_url, DEFAULT_CSDN_BASE_URL);
|
||||
assert_eq!(resolved.model, "glm_for_coding");
|
||||
assert_eq!(resolved.api_key.as_deref(), Some("csdn-config-key"));
|
||||
assert_eq!(
|
||||
resolved.api_key_source,
|
||||
Some(RuntimeApiKeySource::ConfigFile)
|
||||
);
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("CSDN_API_KEY", "csdn-env-key");
|
||||
std::env::set_var("CSDN_BASE_URL", "https://ai.csdn.net/api/model/v1");
|
||||
std::env::set_var("CSDN_MODEL", "deepseek-v3.2");
|
||||
}
|
||||
let env_config = ConfigToml {
|
||||
provider: ProviderKind::Csdn,
|
||||
..ConfigToml::default()
|
||||
};
|
||||
let resolved = env_config.resolve_runtime_options(&CliRuntimeOverrides::default());
|
||||
assert_eq!(resolved.base_url, "https://ai.csdn.net/api/model/v1");
|
||||
assert_eq!(resolved.model, "deepseek-v3.2");
|
||||
assert_eq!(resolved.api_key.as_deref(), Some("csdn-env-key"));
|
||||
assert_eq!(resolved.api_key_source, Some(RuntimeApiKeySource::Env));
|
||||
}
|
||||
|
||||
/// Concentrate is an opt-in, BYOK, Responses-wire gateway: aliases collapse
|
||||
/// onto one catalog identity, the metadata names the official base URL and a
|
||||
/// distinct `CONCENTRATE_API_KEY` slot, the wire policy is fixed on the
|
||||
|
||||
@@ -1197,6 +1197,7 @@ impl Secrets {
|
||||
/// | `telecomjs` / `tokenhub` | `TELECOMJS_API_KEY` |
|
||||
/// | `edenai` / `eden-ai` | `EDENAI_API_KEY` |
|
||||
/// | `zenmux` / `zen-mux` | `ZENMUX_API_KEY` |
|
||||
/// | `csdn` / `csdn-ai` / `starmap` | `CSDN_API_KEY` |
|
||||
/// | `concentrate` / `concentrate-ai` | `CONCENTRATE_API_KEY` |
|
||||
/// | `codewhale` / `codewhale-api` | `CODEWHALE_API_KEY` |
|
||||
///
|
||||
@@ -1255,6 +1256,9 @@ pub fn env_for(name: &str) -> Option<String> {
|
||||
}
|
||||
"edenai" | "eden-ai" | "eden_ai" => &["EDENAI_API_KEY"],
|
||||
"zenmux" | "zen-mux" | "zen_mux" => &["ZENMUX_API_KEY"],
|
||||
"csdn" | "csdn-ai" | "csdn_ai" | "csdn-coding-plan" | "csdn_coding_plan" | "starmap" => {
|
||||
&["CSDN_API_KEY"]
|
||||
}
|
||||
"concentrate" | "concentrate-ai" | "concentrate_ai" | "concentrateai" => {
|
||||
&["CONCENTRATE_API_KEY"]
|
||||
}
|
||||
@@ -1366,6 +1370,7 @@ mod tests {
|
||||
"TELECOMJS_API_KEY",
|
||||
"EDENAI_API_KEY",
|
||||
"ZENMUX_API_KEY",
|
||||
"CSDN_API_KEY",
|
||||
"CONCENTRATE_API_KEY",
|
||||
"MODELSTUDIO_API_KEY",
|
||||
"DASHSCOPE_API_KEY",
|
||||
@@ -1987,6 +1992,26 @@ mod tests {
|
||||
clear_known_envs();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csdn_env_aliases_resolve() {
|
||||
let _guard = env_lock();
|
||||
clear_known_envs();
|
||||
unsafe { std::env::set_var("CSDN_API_KEY", "csdn-key") };
|
||||
|
||||
for alias in [
|
||||
"csdn",
|
||||
"csdn-ai",
|
||||
"csdn_ai",
|
||||
"csdn-coding-plan",
|
||||
"csdn_coding_plan",
|
||||
"starmap",
|
||||
] {
|
||||
assert_eq!(env_for(alias).as_deref(), Some("csdn-key"), "{alias}");
|
||||
}
|
||||
|
||||
clear_known_envs();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concentrate_env_aliases_resolve() {
|
||||
let _guard = env_lock();
|
||||
|
||||
@@ -4735,6 +4735,9 @@ pub(super) fn apply_reasoning_effort(
|
||||
// provider must not inject a model-specific reasoning dialect.
|
||||
ApiProvider::Edenai => {}
|
||||
ApiProvider::Zenmux => {}
|
||||
// CSDN 星图 is OpenAI-compatible but documents no provider-owned
|
||||
// reasoning dialect; do not invent one.
|
||||
ApiProvider::Csdn => {}
|
||||
// The Codewhale API is a passthrough to the account's own
|
||||
// connected provider; it documents no Codewhale-owned
|
||||
// reasoning-effort translation, so nothing is invented here.
|
||||
@@ -4835,6 +4838,9 @@ pub(super) fn apply_reasoning_effort(
|
||||
ApiProvider::Telecomjs => {}
|
||||
ApiProvider::Edenai => {}
|
||||
ApiProvider::Zenmux => {}
|
||||
// CSDN 星图 is OpenAI-compatible but documents no provider-owned
|
||||
// reasoning dialect; do not invent one.
|
||||
ApiProvider::Csdn => {}
|
||||
// The Codewhale API is a passthrough to the account's own
|
||||
// connected provider; it documents no Codewhale-owned
|
||||
// reasoning-effort translation, so nothing is invented here.
|
||||
@@ -4960,6 +4966,9 @@ pub(super) fn apply_reasoning_effort(
|
||||
ApiProvider::Telecomjs => {}
|
||||
ApiProvider::Edenai => {}
|
||||
ApiProvider::Zenmux => {}
|
||||
// CSDN 星图 is OpenAI-compatible but documents no provider-owned
|
||||
// reasoning dialect; do not invent one.
|
||||
ApiProvider::Csdn => {}
|
||||
// The Codewhale API is a passthrough to the account's own
|
||||
// connected provider; it documents no Codewhale-owned
|
||||
// reasoning-effort translation, so nothing is invented here.
|
||||
|
||||
@@ -124,6 +124,8 @@ pub enum ApiProvider {
|
||||
Edenai,
|
||||
/// ZenMux — OpenAI-compatible AI gateway (aggregator).
|
||||
Zenmux,
|
||||
/// CSDN 星图 — OpenAI-compatible hosted platform and Coding Plan.
|
||||
Csdn,
|
||||
/// Concentrate — OpenAI Responses-compatible AI gateway (aggregator; BYOK only).
|
||||
Concentrate,
|
||||
/// Codewhale API — account-backed model access over connected provider keys.
|
||||
@@ -371,7 +373,7 @@ impl ApiProvider {
|
||||
|
||||
/// `ApiProvider` discriminant → `ProviderKind` lookup.
|
||||
/// Index 1 is `None` for the legacy `DeepseekCN` variant.
|
||||
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 52] = [
|
||||
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 53] = [
|
||||
Some(codewhale_config::ProviderKind::Deepseek),
|
||||
None, // DeepseekCN
|
||||
Some(codewhale_config::ProviderKind::DeepseekAnthropic),
|
||||
@@ -417,6 +419,7 @@ impl ApiProvider {
|
||||
Some(codewhale_config::ProviderKind::Telecomjs),
|
||||
Some(codewhale_config::ProviderKind::Edenai),
|
||||
Some(codewhale_config::ProviderKind::Zenmux),
|
||||
Some(codewhale_config::ProviderKind::Csdn),
|
||||
Some(codewhale_config::ProviderKind::Concentrate),
|
||||
Some(codewhale_config::ProviderKind::Codewhale),
|
||||
Some(codewhale_config::ProviderKind::ModelstudioTokenPlan),
|
||||
@@ -427,7 +430,7 @@ impl ApiProvider {
|
||||
];
|
||||
|
||||
/// `ProviderKind` discriminant → `ApiProvider` lookup.
|
||||
const FROM_KIND_LOOKUP: [Self; 51] = [
|
||||
const FROM_KIND_LOOKUP: [Self; 52] = [
|
||||
Self::Deepseek,
|
||||
Self::DeepseekAnthropic,
|
||||
Self::NvidiaNim,
|
||||
@@ -476,6 +479,7 @@ impl ApiProvider {
|
||||
Self::Google,
|
||||
Self::Edenai,
|
||||
Self::Zenmux,
|
||||
Self::Csdn,
|
||||
Self::Concentrate,
|
||||
Self::Codewhale,
|
||||
Self::Custom,
|
||||
@@ -547,6 +551,10 @@ fn subagent_provider_key_matches(key: &str, provider: ApiProvider) -> bool {
|
||||
ApiProvider::Orcarouter => matches!(normalized.as_str(), "orcarouter" | "orca_router"),
|
||||
ApiProvider::Edenai => matches!(normalized.as_str(), "edenai" | "eden_ai"),
|
||||
ApiProvider::Zenmux => matches!(normalized.as_str(), "zenmux" | "zen_mux"),
|
||||
ApiProvider::Csdn => matches!(
|
||||
normalized.as_str(),
|
||||
"csdn" | "csdn_ai" | "csdn_coding_plan" | "starmap"
|
||||
),
|
||||
ApiProvider::Concentrate => matches!(
|
||||
normalized.as_str(),
|
||||
"concentrate" | "concentrate_ai" | "concentrateai"
|
||||
@@ -1713,6 +1721,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
|
||||
ApiProvider::Antigravity => Vec::new(),
|
||||
ApiProvider::Edenai => vec![DEFAULT_EDENAI_MODEL],
|
||||
ApiProvider::Zenmux => vec![DEFAULT_ZENMUX_MODEL],
|
||||
ApiProvider::Csdn => vec![DEFAULT_CSDN_MODEL],
|
||||
ApiProvider::Concentrate => vec![DEFAULT_CONCENTRATE_MODEL],
|
||||
// Bootstrap rows only. The account's authenticated `GET /v1/models`
|
||||
// lists exactly the providers this customer connected and replaces
|
||||
@@ -3924,6 +3933,16 @@ pub struct ProvidersConfig {
|
||||
/// ZenMux — OpenAI-compatible AI gateway (aggregator).
|
||||
#[serde(default, alias = "zen-mux", alias = "zen_mux")]
|
||||
pub zenmux: ProviderConfig,
|
||||
/// CSDN 星图 — OpenAI-compatible hosted platform and Coding Plan.
|
||||
#[serde(
|
||||
default,
|
||||
alias = "csdn-ai",
|
||||
alias = "csdn_ai",
|
||||
alias = "csdn-coding-plan",
|
||||
alias = "csdn_coding_plan",
|
||||
alias = "starmap"
|
||||
)]
|
||||
pub csdn: ProviderConfig,
|
||||
/// Concentrate — OpenAI Responses-compatible AI gateway (aggregator).
|
||||
#[serde(
|
||||
default,
|
||||
@@ -5823,6 +5842,7 @@ impl Config {
|
||||
ApiProvider::Telecomjs => &providers.telecomjs,
|
||||
ApiProvider::Edenai => &providers.edenai,
|
||||
ApiProvider::Zenmux => &providers.zenmux,
|
||||
ApiProvider::Csdn => &providers.csdn,
|
||||
ApiProvider::Concentrate => &providers.concentrate,
|
||||
ApiProvider::Codewhale => &providers.codewhale,
|
||||
ApiProvider::ModelstudioTokenPlan => &providers.modelstudio_token_plan,
|
||||
@@ -5926,6 +5946,7 @@ impl Config {
|
||||
ApiProvider::Telecomjs => &mut providers.telecomjs,
|
||||
ApiProvider::Edenai => &mut providers.edenai,
|
||||
ApiProvider::Zenmux => &mut providers.zenmux,
|
||||
ApiProvider::Csdn => &mut providers.csdn,
|
||||
ApiProvider::Concentrate => &mut providers.concentrate,
|
||||
ApiProvider::Codewhale => &mut providers.codewhale,
|
||||
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
|
||||
@@ -6401,6 +6422,7 @@ impl Config {
|
||||
ApiProvider::Telecomjs => DEFAULT_TELECOMJS_MODEL,
|
||||
ApiProvider::Edenai => DEFAULT_EDENAI_MODEL,
|
||||
ApiProvider::Zenmux => DEFAULT_ZENMUX_MODEL,
|
||||
ApiProvider::Csdn => DEFAULT_CSDN_MODEL,
|
||||
ApiProvider::Concentrate => DEFAULT_CONCENTRATE_MODEL,
|
||||
ApiProvider::Codewhale => DEFAULT_CODEWHALE_MODEL,
|
||||
ApiProvider::ModelstudioTokenPlan
|
||||
@@ -6528,6 +6550,7 @@ impl Config {
|
||||
| ApiProvider::Telecomjs
|
||||
| ApiProvider::Edenai
|
||||
| ApiProvider::Zenmux
|
||||
| ApiProvider::Csdn
|
||||
| ApiProvider::Concentrate
|
||||
| ApiProvider::Codewhale
|
||||
| ApiProvider::ModelstudioTokenPlan
|
||||
@@ -6642,6 +6665,7 @@ impl Config {
|
||||
ApiProvider::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
|
||||
ApiProvider::Edenai => DEFAULT_EDENAI_BASE_URL,
|
||||
ApiProvider::Zenmux => DEFAULT_ZENMUX_BASE_URL,
|
||||
ApiProvider::Csdn => DEFAULT_CSDN_BASE_URL,
|
||||
ApiProvider::Concentrate => DEFAULT_CONCENTRATE_BASE_URL,
|
||||
ApiProvider::Codewhale => DEFAULT_CODEWHALE_BASE_URL,
|
||||
ApiProvider::ModelstudioTokenPlan
|
||||
@@ -8551,6 +8575,7 @@ fn provider_env_base_url_override(provider: ApiProvider) -> Option<String> {
|
||||
ApiProvider::Telecomjs => &["TELECOMJS_BASE_URL"],
|
||||
ApiProvider::Edenai => &["EDENAI_BASE_URL"],
|
||||
ApiProvider::Zenmux => &["ZENMUX_BASE_URL"],
|
||||
ApiProvider::Csdn => &["CSDN_BASE_URL"],
|
||||
ApiProvider::Concentrate => &["CONCENTRATE_BASE_URL"],
|
||||
ApiProvider::Codewhale => &["CODEWHALE_API_BASE"],
|
||||
ApiProvider::ModelstudioTokenPlan | ApiProvider::ModelstudioTokenPlanAnthropic => {
|
||||
@@ -8951,6 +8976,13 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
|
||||
.zenmux
|
||||
.base_url = Some(value);
|
||||
}
|
||||
ApiProvider::Csdn => {
|
||||
config
|
||||
.providers
|
||||
.get_or_insert_with(ProvidersConfig::default)
|
||||
.csdn
|
||||
.base_url = Some(value);
|
||||
}
|
||||
ApiProvider::Concentrate => {
|
||||
config
|
||||
.providers
|
||||
@@ -9242,6 +9274,16 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
|
||||
.zenmux
|
||||
.base_url = Some(value);
|
||||
}
|
||||
if matches!(config.api_provider(), ApiProvider::Csdn)
|
||||
&& let Ok(value) = std::env::var("CSDN_BASE_URL")
|
||||
&& !value.trim().is_empty()
|
||||
{
|
||||
config
|
||||
.providers
|
||||
.get_or_insert_with(ProvidersConfig::default)
|
||||
.csdn
|
||||
.base_url = Some(value);
|
||||
}
|
||||
// Concentrate has no inline block here on purpose: CONCENTRATE_BASE_URL
|
||||
// is already served by `provider_env_base_url_override`, which the route
|
||||
// resolver consults — a second inline assignment was a duplicate.
|
||||
@@ -9359,6 +9401,7 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
|
||||
ApiProvider::Telecomjs => &mut providers.telecomjs,
|
||||
ApiProvider::Edenai => &mut providers.edenai,
|
||||
ApiProvider::Zenmux => &mut providers.zenmux,
|
||||
ApiProvider::Csdn => &mut providers.csdn,
|
||||
ApiProvider::Concentrate => &mut providers.concentrate,
|
||||
ApiProvider::Codewhale => &mut providers.codewhale,
|
||||
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
|
||||
@@ -9661,6 +9704,17 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
|
||||
.model = Some(value);
|
||||
config.environment_model_applied = true;
|
||||
}
|
||||
if matches!(config.api_provider(), ApiProvider::Csdn)
|
||||
&& let Ok(value) = std::env::var("CSDN_MODEL")
|
||||
&& !value.trim().is_empty()
|
||||
{
|
||||
config
|
||||
.providers
|
||||
.get_or_insert_with(ProvidersConfig::default)
|
||||
.csdn
|
||||
.model = Some(value);
|
||||
config.environment_model_applied = true;
|
||||
}
|
||||
if matches!(
|
||||
config.api_provider(),
|
||||
ApiProvider::ModelstudioTokenPlan | ApiProvider::ModelstudioTokenPlanAnthropic
|
||||
@@ -10096,6 +10150,7 @@ pub(crate) fn provider_passes_model_through(provider: ApiProvider) -> bool {
|
||||
| ApiProvider::Telecomjs
|
||||
| ApiProvider::Edenai
|
||||
| ApiProvider::Zenmux
|
||||
| ApiProvider::Csdn
|
||||
// Concentrate ids are gateway-owned (plain, `provider/model`, or the
|
||||
// gateway's own `auto`); the resolver strips only `concentrate/`.
|
||||
| ApiProvider::Concentrate
|
||||
@@ -11380,6 +11435,7 @@ fn merge_providers(
|
||||
telecomjs: merge_provider_config(base.telecomjs, override_cfg.telecomjs),
|
||||
edenai: merge_provider_config(base.edenai, override_cfg.edenai),
|
||||
zenmux: merge_provider_config(base.zenmux, override_cfg.zenmux),
|
||||
csdn: merge_provider_config(base.csdn, override_cfg.csdn),
|
||||
concentrate: merge_provider_config(base.concentrate, override_cfg.concentrate),
|
||||
codewhale: merge_provider_config(base.codewhale, override_cfg.codewhale),
|
||||
modelstudio_token_plan: merge_provider_config(
|
||||
|
||||
@@ -273,6 +273,8 @@ pub const DEFAULT_EDENAI_MODEL: &str = "deepseek/deepseek-v4-pro";
|
||||
pub const DEFAULT_EDENAI_BASE_URL: &str = "https://api.edenai.run/v3";
|
||||
pub const DEFAULT_ZENMUX_MODEL: &str = "deepseek/deepseek-v4.1-flash";
|
||||
pub const DEFAULT_ZENMUX_BASE_URL: &str = "https://zenmux.ai/api/v1";
|
||||
pub const DEFAULT_CSDN_MODEL: &str = "glm_for_coding";
|
||||
pub const DEFAULT_CSDN_BASE_URL: &str = "https://ai.csdn.net/api/model/v1";
|
||||
// Concentrate (OpenAI Responses-compatible AI gateway) defaults; mirrors
|
||||
// crates/config/src/provider_defaults.rs for the docs drift check.
|
||||
pub const DEFAULT_CONCENTRATE_MODEL: &str = "deepseek-v4-pro";
|
||||
|
||||
@@ -617,6 +617,7 @@ fn provider_base_url_table_key(provider: ApiProvider) -> anyhow::Result<&'static
|
||||
ApiProvider::Telecomjs => Ok("telecomjs"),
|
||||
ApiProvider::Edenai => Ok("edenai"),
|
||||
ApiProvider::Zenmux => Ok("zenmux"),
|
||||
ApiProvider::Csdn => Ok("csdn"),
|
||||
ApiProvider::Concentrate => Ok("concentrate"),
|
||||
ApiProvider::Codewhale => Ok("codewhale"),
|
||||
ApiProvider::ModelstudioTokenPlan => Ok("modelstudio_token_plan"),
|
||||
|
||||
@@ -267,6 +267,10 @@ pub(crate) const AGGREGATOR_BILLING_SURFACE: &str = "aggregator-payg";
|
||||
pub(crate) const MODELSTUDIO_TOKEN_PLAN_BILLING_SURFACE: &str = "modelstudio-token-plan";
|
||||
pub(crate) const MODELSTUDIO_CODING_PLAN_BILLING_SURFACE: &str = "modelstudio-coding-plan";
|
||||
pub(crate) const VOLCENGINE_CODING_PLAN_BILLING_SURFACE: &str = "volcengine-coding-plan";
|
||||
/// CSDN 星图's Coding Plan subscription product (the `glm_for_coding` route).
|
||||
pub(crate) const CSDN_CODING_PLAN_BILLING_SURFACE: &str = "csdn-coding-plan";
|
||||
/// CSDN 星图's ordinary metered marketplace access on the same endpoint.
|
||||
pub(crate) const CSDN_PAYG_BILLING_SURFACE: &str = "csdn-payg";
|
||||
/// A reachable endpoint CodeWhale could not match to any known billing surface.
|
||||
/// Distinct from "not classified yet": this is a positive statement that the
|
||||
/// surface is unknown, and it fails closed everywhere it is consumed.
|
||||
@@ -308,6 +312,7 @@ pub fn endpoint_metering_for_billing_surface(billing_surface: Option<&str>) -> E
|
||||
(MOONSHOT_PAYG_BILLING_SURFACE, EndpointMetering::Money),
|
||||
(MINIMAX_PAYG_BILLING_SURFACE, EndpointMetering::Money),
|
||||
(XIAOMI_PAYG_BILLING_SURFACE, EndpointMetering::Money),
|
||||
(CSDN_PAYG_BILLING_SURFACE, EndpointMetering::Money),
|
||||
(FIRST_PARTY_PAYG_BILLING_SURFACE, EndpointMetering::Money),
|
||||
(AGGREGATOR_BILLING_SURFACE, EndpointMetering::Money),
|
||||
(
|
||||
@@ -342,6 +347,10 @@ pub fn endpoint_metering_for_billing_surface(billing_surface: Option<&str>) -> E
|
||||
XIAOMI_TOKEN_PLAN_BILLING_SURFACE,
|
||||
EndpointMetering::ExactSubscription,
|
||||
),
|
||||
(
|
||||
CSDN_CODING_PLAN_BILLING_SURFACE,
|
||||
EndpointMetering::ExactSubscription,
|
||||
),
|
||||
(
|
||||
OAUTH_SUBSCRIPTION_BILLING_SURFACE,
|
||||
EndpointMetering::ExactSubscription,
|
||||
@@ -430,6 +439,7 @@ pub(crate) fn billing_surface_for_route(
|
||||
ApiProvider::Zai => zai_surface(&shape),
|
||||
ApiProvider::Moonshot => moonshot_surface(&shape),
|
||||
ApiProvider::Minimax | ApiProvider::MinimaxAnthropic => minimax_surface(&shape),
|
||||
ApiProvider::Csdn => csdn_surface(&shape),
|
||||
ApiProvider::XiaomiMimo => xiaomi_surface(&shape),
|
||||
ApiProvider::ModelstudioTokenPlan
|
||||
| ApiProvider::ModelstudioTokenPlanAnthropic
|
||||
@@ -541,6 +551,15 @@ fn minimax_surface(shape: &EndpointShape) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn csdn_surface(shape: &EndpointShape) -> Option<&'static str> {
|
||||
// Coding Plan keys and general marketplace keys share the one
|
||||
// ai.csdn.net/api/model/v1 endpoint, so the URL proves neither product;
|
||||
// only the captured credential product can produce a concrete surface.
|
||||
let _is_supported_endpoint = shape.host == "ai.csdn.net"
|
||||
&& matches!(shape.path.as_str(), "/api/model" | "/api/model/v1");
|
||||
None
|
||||
}
|
||||
|
||||
fn xiaomi_surface(shape: &EndpointShape) -> Option<&'static str> {
|
||||
if matches!(
|
||||
shape.host.as_str(),
|
||||
|
||||
@@ -120,7 +120,7 @@ fn shipped_default_routes_have_reviewed_pricing_coverage() {
|
||||
|
||||
// A deliberate breadth receipt, including the generic Custom placeholder.
|
||||
// Inventory still comes from production owners, not these expected counts.
|
||||
assert_eq!(built_in_count, 50, "review changed shipped-default breadth");
|
||||
assert_eq!(built_in_count, 51, "review changed shipped-default breadth");
|
||||
assert_eq!(
|
||||
defaults.len() - built_in_count,
|
||||
6,
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"reason": "not_money_metered",
|
||||
"review": "Exact shipped plan/OAuth/local surfaces are excluded from money coverage before model-price lookup (route_billing.rs:345,470; pricing.rs:398; cost_status.rs:235). Retain exact dialect and plan endpoint distinctions; the same model on PAYG is a different tuple.",
|
||||
"routes": [
|
||||
["csdn", "glm_for_coding", "https://ai.csdn.net/api/model/v1", "chat_completions", "subscription"],
|
||||
["modelstudio-coding-plan", "qwen3.8-max", "https://coding-intl.dashscope.aliyuncs.com/v1", "chat_completions", "subscription"],
|
||||
["modelstudio-coding-plan-anthropic", "qwen3.8-max", "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic", "anthropic_messages", "subscription"],
|
||||
["modelstudio-token-plan", "qwen3.8-max", "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", "chat_completions", "subscription"],
|
||||
|
||||
@@ -260,6 +260,11 @@ pub fn capture_product(config: &Config, provider: ApiProvider) -> RouteProduct {
|
||||
CredentialProduct::Unprovable => RouteProduct::Unproven,
|
||||
}
|
||||
}
|
||||
ApiProvider::Csdn => match csdn_credential_product(provider_config) {
|
||||
CredentialProduct::Plan => RouteProduct::Subscription("CSDN Coding Plan quota"),
|
||||
CredentialProduct::PayAsYouGo => RouteProduct::Metered,
|
||||
CredentialProduct::Unprovable => RouteProduct::Unproven,
|
||||
},
|
||||
ApiProvider::XiaomiMimo => {
|
||||
if xiaomi_is_explicit_pay_as_you_go(provider_config) {
|
||||
RouteProduct::Metered
|
||||
@@ -405,6 +410,20 @@ fn classify(
|
||||
BillingPresentation::Unknown
|
||||
}
|
||||
ApiProvider::Minimax | ApiProvider::MinimaxAnthropic => product_billing(product),
|
||||
// CSDN 星图 sells the Coding Plan (`glm_for_coding`) and metered
|
||||
// marketplace models over the same endpoint; the URL proves only that
|
||||
// the route is first-party, so the captured product decides. Anything
|
||||
// off the supported direct endpoint is Unknown no matter what was
|
||||
// captured.
|
||||
ApiProvider::Csdn
|
||||
if !codewhale_config::provider::is_exact_csdn_platform_route(
|
||||
codewhale_config::ProviderKind::Csdn,
|
||||
base_url,
|
||||
) =>
|
||||
{
|
||||
BillingPresentation::Unknown
|
||||
}
|
||||
ApiProvider::Csdn => product_billing(product),
|
||||
ApiProvider::Xai | ApiProvider::Anthropic => product_billing(product),
|
||||
// A named custom route is billed from the identity and endpoint it
|
||||
// dispatched on. Without an identity there is no vendor to name, and
|
||||
@@ -533,6 +552,7 @@ pub fn billing_surface_for_dispatch(
|
||||
ApiProvider::Minimax | ApiProvider::MinimaxAnthropic => {
|
||||
crate::pricing::MINIMAX_TOKEN_PLAN_BILLING_SURFACE
|
||||
}
|
||||
ApiProvider::Csdn => crate::pricing::CSDN_CODING_PLAN_BILLING_SURFACE,
|
||||
ApiProvider::OpenaiCodex
|
||||
| ApiProvider::OpencodeGo
|
||||
| ApiProvider::Anthropic
|
||||
@@ -549,6 +569,9 @@ pub fn billing_surface_for_dispatch(
|
||||
{
|
||||
return Some(crate::pricing::MINIMAX_PAYG_BILLING_SURFACE);
|
||||
}
|
||||
BillingPresentation::Metered if provider == ApiProvider::Csdn => {
|
||||
return Some(crate::pricing::CSDN_PAYG_BILLING_SURFACE);
|
||||
}
|
||||
BillingPresentation::Local => return Some(crate::pricing::LOCAL_BILLING_SURFACE),
|
||||
BillingPresentation::Unknown | BillingPresentation::Metered => {}
|
||||
}
|
||||
@@ -998,6 +1021,42 @@ fn minimax_credential_product(
|
||||
}
|
||||
}
|
||||
|
||||
/// CSDN 星图 sells the Coding Plan and metered marketplace models over the
|
||||
/// same `ai.csdn.net/api/model/v1` endpoint and `CSDN_API_KEY` slot, so the
|
||||
/// product comes from an explicit saved pay mode or the routed model:
|
||||
/// `glm_for_coding` is the Coding Plan's dedicated model id — the route the
|
||||
/// plan sells — while every other model on the platform endpoint is ordinary
|
||||
/// metered marketplace access. An explicit operator-set mode wins over the
|
||||
/// model in both directions; an unrecognized mode is not a product claim.
|
||||
fn csdn_credential_product(provider_config: Option<&ProviderConfig>) -> CredentialProduct {
|
||||
if let Some(mode) = provider_config
|
||||
.and_then(|config| config.mode.as_deref())
|
||||
.filter(|mode| !mode.trim().is_empty())
|
||||
.map(normalized)
|
||||
{
|
||||
return match mode.as_str() {
|
||||
"coding_plan" | "codingplan" | "plan" | "subscription" | "subscription_plan" => {
|
||||
CredentialProduct::Plan
|
||||
}
|
||||
"pay_as_you_go" | "payg" | "paygo" | "pay_as_go" | "metered" | "standard" | "api"
|
||||
| "api_key" | "default" => CredentialProduct::PayAsYouGo,
|
||||
_ => CredentialProduct::Unprovable,
|
||||
};
|
||||
}
|
||||
// No table at all still resolves to the plan model: `glm_for_coding` is
|
||||
// the shipped default for the `csdn` route.
|
||||
let model = provider_config
|
||||
.and_then(|entry| entry.model.as_deref())
|
||||
.map(str::trim)
|
||||
.filter(|model| !model.is_empty())
|
||||
.unwrap_or(crate::config::DEFAULT_CSDN_MODEL);
|
||||
if model.eq_ignore_ascii_case(crate::config::DEFAULT_CSDN_MODEL) {
|
||||
CredentialProduct::Plan
|
||||
} else {
|
||||
CredentialProduct::PayAsYouGo
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a MiniMax credential is visible in non-secret-store provenance,
|
||||
/// and if so whether it carries the Token Plan (`sk-cp…`) product prefix.
|
||||
///
|
||||
@@ -3080,6 +3139,10 @@ mod tests {
|
||||
(ApiProvider::Google, BillingPresentation::Metered),
|
||||
(ApiProvider::Edenai, BillingPresentation::Metered),
|
||||
(ApiProvider::Zenmux, BillingPresentation::Metered),
|
||||
(
|
||||
ApiProvider::Csdn,
|
||||
BillingPresentation::Subscription("CSDN Coding Plan quota"),
|
||||
),
|
||||
(ApiProvider::Concentrate, BillingPresentation::Metered),
|
||||
(ApiProvider::Codewhale, BillingPresentation::Metered),
|
||||
(ApiProvider::Custom, BillingPresentation::Unknown),
|
||||
@@ -3118,7 +3181,7 @@ mod tests {
|
||||
}
|
||||
assert_eq!(
|
||||
DEFAULT_ROUTE_BILLING_AUDIT.len(),
|
||||
51,
|
||||
52,
|
||||
"the audit covers every provider identity, primary and alternate"
|
||||
);
|
||||
|
||||
|
||||
@@ -987,6 +987,7 @@ pub(crate) fn mirror_saved_api_key_in_config(
|
||||
ApiProvider::Telecomjs => &mut providers.telecomjs,
|
||||
ApiProvider::Edenai => &mut providers.edenai,
|
||||
ApiProvider::Zenmux => &mut providers.zenmux,
|
||||
ApiProvider::Csdn => &mut providers.csdn,
|
||||
ApiProvider::Concentrate => &mut providers.concentrate,
|
||||
ApiProvider::Codewhale => &mut providers.codewhale,
|
||||
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
|
||||
|
||||
+4
-1
@@ -128,7 +128,7 @@ The canonical provider IDs are the 44 entries of `ProviderKind::ALL`
|
||||
`together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`,
|
||||
`stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`,
|
||||
`opencode-zen`, `meta`, `xai`, `mistral`, `telecomjs`, `modelstudio-token-plan`, `modelscope`,
|
||||
`google`, `edenai`, `zenmux`, `concentrate`, `codewhale`, and `custom`.
|
||||
`google`, `edenai`, `zenmux`, `csdn`, `concentrate`, `codewhale`, and `custom`.
|
||||
|
||||
`deepseek-anthropic` is *not* on this list — it is a wire dialect of
|
||||
`deepseek`, reached with `wire = "anthropic"`, not a separate route to select.
|
||||
@@ -234,6 +234,7 @@ the listed provider env vars.
|
||||
| `google` | `[providers.google]` | OpenAI Chat Completions (official Gemini OpenAI-compat route; captures and replays thought signatures on tool calls) | `GOOGLE_API_KEY`, `GEMINI_API_KEY` |
|
||||
| `edenai` | `[providers.edenai]` | OpenAI Chat Completions | `EDENAI_API_KEY` |
|
||||
| `zenmux` | `[providers.zenmux]` | OpenAI Chat Completions | `ZENMUX_API_KEY` |
|
||||
| `csdn` | `[providers.csdn]` | OpenAI Chat Completions | `CSDN_API_KEY` |
|
||||
| `concentrate` | `[providers.concentrate]` | OpenAI Responses (`/v1/responses`) | `CONCENTRATE_API_KEY` |
|
||||
| `codewhale` | `[providers.codewhale]` | Model-aware: OpenAI Chat Completions (`/v1/chat/completions`) or Anthropic Messages (`/v1/messages`), chosen per model by the account catalog | `CODEWHALE_API_KEY` |
|
||||
| `modelstudio-token-plan` | `[providers.modelstudio_token_plan]` | OpenAI Chat Completions | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
|
||||
@@ -595,6 +596,7 @@ configuration path instead of guessing a vendor page.
|
||||
| `google` | [Google AI Studio](https://aistudio.google.com/apikey) — Codewhale uses the official Gemini OpenAI-compatible endpoint and never reads Google OAuth files. |
|
||||
| `edenai` | [Eden AI API keys](https://app.edenai.run/settings/api-keys) |
|
||||
| `zenmux` | [ZenMux API keys](https://zenmux.ai/platform/pay-as-you-go) |
|
||||
| `csdn` | [CSDN 星图 console](https://ai.csdn.net/workbench/api-key) — choose the Coding Plan key type for the `glm_for_coding` plan route; a general key bills metered. Docs: [Coding Plan](https://ai.csdn.net/coding-plan). |
|
||||
| `concentrate` | [Concentrate dashboard](https://concentrate.ai/) → API Keys → Create API Key (a Universal API key); docs: [API introduction](https://concentrate.ai/docs/api-reference/introduction). BYOK only — the key stays in the local secret store. |
|
||||
| `modelstudio-token-plan`, `modelstudio-token-plan-anthropic`, `modelstudio-coding-plan`, `modelstudio-coding-plan-anthropic` | [Alibaba Cloud Model Studio (Bailian console)](https://bailian.console.aliyun.com/) — create or copy a Model Studio API key. |
|
||||
| `codewhale` | [Codewhale account settings](https://app.codewhale.net/settings?section=api) — create an API key with the `models:infer` scope, or run `codewhale account api-keys create --name <name> --use`. |
|
||||
@@ -701,6 +703,7 @@ overlay and lets DSH resolve its own keys.
|
||||
| `mistral` | `[providers.mistral]` | `MISTRAL_API_KEY` | `MISTRAL_BASE_URL`; default `https://api.mistral.ai/v1` | `mistral-code-latest` (default; `codestral-latest` accepted as alias), `mistral-medium-latest` (aliases: `mistral-medium-3-5`), `mistral-small-latest` (aliases: `mistral-small-2603`), `mistral-large-latest` | Mistral AI (la Plateforme) OpenAI-compatible Chat route. On the documented first-party HTTPS `/v1` hosts, Medium and Small send adjustable `reasoning_effort` (`none` or `high` only), parse Mistral's polymorphic thinking/text blocks, and replay stored thinking in that same wire shape. Deprecated native Magistral IDs remain explicit-configuration compatibility routes: they are always-reasoning and never receive the adjustable effort field. Code and Large are non-reasoning. A custom `MISTRAL_BASE_URL` keeps generic Chat semantics unless it is one of the documented first-party hosts. `MISTRAL_MODEL` is accepted. Provider aliases: `mistral-ai`, `mistralai`, `la-plateforme`. |
|
||||
| `edenai` | `[providers.edenai]` | `EDENAI_API_KEY` | `EDENAI_BASE_URL`; default `https://api.edenai.run/v3`; EU `https://api.eu.edenai.run/v3` | `deepseek/deepseek-v4-pro` (default); live `/models` catalog of `provider/model` ids | Eden AI OpenAI-compatible aggregation gateway. Catalog rows remain provider-scoped; generic reasoning controls are omitted because supported fields depend on the selected upstream family. `EDENAI_MODEL` is accepted. The default `deepseek/deepseek-v4-pro` is listed on the global catalog only; on the EU endpoint set `EDENAI_MODEL` (or `model`) to a row from the EU `/models` list, for example `qwen/deepseek-v4-pro`. Provider aliases: `eden-ai`, `eden_ai`. |
|
||||
| `zenmux` | `[providers.zenmux]` | `ZENMUX_API_KEY` | `ZENMUX_BASE_URL`; default `https://zenmux.ai/api/v1` | `deepseek/deepseek-v4.1-flash` (default); live `/models` catalog of `provider/model` ids (keyless-readable) | ZenMux OpenAI-compatible aggregation gateway (~190 models). Catalog rows remain provider-scoped; generic reasoning controls are omitted because supported fields depend on the selected upstream family. `ZENMUX_MODEL` is accepted. Provider aliases: `zen-mux`, `zen_mux`. |
|
||||
| `csdn` | `[providers.csdn]` | `CSDN_API_KEY` | `CSDN_BASE_URL`; default `https://ai.csdn.net/api/model/v1` | `glm_for_coding` (default; the Coding Plan's dedicated model id); other marketplace model ids pass through | CSDN 星图 (Starmap) OpenAI-compatible hosted platform. Coding Plan keys and general marketplace keys share the one endpoint, so billing follows the credential product, never the URL alone: routing `glm_for_coding` — the shipped default — or setting `mode = "coding_plan"`/`"plan"`/`"subscription"` bills as CSDN Coding Plan quota with no dollar estimates; any other model, or an explicit `pay-as-you-go`/`metered` mode, bills metered; an unrecognized mode or an endpoint off `ai.csdn.net/api/model/v1` reports `cost: unknown`. `CSDN_MODEL` is accepted. Provider aliases: `csdn-ai`, `csdn_ai`, `csdn-coding-plan`, `csdn_coding_plan`, `starmap`. |
|
||||
| `concentrate` | `[providers.concentrate]` | `CONCENTRATE_API_KEY` | `CONCENTRATE_BASE_URL`; default `https://api.concentrate.ai/v1` | `deepseek-v4-pro` (default; a plain catalog id lets the gateway pick the upstream provider); `provider/model` ids such as `openai/gpt-5.6-sol` pin one upstream; `concentrate/auto` is the gateway's own router; unauthenticated live `/v1/models` catalog | Concentrate OpenAI Responses-compatible gateway (`POST /v1/responses`, bearer Universal API key). Opt-in and BYOK only: your key, your Concentrate bill, zero Codewhale fee, no managed default. Requests carry only documented fields (the system prompt rides as a `system` input item). Contract: [API introduction](https://concentrate.ai/docs/api-reference/introduction), [request parameters](https://concentrate.ai/docs/api-reference/endpoint/request-parameters), [streaming](https://concentrate.ai/docs/api-reference/endpoint/streaming), [errors](https://concentrate.ai/docs/api-reference/endpoint/errors). See [Concentrate Notes](#concentrate-notes). |
|
||||
| `codewhale` | `[providers.codewhale]` | `CODEWHALE_API_KEY` | `CODEWHALE_API_BASE`; default `https://api.codewhale.net/v1` | `deepseek/deepseek-v4-pro` (default), `anthropic/claude-sonnet-5`, `openai/gpt-5.6` are offline bootstrap rows only; the authenticated `GET /v1/models` listing of the account's connected providers is the catalog authority | Codewhale API: account-backed model access over the provider keys the customer connected to their Codewhale account. One `cwc_key_…` account API key with the `models:infer` scope authenticates every model. Model ids are `provider/model` exactly as the account catalog returns them, and each row states its protocol (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`); every protocol uses `Authorization: Bearer`, never `x-api-key`. `CODEWHALE_API_BASE` must be HTTPS except on loopback. Connect provider keys with `codewhale account keys set <provider>`. Provider aliases: `codewhale-api`, `cw-api`, `codewhale-cloud`. |
|
||||
| `xai` | `[providers.xai]` | `XAI_API_KEY`, Codewhale-owned device OAuth, or explicit read-only Grok CLI consent | `XAI_BASE_URL`; default `https://api.x.ai/v1` | `grok-4.6` (default), `grok-4.5`, `grok-4.3`, `grok-build`, `grok-composer-2.5-fast`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning` | xAI/Grok OpenAI-compatible Chat Completions route. Grok 4.6 has a 500K context window, text/image input, function calls, structured output, server-side web search, and `low`/`medium`/`high`/`xhigh` reasoning (default `high`). Its standard rates double when the prompt reaches 200K tokens; the same 2x long-context rule applies to `grok-4.5` (500K context, $2.00 / $0.30 cached / $6.00) and `grok-4.3` (1M context, $1.25 / $0.20 cached / $2.50) per their [model pages](https://docs.x.ai/docs/models/grok-4.5). There is no documented `latest`/`fast` alias and no published numeric output limit. **API-key** (default): Bearer token from console.x.ai via `XAI_API_KEY` / keyring / `api_key`. **OAuth**: `codewhale auth xai-device` uses SSH-friendly device login and Codewhale-owned storage, which may refresh itself. Existing Grok CLI credentials require `codewhale auth external-consent --provider xai --mode read-only`; the granted external file is never refreshed or rewritten. OAuth may return HTTP 403 on some SuperGrok tiers — keep API-key as the reliable fallback. `XAI_MODEL` is accepted. Provider aliases: `x-ai`, `x_ai`, `grok`. |
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"sourceCandidate": {
|
||||
"version": "0.9.14",
|
||||
"providerCount": 49,
|
||||
"providerCount": 50,
|
||||
"toolCount": 80,
|
||||
"sandboxBackends": [
|
||||
"seatbelt (macOS, when available)",
|
||||
|
||||
@@ -26,7 +26,7 @@ DeepSeek 仍是默认提供商,但 `ProviderKind::ALL` 中的每个条目都
|
||||
`together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`,
|
||||
`stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`,
|
||||
`opencode-zen`, `meta`, `xai`, `mistral`, `telecomjs`, `modelstudio-token-plan`, `modelscope`,
|
||||
`google`, `antigravity`, `edenai`, `zenmux`, 和 `custom`。
|
||||
`google`, `antigravity`, `edenai`, `zenmux`, `csdn`, 和 `custom`。
|
||||
|
||||
`deepseek-anthropic` *不在*此列表中——它是 `deepseek` 的线协议方言,通过 `wire = "anthropic"` 访问,而不是一个可单独选择的路由。
|
||||
|
||||
@@ -97,6 +97,7 @@ DeepSeek 仍是默认提供商,但 `ProviderKind::ALL` 中的每个条目都
|
||||
| `antigravity` | `[providers.antigravity]` | 无——请求默认失败关闭;仅凭据导入 | `ANTIGRAVITY_API_KEY`(密钥平面);`AGY_ADC_AUTH`(进程环境变量) |
|
||||
| `edenai` | `[providers.edenai]` | OpenAI Chat Completions | `EDENAI_API_KEY` |
|
||||
| `zenmux` | `[providers.zenmux]` | OpenAI Chat Completions | `ZENMUX_API_KEY` |
|
||||
| `csdn` | `[providers.csdn]` | OpenAI Chat Completions | `CSDN_API_KEY` |
|
||||
| `modelstudio-token-plan` | `[providers.modelstudio_token_plan]` | OpenAI Chat Completions | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
|
||||
| `modelstudio-token-plan-anthropic` | `[providers.modelstudio_token_plan_anthropic]` | Anthropic Messages | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
|
||||
| `modelstudio-coding-plan` | `[providers.modelstudio_coding_plan]` | OpenAI Chat Completions | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |
|
||||
@@ -348,6 +349,7 @@ model = "qwen3:8b" # 默认是 deepseek-v4-flash
|
||||
| `antigravity` | 用官方 `agy` CLI(1.1.13)登录。在 `codewhale auth external-consent` 之后,Codewhale 可以只读地读取该登录的精确定点 `state.vscdb` 中的 OAuth token;它从不写入或刷新该文件。`ANTIGRAVITY_API_KEY` 或进程的 `AGY_ADC_AUTH` 优先于该文件。云代码线协议未实现:请求失败关闭并给出可操作的消息——Gemini 模型请使用 `google`。 |
|
||||
| `edenai` | [Eden AI API 密钥](https://app.edenai.run/settings/api-keys) |
|
||||
| `zenmux` | [ZenMux API 密钥](https://zenmux.ai/platform/pay-as-you-go) |
|
||||
| `csdn` | [CSDN 星图控制台](https://ai.csdn.net/workbench/api-key)——`glm_for_coding` 套餐路由请选择 Coding Plan 密钥类型;通用密钥按量计费。文档:[Coding Plan](https://ai.csdn.net/coding-plan)。 |
|
||||
| `modelstudio-token-plan`, `modelstudio-token-plan-anthropic`, `modelstudio-coding-plan`, `modelstudio-coding-plan-anthropic` | [阿里云百炼 Model Studio(百炼控制台)](https://bailian.console.aliyun.com/) —— 创建或复制 Model Studio API 密钥。 |
|
||||
| `custom` | 设置该具名提供商的 `base_url` 和 `api_key_env` 或 `api_key`;不存在规范的厂商凭据页面。 |
|
||||
|
||||
@@ -424,6 +426,7 @@ Kimi 仍然仅支持 API 密钥;对 Kimi 的外部授权被拒绝。
|
||||
| `mistral` | `[providers.mistral]` | `MISTRAL_API_KEY` | `MISTRAL_BASE_URL`;默认 `https://api.mistral.ai/v1` | `mistral-code-latest`(默认;`codestral-latest` 接受为别名), `mistral-medium-latest`(别名:`mistral-medium-3-5`), `mistral-small-latest`(别名:`mistral-small-2603`), `mistral-large-latest` | Mistral AI(la Plateforme)OpenAI 兼容 Chat 路由。在文档化的第一方 HTTPS `/v1` 主机上,Medium 和 Small 发送可调的 `reasoning_effort`(仅 `none` 或 `high`),解析 Mistral 的多态 thinking/text 块,并以相同线协议形状重放存储的思考。已废弃的原生 Magistral ID 仍是显式配置兼容路由:它们始终思考,从不接收可调 effort 字段。Code 和 Large 不推理。除非是文档化的第一方主机之一,自定义 `MISTRAL_BASE_URL` 保持通用 Chat 语义。接受 `MISTRAL_MODEL`。提供商别名:`mistral-ai`, `mistralai`, `la-plateforme`。 |
|
||||
| `edenai` | `[providers.edenai]` | `EDENAI_API_KEY` | `EDENAI_BASE_URL`;默认 `https://api.edenai.run/v3`;欧盟 `https://api.eu.edenai.run/v3` | `deepseek/deepseek-v4-pro`(默认);`provider/model` id 的实时 `/models` 目录 | Eden AI OpenAI 兼容聚合网关。目录行保持提供商范围;通用推理控制被省略,因为受支持的字段取决于所选上游家族。接受 `EDENAI_MODEL`。默认的 `deepseek/deepseek-v4-pro` 只列在全球目录上;在欧盟端点上把 `EDENAI_MODEL`(或 `model`)设为欧盟 `/models` 列表中的一行,例如 `qwen/deepseek-v4-pro`。提供商别名:`eden-ai`, `eden_ai`。 |
|
||||
| `zenmux` | `[providers.zenmux]` | `ZENMUX_API_KEY` | `ZENMUX_BASE_URL`;默认 `https://zenmux.ai/api/v1` | `deepseek/deepseek-v4.1-flash`(默认);`provider/model` id 的实时 `/models` 目录(免密钥可读) | ZenMux OpenAI 兼容聚合网关(约 190 个模型)。目录行保持提供商范围;通用推理控制被省略,因为受支持的字段取决于所选上游家族。接受 `ZENMUX_MODEL`。提供商别名:`zen-mux`, `zen_mux`。 |
|
||||
| `csdn` | `[providers.csdn]` | `CSDN_API_KEY` | `CSDN_BASE_URL`;默认 `https://ai.csdn.net/api/model/v1` | `glm_for_coding`(默认;Coding Plan 专用模型 id);其他市场模型 id 直通 | CSDN 星图(Starmap)OpenAI 兼容托管平台。Coding Plan 密钥与通用市场密钥共用一个端点,因此计费取决于凭据产品而非 URL 本身:路由 `glm_for_coding`(出厂默认)或设置 `mode = "coding_plan"`/`"plan"`/`"subscription"` 按 CSDN Coding Plan 配额计费,不显示美元估算;任何其他模型或显式 `pay-as-you-go`/`metered` 模式按量计费;无法识别的模式或 `ai.csdn.net/api/model/v1` 之外的端点报告 `cost: unknown`。接受 `CSDN_MODEL`。提供商别名:`csdn-ai`, `csdn_ai`, `csdn-coding-plan`, `csdn_coding_plan`, `starmap`。 |
|
||||
| `xai` | `[providers.xai]` | `XAI_API_KEY`、Codewhale 自有的设备 OAuth,或显式的只读 Grok CLI 授权 | `XAI_BASE_URL`;默认 `https://api.x.ai/v1` | `grok-4.6`(默认), `grok-4.5`, `grok-4.3`, `grok-build`, `grok-composer-2.5-fast`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning` | xAI/Grok OpenAI 兼容 Chat Completions 路由。Grok 4.6 有 500K 上下文窗口、文本/图像输入、函数调用、结构化输出、服务端 web 搜索和 `low`/`medium`/`high`/`xhigh` 推理(默认 `high`)。提示达到 200K token 时其标准费率翻倍;同样的 2 倍长上下文规则适用于 `grok-4.5`(500K 上下文,$2.00 / $0.30 缓存 / $6.00)和 `grok-4.3`(1M 上下文,$1.25 / $0.20 缓存 / $2.50),按其[模型页](https://docs.x.ai/docs/models/grok-4.5)。没有文档化的 `latest`/`fast` 别名,也没有公布的数字输出上限。**API 密钥**(默认):来自 console.x.ai 的 Bearer token,通过 `XAI_API_KEY` / keyring / `api_key`。**OAuth**:`codewhale auth xai-device` 使用 SSH 友好的设备登录和 Codewhale 自有的存储,它可能自行刷新。现有的 Grok CLI 凭据需要 `codewhale auth external-consent --provider xai --mode read-only`;被授予的外部文件从不刷新或重写。OAuth 在某些 SuperGrok 层级上可能返回 HTTP 403——把 API 密钥作为可靠回退。接受 `XAI_MODEL`。提供商别名:`x-ai`, `x_ai`, `grok`。 |
|
||||
| `modelstudio-token-plan` | `[providers.modelstudio_token_plan]` | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` | `MODELSTUDIO_TOKEN_PLAN_BASE_URL`;默认 `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` | `qwen3.8-max`(默认), `qwen3.8-max-preview`, `qwen3.7-plus`, `qwen3.7-max`, `qwen3.6-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-0731`, `glm-5.2` | 阿里云 Model Studio Token Plan OpenAI 兼容 Chat Completions 路由。Token Plan 个人版和团队版共享该端点。列出的所有模型都是可推理的文本/编码模型。DeepSeek 和 GLM 条目是提供商范围的,不与第一方路由冲突。接受 `MODELSTUDIO_TOKEN_PLAN_MODEL`。提供商别名:`modelstudio-token-plan`, `alibaba-token-plan`, `dashscope-token-plan`。 |
|
||||
| `modelstudio-token-plan-anthropic` | `[providers.modelstudio_token_plan_anthropic]` | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` | 默认 `https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic` | 与 `modelstudio-token-plan` 相同的模型目录 | Token Plan Anthropic 兼容 Messages 路由(`/apps/anthropic`)。与 OpenAI 方言相同的 API 密钥。提供商别名:`modelstudio-token-plan-anthropic`, `alibaba-token-plan-anthropic`。 |
|
||||
|
||||
@@ -153,6 +153,7 @@ function deriveProvidersFromConfig(cfg: string): ProviderFact[] {
|
||||
ModelstudioCodingPlan: { id: "modelstudio-coding-plan", label: "Model Studio Coding Plan", env: "MODELSTUDIO_API_KEY" },
|
||||
ModelstudioCodingPlanAnthropic: { id: "modelstudio-coding-plan-anthropic", label: "Model Studio Coding Plan (Anthropic-compatible)", env: "MODELSTUDIO_API_KEY" },
|
||||
Zenmux: { id: "zenmux", label: "ZenMux", env: "ZENMUX_API_KEY" },
|
||||
Csdn: { id: "csdn", label: "CSDN 星图 (Starmap)", env: "CSDN_API_KEY" },
|
||||
};
|
||||
// Log loudly on unmapped variants so a new provider can never be silently
|
||||
// dropped from the drift-derived facts again. DeepseekCN (#1104), the
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface RepoFacts {
|
||||
}
|
||||
|
||||
export const FACTS: RepoFacts = {
|
||||
"generatedAt": "2026-09-18T18:49:13.339Z",
|
||||
"generatedAt": "2026-09-19T00:08:11.198Z",
|
||||
"sourceRevision": null,
|
||||
"sourceCommittedAt": null,
|
||||
"version": "0.9.14",
|
||||
@@ -289,6 +289,11 @@ export const FACTS: RepoFacts = {
|
||||
"label": "ZenMux",
|
||||
"env": "ZENMUX_API_KEY"
|
||||
},
|
||||
{
|
||||
"id": "csdn",
|
||||
"label": "CSDN 星图 (Starmap)",
|
||||
"env": "CSDN_API_KEY"
|
||||
},
|
||||
{
|
||||
"id": "concentrate",
|
||||
"label": "Concentrate",
|
||||
|
||||
@@ -115,6 +115,7 @@ const PROVIDER_LABEL_MAP = {
|
||||
ModelstudioCodingPlan: { id: "modelstudio-coding-plan", label: "Model Studio Coding Plan", env: "MODELSTUDIO_API_KEY" },
|
||||
ModelstudioCodingPlanAnthropic: { id: "modelstudio-coding-plan-anthropic", label: "Model Studio Coding Plan (Anthropic-compatible)", env: "MODELSTUDIO_API_KEY" },
|
||||
Zenmux: { id: "zenmux", label: "ZenMux", env: "ZENMUX_API_KEY" },
|
||||
Csdn: { id: "csdn", label: "CSDN 星图 (Starmap)", env: "CSDN_API_KEY" },
|
||||
};
|
||||
|
||||
// DeepseekCN: not wired through shared ProviderKind (#1104).
|
||||
|
||||
Reference in new issue
Block a user