fix(providers): 1M Model Studio context; dialect is wire config, not catalog rows

qwen3.8-max was showing 128K because Model Studio hand-seam offerings
shipped empty RouteLimits, won identity collisions over the 1M catalog
rows, and fell through to the legacy 128K default (that number is the
generation ceiling, not the window). Hand-seam limits now publish 1M
context / 128K output for qwen3.8-max; models.rs pins the same facts.

Catalog surface: one identity per vendor. Dual-wire kinds (DeepSeek /
MiniMax / Model Studio *Anthropic) and Model Studio coding-plan kinds
stay on the enum for serde, but leave ProviderKind::ALL / picker catalog.
Plan is mode/base_url (Z.ai/Xiaomi shape). Dialect is
providers.<id>.wire = openai|anthropic — a power-user toggle, not a
second row. Aliases collapse onto the primary; legacy kinds still resolve.

Verified: codewhale-config lib 489 pass; targeted modelstudio/picker/cli
helpers green.
This commit is contained in:
Hmbown
2026-08-03 00:03:41 -07:00
parent 70d729bd05
commit d53f4f998b
11 changed files with 607 additions and 198 deletions
+10 -2
View File
@@ -7927,9 +7927,17 @@ model = "qwen-2.5-7b"
.iter()
.map(|provider| provider.kind())
.collect();
assert_eq!(registry_kinds, ProviderKind::ALL);
// Full registry keeps legacy dialect/plan kinds; ALL is the catalog surface.
assert_eq!(registry_kinds.len(), 41);
assert_eq!(ProviderKind::ALL.len(), 36);
for kind in ProviderKind::ALL {
assert!(
registry_kinds.contains(&kind),
"catalog kind {kind:?} must remain in the full registry"
);
}
for provider in ProviderKind::ALL {
for provider in registry_kinds {
assert_eq!(provider_env_vars(provider), provider.provider().env_vars());
// Shared-account families collapse onto one durable slot (see
// ProviderKind::secret_store_slot); everything else uses its own id.
+149 -11
View File
@@ -137,6 +137,18 @@ pub struct ProviderConfigToml {
pub context_window: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
/// Wire dialect preference for dual-protocol vendors (DeepSeek, MiniMax,
/// Model Studio): `openai` (Chat Completions, default) or `anthropic`
/// (Messages). Not a separate catalog provider — a power-user toggle.
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "api_style",
alias = "protocol",
alias = "wire_format",
alias = "dialect"
)]
pub wire: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -172,6 +184,7 @@ impl ProviderConfigToml {
&& blank(self.model.as_ref())
&& self.context_window.is_none()
&& blank(self.mode.as_ref())
&& blank(self.wire.as_ref())
&& blank(self.auth_mode.as_ref())
&& self.insecure_skip_tls_verify.is_none()
&& http_headers_are_effectively_empty(&self.http_headers)
@@ -758,6 +771,7 @@ enum ProviderConfigField {
Model,
ContextWindow,
Mode,
Wire,
AuthMode,
InsecureSkipTlsVerify,
HttpHeaders,
@@ -772,6 +786,7 @@ impl ProviderConfigField {
"model" => Self::Model,
"context_window" | "context_window_tokens" => Self::ContextWindow,
"mode" => Self::Mode,
"wire" | "api_style" | "protocol" | "wire_format" | "dialect" => Self::Wire,
"auth_mode" => Self::AuthMode,
"insecure_skip_tls_verify" => Self::InsecureSkipTlsVerify,
"http_headers" => Self::HttpHeaders,
@@ -787,6 +802,7 @@ impl ProviderConfigField {
Self::Model => "model",
Self::ContextWindow => "context_window",
Self::Mode => "mode",
Self::Wire => "wire",
Self::AuthMode => "auth_mode",
Self::InsecureSkipTlsVerify => "insecure_skip_tls_verify",
Self::HttpHeaders => "http_headers",
@@ -823,7 +839,7 @@ fn is_builtin_provider_config_id(provider_id: &str) -> bool {
/// Field legs a `[providers.<id>]` custom table accepts through
/// `config set`, including the required `kind` marker.
const CUSTOM_PROVIDER_FIELD_HINT: &str = "api_key, base_url, model, context_window, mode, auth_mode, \
const CUSTOM_PROVIDER_FIELD_HINT: &str = "api_key, base_url, model, context_window, mode, wire, auth_mode, \
insecure_skip_tls_verify, http_headers, path_suffix, kind";
fn provider_config_key(provider: ProviderKind, field: ProviderConfigField) -> String {
@@ -844,6 +860,7 @@ fn get_provider_config_value(
ProviderConfigField::Model => config.model.clone(),
ProviderConfigField::ContextWindow => config.context_window.map(|value| value.to_string()),
ProviderConfigField::Mode => config.mode.clone(),
ProviderConfigField::Wire => config.wire.clone(),
ProviderConfigField::AuthMode => config.auth_mode.clone(),
ProviderConfigField::InsecureSkipTlsVerify => config
.insecure_skip_tls_verify
@@ -911,6 +928,9 @@ fn set_provider_config_value(
ProviderConfigField::Mode => {
config.providers.for_provider_mut(provider).mode = Some(value.to_string());
}
ProviderConfigField::Wire => {
config.providers.for_provider_mut(provider).wire = Some(value.to_string());
}
ProviderConfigField::AuthMode => {
config.providers.for_provider_mut(provider).auth_mode = Some(value.to_string());
}
@@ -964,6 +984,9 @@ fn unset_provider_config_value(
ProviderConfigField::Mode => {
config.providers.for_provider_mut(provider).mode = None;
}
ProviderConfigField::Wire => {
config.providers.for_provider_mut(provider).wire = None;
}
ProviderConfigField::AuthMode => {
config.providers.for_provider_mut(provider).auth_mode = None;
}
@@ -2255,6 +2278,7 @@ impl ConfigToml {
| ProviderConfigField::BaseUrl
| ProviderConfigField::Model
| ProviderConfigField::Mode
| ProviderConfigField::Wire
| ProviderConfigField::AuthMode
| ProviderConfigField::PathSuffix => toml::Value::String(value.to_string()),
ProviderConfigField::ContextWindow => {
@@ -2724,12 +2748,30 @@ impl ConfigToml {
classify_config_api_key_value(value) == ConfigApiKeyValueKind::Literal
}))
.or(xiaomi_mimo_env_api_key.as_deref());
let provider_wire = provider_cfg.wire.as_deref();
let base_url = if provider == ProviderKind::XiaomiMimo {
resolve_xiaomi_mimo_base_url(
configured_base_url,
explicit_api_key_for_endpoint,
xiaomi_mimo_mode.as_deref(),
)
} else if is_modelstudio_family(provider) {
resolve_modelstudio_base_url(
configured_base_url,
provider,
provider_cfg.mode.as_deref(),
provider_wire,
)
} else if matches!(
provider,
ProviderKind::Minimax | ProviderKind::MinimaxAnthropic
) {
resolve_minimax_base_url(configured_base_url, provider, provider_wire)
} else if matches!(
provider,
ProviderKind::Deepseek | ProviderKind::DeepseekAnthropic
) {
resolve_deepseek_base_url(configured_base_url, provider, provider_wire)
} else {
configured_base_url.unwrap_or_else(|| match provider {
ProviderKind::Deepseek => DEFAULT_DEEPSEEK_BASE_URL.to_string(),
@@ -2777,18 +2819,12 @@ impl ConfigToml {
ProviderKind::Meta => DEFAULT_META_BASE_URL.to_string(),
ProviderKind::Xai => DEFAULT_XAI_BASE_URL.to_string(),
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL.to_string(),
ProviderKind::ModelstudioTokenPlan => {
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
| ProviderKind::ModelstudioCodingPlan
| ProviderKind::ModelstudioCodingPlanAnthropic => {
DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL.to_string()
}
ProviderKind::ModelstudioTokenPlanAnthropic => {
MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL.to_string()
}
ProviderKind::ModelstudioCodingPlan => {
DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL.to_string()
}
ProviderKind::ModelstudioCodingPlanAnthropic => {
MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL.to_string()
}
// The custom provider has no built-in endpoint; fall back to its
// descriptor placeholder so the lookup is total. Real custom
// routes always supply a configured base_url before this point.
@@ -3788,6 +3824,108 @@ fn moonshot_base_url_uses_kimi_code(base_url: &str) -> bool {
|| normalized.starts_with("https://api.kimi.com/coding/")
}
/// Dual-wire vendors: dialect is config (`wire`), not a separate ProviderKind.
fn wire_prefers_anthropic(kind: ProviderKind, wire: Option<&str>) -> bool {
if matches!(
kind,
ProviderKind::DeepseekAnthropic
| ProviderKind::MinimaxAnthropic
| ProviderKind::ModelstudioTokenPlanAnthropic
| ProviderKind::ModelstudioCodingPlanAnthropic
) {
return true;
}
let Some(raw) = wire.map(str::trim).filter(|value| !value.is_empty()) else {
return false;
};
let normalized = raw.to_ascii_lowercase().replace(['_', ' '], "-");
matches!(
normalized.as_str(),
"anthropic"
| "anthropic-messages"
| "messages"
| "claude"
| "anthropic-compatible"
| "anthropic-compat"
)
}
fn modelstudio_mode_is_coding_plan(kind: ProviderKind, mode: Option<&str>) -> bool {
if matches!(
kind,
ProviderKind::ModelstudioCodingPlan | ProviderKind::ModelstudioCodingPlanAnthropic
) {
return true;
}
let Some(raw) = mode.map(str::trim).filter(|value| !value.is_empty()) else {
return false;
};
let normalized = raw.to_ascii_lowercase().replace(['_', ' '], "-");
matches!(
normalized.as_str(),
"coding-plan" | "coding" | "codingplan" | "dashscope-coding" | "code"
)
}
fn is_modelstudio_family(kind: ProviderKind) -> bool {
matches!(
kind,
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
| ProviderKind::ModelstudioCodingPlan
| ProviderKind::ModelstudioCodingPlanAnthropic
)
}
fn resolve_modelstudio_base_url(
configured: Option<String>,
kind: ProviderKind,
mode: Option<&str>,
wire: Option<&str>,
) -> String {
if let Some(url) = configured.filter(|value| !value.trim().is_empty()) {
return url;
}
let coding = modelstudio_mode_is_coding_plan(kind, mode);
let anthropic = wire_prefers_anthropic(kind, wire);
match (coding, anthropic) {
(true, true) => MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL.to_string(),
(true, false) => DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL.to_string(),
(false, true) => MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL.to_string(),
(false, false) => DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL.to_string(),
}
}
fn resolve_minimax_base_url(
configured: Option<String>,
kind: ProviderKind,
wire: Option<&str>,
) -> String {
if let Some(url) = configured.filter(|value| !value.trim().is_empty()) {
return url;
}
if wire_prefers_anthropic(kind, wire) {
DEFAULT_MINIMAX_ANTHROPIC_BASE_URL.to_string()
} else {
DEFAULT_MINIMAX_BASE_URL.to_string()
}
}
fn resolve_deepseek_base_url(
configured: Option<String>,
kind: ProviderKind,
wire: Option<&str>,
) -> String {
if let Some(url) = configured.filter(|value| !value.trim().is_empty()) {
return url;
}
if wire_prefers_anthropic(kind, wire) {
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL.to_string()
} else {
DEFAULT_DEEPSEEK_BASE_URL.to_string()
}
}
fn xiaomi_mimo_base_url_for_mode(mode: &str) -> Option<&'static str> {
let normalized = mode.trim().to_ascii_lowercase().replace(['_', ' '], "-");
if normalized.is_empty() || xiaomi_mimo_mode_uses_standard_endpoint(&normalized) {
+51 -38
View File
@@ -610,6 +610,11 @@ impl Provider for Deepseek {
"deepseek_china",
"deepseekcn",
"deepseek-china",
// Dialect is wire=anthropic on this provider, not a second catalog row.
"deepseek-anthropic",
"deepseek_anthropic",
"deepseek-claude",
"deepseek_claude",
]
}
@@ -619,6 +624,8 @@ impl Provider for Deepseek {
}
/// Opt-in DeepSeek route that speaks the Anthropic Messages wire protocol.
///
/// Legacy kind kept for serde; parse/catalog collapse onto [`Deepseek`].
pub struct DeepseekAnthropic;
impl Provider for DeepseekAnthropic {
@@ -631,7 +638,8 @@ impl Provider for DeepseekAnthropic {
}
fn display_name(&self) -> &'static str {
"DeepSeek (Anthropic-compatible)"
// Legacy dialect kind — catalog surface is "DeepSeek" with wire=anthropic.
"DeepSeek"
}
fn default_base_url(&self) -> &'static str {
@@ -651,7 +659,7 @@ impl Provider for DeepseekAnthropic {
}
fn aliases(&self) -> &'static [&'static str] {
&["deepseek_anthropic", "deepseek-claude", "deepseek_claude"]
&[]
}
fn wire_policy(&self) -> WirePolicy {
@@ -1058,7 +1066,8 @@ provider!(
DEFAULT_MINIMAX_MODEL,
["MINIMAX_API_KEY"],
"minimax",
aliases: ["mini-max", "mini_max"]
// Anthropic dialect is wire=anthropic on this provider, not a second row.
aliases: ["mini-max", "mini_max", "minimax-anthropic", "minimax_anthropic", "mini-max-anthropic", "mini_max_anthropic"]
);
/// MiniMax route that speaks the Anthropic Messages wire protocol.
@@ -1074,7 +1083,8 @@ impl Provider for MinimaxAnthropic {
}
fn display_name(&self) -> &'static str {
"MiniMax (Anthropic-compatible)"
// Legacy dialect kind — catalog surface is "MiniMax" with wire=anthropic.
"MiniMax"
}
fn default_base_url(&self) -> &'static str {
@@ -1094,11 +1104,7 @@ impl Provider for MinimaxAnthropic {
}
fn aliases(&self) -> &'static [&'static str] {
&[
"minimax_anthropic",
"mini-max-anthropic",
"mini_max_anthropic",
]
&[]
}
fn wire_policy(&self) -> WirePolicy {
@@ -1255,7 +1261,10 @@ impl Provider for ModelstudioTokenPlan {
}
fn display_name(&self) -> &'static str {
"Alibaba Cloud Model Studio (Token Plan)"
// One vendor row. Plan (token vs coding) is `mode` / base_url; wire
// dialect (OpenAI vs Anthropic Messages) is `wire` — never separate
// catalog identities (same product rule as Z.ai / Xiaomi for plans).
"Alibaba Cloud Model Studio"
}
fn default_base_url(&self) -> &'static str {
@@ -1275,19 +1284,36 @@ impl Provider for ModelstudioTokenPlan {
}
fn aliases(&self) -> &'static [&'static str] {
// Plan and dialect aliases collapse onto this primary identity.
// Config fields: mode = token-plan|coding-plan, wire = openai|anthropic.
&[
"modelstudio-token-plan",
"modelstudio_token_plan",
"modelstudio",
"alibaba-token-plan",
"dashscope-token-plan",
"alibaba",
"dashscope",
// Legacy plan/dialect kinds — keep resolving so old configs and
// CLI flags do not break; they no longer appear as catalog rows.
"modelstudio-coding-plan",
"modelstudio_coding_plan",
"alibaba-coding-plan",
"dashscope-coding-plan",
"modelstudio-token-plan-anthropic",
"modelstudio_token_plan_anthropic",
"alibaba-token-plan-anthropic",
"modelstudio-coding-plan-anthropic",
"modelstudio_coding_plan_anthropic",
"alibaba-coding-plan-anthropic",
]
}
}
/// Alibaba Cloud Model Studio — Token Plan Anthropic-compatible endpoint.
/// Legacy Model Studio Anthropic dialect kind.
///
/// Same API key as `modelstudio-token-plan`; speaks the native Anthropic
/// Messages wire protocol on the `/apps/anthropic` path.
/// Kept for serde / provider_for_kind only. Catalog surface and parse aliases
/// collapse onto [`ModelstudioTokenPlan`] with `wire = "anthropic"`.
pub struct ModelstudioTokenPlanAnthropic;
impl Provider for ModelstudioTokenPlanAnthropic {
@@ -1300,7 +1326,7 @@ impl Provider for ModelstudioTokenPlanAnthropic {
}
fn display_name(&self) -> &'static str {
"Alibaba Cloud Model Studio (Token Plan, Anthropic-compatible)"
"Alibaba Cloud Model Studio"
}
fn default_base_url(&self) -> &'static str {
@@ -1320,11 +1346,8 @@ impl Provider for ModelstudioTokenPlanAnthropic {
}
fn aliases(&self) -> &'static [&'static str] {
&[
"modelstudio-token-plan-anthropic",
"modelstudio_token_plan_anthropic",
"alibaba-token-plan-anthropic",
]
// Empty: aliases live on the primary so parse collapses to it.
&[]
}
fn wire_policy(&self) -> WirePolicy {
@@ -1332,7 +1355,9 @@ impl Provider for ModelstudioTokenPlanAnthropic {
}
}
/// Alibaba Cloud Model Studio Coding Plan (OpenAI-compatible Chat Completions).
/// Legacy Model Studio Coding Plan kind (OpenAI wire).
///
/// Catalog/parse collapse onto [`ModelstudioTokenPlan`] with `mode = "coding-plan"`.
pub struct ModelstudioCodingPlan;
impl Provider for ModelstudioCodingPlan {
@@ -1345,7 +1370,7 @@ impl Provider for ModelstudioCodingPlan {
}
fn display_name(&self) -> &'static str {
"Alibaba Cloud Model Studio (Coding Plan)"
"Alibaba Cloud Model Studio"
}
fn default_base_url(&self) -> &'static str {
@@ -1365,19 +1390,11 @@ impl Provider for ModelstudioCodingPlan {
}
fn aliases(&self) -> &'static [&'static str] {
&[
"modelstudio-coding-plan",
"modelstudio_coding_plan",
"alibaba-coding-plan",
"dashscope-coding-plan",
]
&[]
}
}
/// Alibaba Cloud Model Studio Coding Plan Anthropic-compatible endpoint.
///
/// Same API key as `modelstudio-coding-plan`; speaks the native Anthropic
/// Messages wire protocol on the `/apps/anthropic` path.
/// Legacy Model Studio Coding Plan Anthropic dialect kind.
pub struct ModelstudioCodingPlanAnthropic;
impl Provider for ModelstudioCodingPlanAnthropic {
@@ -1390,7 +1407,7 @@ impl Provider for ModelstudioCodingPlanAnthropic {
}
fn display_name(&self) -> &'static str {
"Alibaba Cloud Model Studio (Coding Plan, Anthropic-compatible)"
"Alibaba Cloud Model Studio"
}
fn default_base_url(&self) -> &'static str {
@@ -1410,11 +1427,7 @@ impl Provider for ModelstudioCodingPlanAnthropic {
}
fn aliases(&self) -> &'static [&'static str] {
&[
"modelstudio-coding-plan-anthropic",
"modelstudio_coding_plan_anthropic",
"alibaba-coding-plan-anthropic",
]
&[]
}
fn wire_policy(&self) -> WirePolicy {
@@ -2001,7 +2014,7 @@ mod tests {
// actually took effect.
assert_eq!(
display[0].display_name(),
"Alibaba Cloud Model Studio (Coding Plan)",
"Alibaba Cloud Model Studio",
"alphabetical display order should lead with Alibaba Cloud Model Studio"
);
}
+7 -6
View File
@@ -189,9 +189,14 @@ pub enum ProviderKind {
}
impl ProviderKind {
pub const ALL: [Self; 41] = [
/// Catalog / picker surface: one identity per vendor.
///
/// Dual-wire dialect kinds (`*Anthropic`) and Model Studio plan variants
/// 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; 36] = [
Self::Deepseek,
Self::DeepseekAnthropic,
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
@@ -217,7 +222,6 @@ impl ProviderKind {
Self::Zai,
Self::Stepfun,
Self::Minimax,
Self::MinimaxAnthropic,
Self::Deepinfra,
Self::Sakana,
Self::LongCat,
@@ -227,9 +231,6 @@ impl ProviderKind {
Self::Xai,
Self::Telecomjs,
Self::ModelstudioTokenPlan,
Self::ModelstudioTokenPlanAnthropic,
Self::ModelstudioCodingPlan,
Self::ModelstudioCodingPlanAnthropic,
Self::Custom,
];
+24 -6
View File
@@ -139,6 +139,28 @@ fn every_provider_kind_resolves_the_auto_selector() {
}
}
#[test]
fn modelstudio_qwen38_max_offering_publishes_1m_context() {
use super::RouteLimits;
let offering = bundled_offerings()
.into_iter()
.find(|offering| {
offering.provider.as_str() == "modelstudio-token-plan"
&& offering.wire_model_id.as_str() == "qwen3.8-max"
})
.expect("modelstudio qwen3.8-max offering");
assert_eq!(
offering.limits,
RouteLimits {
context_tokens: Some(1_000_000),
input_tokens: None,
output_tokens: Some(131_072),
},
"hand-seam limits must not be empty (empty won collisions and fell to 128K)"
);
}
#[test]
fn modelstudio_image_input_capability_is_per_model() {
use super::capabilities::CapabilityState;
@@ -158,12 +180,8 @@ fn modelstudio_image_input_capability_is_per_model() {
"deepseek-v4-flash-0731",
"glm-5.2",
];
const PROVIDERS: &[&str] = &[
"modelstudio-token-plan",
"modelstudio-coding-plan",
"modelstudio-token-plan-anthropic",
"modelstudio-coding-plan-anthropic",
];
// Catalog surface is one vendor identity; plan/dialect are config.
const PROVIDERS: &[&str] = &["modelstudio-token-plan"];
let offerings = bundled_offerings();
for provider in PROVIDERS {
+49 -47
View File
@@ -232,14 +232,18 @@ pub fn bundled_offerings() -> Vec<ProviderModelOffering> {
})
}));
// Alibaba Cloud Model Studio — Token Plan and Coding Plan.
// All models below are classified as Text Generation / Reasoning on the
// Model Studio catalog; reasoning and tool-call capabilities are marked
// Supported conservatively. Image input is per model: the owner's Token
// Plan console (verified 2026-08-03) lists Visual Understanding for
// qwen3.8-max, qwen3.8-max-preview, qwen3.7-plus, and qwen3.6-flash —
// corroborated by upstream Models.dev modalities — while qwen3.7-max,
// the DeepSeek rows, and glm-5.2 are text-only on both sources.
// Alibaba Cloud Model Studio — one vendor identity in the hand seam
// (`modelstudio-token-plan`). Plan (token vs coding) and wire dialect
// (OpenAI Chat Completions vs Anthropic Messages) are config (`mode` /
// `wire`), not separate ProviderKinds — same product shape as Z.ai /
// Xiaomi for plans and a power-user toggle for dialect. Legacy provider
// ids still get catalog rows so old configs resolve, but the picker
// catalog surface only lists the primary id.
//
// Limits: owner's Token Plan console + curated models_dev rows
// (2026-08-03): qwen3.8-max is ~1M context / 128K output, NOT 128K
// total. Empty RouteLimits here used to win identity collisions over
// the asset catalog and fall through to the 128K legacy default.
fn ms_capabilities(model: &str) -> RouteCapabilities {
let image_input = match model {
"qwen3.8-max" | "qwen3.8-max-preview" | "qwen3.7-plus" | "qwen3.6-flash" => {
@@ -256,46 +260,44 @@ pub fn bundled_offerings() -> Vec<ProviderModelOffering> {
..RouteCapabilities::default()
}
}
for plan_provider_id in &["modelstudio-token-plan", "modelstudio-coding-plan"] {
let plan = ProviderId::from(*plan_provider_id);
offerings.extend(
MODELSTUDIO_TEXT_MODELS
.iter()
.enumerate()
.map(|(i, model)| ProviderModelOffering {
provider: plan.clone(),
canonical_model: None,
wire_model_id: WireModelId::from(*model),
endpoint_key: "chat".to_string(),
default_for_provider: i == 0,
limits: RouteLimits::default(),
capabilities: ms_capabilities(model),
pricing: PricingSku::UnknownOrStale,
}),
);
}
// Anthropic-dialect variants use the messages endpoint key.
for plan_provider_id in &[
"modelstudio-token-plan-anthropic",
"modelstudio-coding-plan-anthropic",
] {
let plan = ProviderId::from(*plan_provider_id);
offerings.extend(
MODELSTUDIO_TEXT_MODELS
.iter()
.enumerate()
.map(|(i, model)| ProviderModelOffering {
provider: plan.clone(),
canonical_model: None,
wire_model_id: WireModelId::from(*model),
endpoint_key: "messages".to_string(),
default_for_provider: i == 0,
limits: RouteLimits::default(),
capabilities: ms_capabilities(model),
pricing: PricingSku::UnknownOrStale,
}),
);
fn ms_limits(model: &str) -> RouteLimits {
// Context/output from models_dev.bundled.json Model Studio rows and
// the owner console (verified 2026-08-03). Keep output separate from
// context so a 128K generation ceiling is never mistaken for the
// window.
let (context_tokens, output_tokens) = match model {
"qwen3.8-max" | "qwen3.8-max-preview" => (1_000_000, 131_072),
"qwen3.7-plus" | "qwen3.7-max" => (1_000_000, 65_536),
"qwen3.6-flash" => (1_000_000, 65_536),
"deepseek-v4-pro" | "deepseek-v4-flash-0731" => (1_000_000, 384_000),
"glm-5.2" => (1_000_000, 131_072),
_ => (1_000_000, 131_072),
};
RouteLimits {
context_tokens: Some(context_tokens),
input_tokens: None,
output_tokens: Some(output_tokens),
}
}
// Primary vendor id only in the hand seam. Coding-plan / anthropic
// dialect endpoint selection is owned by config resolution (mode/wire),
// which rewrites base_url + request dialect without inventing kinds.
let plan = ProviderId::from("modelstudio-token-plan");
offerings.extend(
MODELSTUDIO_TEXT_MODELS
.iter()
.enumerate()
.map(|(i, model)| ProviderModelOffering {
provider: plan.clone(),
canonical_model: None,
wire_model_id: WireModelId::from(*model),
endpoint_key: "chat".to_string(),
default_for_provider: i == 0,
limits: ms_limits(model),
capabilities: ms_capabilities(model),
pricing: PricingSku::UnknownOrStale,
}),
);
offerings
}
+59 -62
View File
@@ -4319,61 +4319,47 @@ fn provider_kind_accepts_legacy_deepseek_cn_aliases() {
}
#[test]
fn deepseek_anthropic_route_defaults_to_anthropic_endpoint() {
fn deepseek_anthropic_aliases_collapse_onto_primary_with_wire_toggle() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
// Dialect is not a catalog identity — aliases resolve to DeepSeek primary.
for alias in [
"deepseek-anthropic",
"deepseek_anthropic",
"deepseek-claude",
"deepseek_claude",
] {
assert_eq!(
ProviderKind::parse(alias),
Some(ProviderKind::DeepseekAnthropic)
);
let parsed: ConfigToml =
toml::from_str(&format!("provider = \"{alias}\"")).expect("deepseek anthropic alias");
assert_eq!(parsed.provider, ProviderKind::DeepseekAnthropic);
assert_eq!(ProviderKind::parse(alias), Some(ProviderKind::Deepseek));
}
let provider = provider::resolve_provider("deepseek-anthropic")
.expect("deepseek anthropic metadata resolves");
assert_eq!(provider.kind(), ProviderKind::DeepseekAnthropic);
assert_eq!(provider.provider_config_key(), "deepseek_anthropic");
assert_eq!(provider.default_model(), DEFAULT_DEEPSEEK_ANTHROPIC_MODEL);
assert_eq!(
provider.default_base_url(),
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
);
assert_eq!(provider.env_vars(), &["DEEPSEEK_API_KEY"]);
assert_eq!(
provider.wire_policy().fixed(),
Some(provider::WireFormat::AnthropicMessages)
);
.expect("deepseek anthropic alias resolves to primary");
assert_eq!(provider.kind(), ProviderKind::Deepseek);
assert_eq!(provider.id(), "deepseek");
let config = ConfigToml {
// wire=anthropic selects the Messages endpoint without a second provider.
let config: ConfigToml = toml::from_str(
r#"
provider = "deepseek"
[providers.deepseek]
wire = "anthropic"
"#,
)
.expect("deepseek wire config");
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Deepseek);
assert_eq!(resolved.base_url, DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL);
// Legacy serde kind still resolves the anthropic endpoint.
let legacy = ConfigToml {
provider: ProviderKind::DeepseekAnthropic,
..ConfigToml::default()
};
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::DeepseekAnthropic);
assert_eq!(resolved.base_url, DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL);
assert_eq!(resolved.model, DEFAULT_DEEPSEEK_ANTHROPIC_MODEL);
unsafe {
std::env::set_var(
"DEEPSEEK_ANTHROPIC_BASE_URL",
"https://gateway.example.test/anthropic",
);
}
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.base_url, "https://gateway.example.test/anthropic");
unsafe {
std::env::remove_var("DEEPSEEK_ANTHROPIC_BASE_URL");
}
let legacy_resolved = legacy.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(
legacy_resolved.base_url,
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL
);
}
#[test]
@@ -4704,17 +4690,24 @@ fn meta_model_api_scopes_both_documented_key_names_to_official_endpoint() {
#[test]
fn provider_metadata_registry_covers_every_provider_kind_once() {
let providers = provider::all_providers();
assert_eq!(providers.len(), ProviderKind::ALL.len());
for (kind, provider) in ProviderKind::ALL.iter().zip(providers.iter()) {
assert_eq!(provider.kind(), *kind);
assert_eq!(provider.id(), kind.as_str());
assert_eq!(kind.provider().id(), kind.as_str());
}
// Full registry keeps legacy dialect/plan kinds for provider_for_kind.
assert_eq!(providers.len(), 41);
// Catalog surface is one identity per vendor (no dual-wire / plan rows).
assert_eq!(ProviderKind::ALL.len(), 36);
assert!(ProviderKind::ALL.len() < providers.len());
let mut ids = std::collections::BTreeSet::new();
for provider in providers {
assert!(ids.insert(provider.id()), "duplicate provider id");
assert_eq!(provider.id(), provider.kind().as_str());
assert_eq!(provider.kind().provider().id(), provider.id());
}
// Catalog entries are a subset of the full registry.
for kind in ProviderKind::ALL {
assert!(
providers.iter().any(|p| p.kind() == kind),
"catalog kind {kind:?} missing from full registry"
);
}
}
@@ -5246,25 +5239,29 @@ fn minimax_env_model_override_canonicalizes_known_aliases() {
}
#[test]
fn minimax_anthropic_env_overrides_use_messages_base_url() {
fn minimax_wire_anthropic_selects_messages_endpoint() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
unsafe {
env::set_var("CODEWHALE_PROVIDER", "minimax-anthropic");
env::set_var(
"MINIMAX_ANTHROPIC_BASE_URL",
"https://messages.minimax.example/anthropic",
);
env::set_var("MINIMAX_MODEL", "MiniMax-M2.7");
}
let resolved = ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::MinimaxAnthropic);
// minimax-anthropic is an alias of MiniMax; dialect is wire config.
assert_eq!(
resolved.base_url,
"https://messages.minimax.example/anthropic"
ProviderKind::parse("minimax-anthropic"),
Some(ProviderKind::Minimax)
);
let config: ConfigToml = toml::from_str(
r#"
provider = "minimax"
[providers.minimax]
wire = "anthropic"
model = "MiniMax-M2.7"
"#,
)
.expect("minimax wire config");
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Minimax);
assert_eq!(resolved.base_url, DEFAULT_MINIMAX_ANTHROPIC_BASE_URL);
assert_eq!(resolved.model, "MiniMax-M2.7");
}
+57 -1
View File
@@ -1006,7 +1006,7 @@ impl DeepSeekClient {
Self::from_parts(
config.deepseek_base_url(),
config.default_model(),
provider_default_wire_format(api_provider),
provider_wire_format_for_config(api_provider, Some(config)),
config,
)
}
@@ -1500,6 +1500,46 @@ fn is_auth_dialect_header(header_name: &HeaderName) -> bool {
}
fn provider_default_wire_format(api_provider: ApiProvider) -> WireFormat {
provider_wire_format_for_config(api_provider, None)
}
/// Resolve the wire dialect for a dual-protocol vendor.
///
/// Power-user toggle: `providers.<id>.wire = "openai" | "anthropic"`.
/// Legacy dialect kinds (`*Anthropic`) still force Messages. Everyone else
/// keeps the descriptor's fixed policy (or Chat Completions).
fn provider_wire_format_for_config(
api_provider: ApiProvider,
config: Option<&crate::config::Config>,
) -> WireFormat {
let catalog = api_provider.catalog_identity();
let wire = config
.and_then(|cfg| cfg.provider_config_for(catalog))
.and_then(|entry| entry.wire.as_deref());
let prefers_anthropic = matches!(
api_provider,
ApiProvider::DeepseekAnthropic
| ApiProvider::MinimaxAnthropic
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlanAnthropic
) || wire_config_prefers_anthropic(wire);
if prefers_anthropic
&& matches!(
catalog,
ApiProvider::Deepseek
| ApiProvider::Minimax
| ApiProvider::ModelstudioTokenPlan
| ApiProvider::DeepseekAnthropic
| ApiProvider::MinimaxAnthropic
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
| ApiProvider::ModelstudioCodingPlanAnthropic
)
{
return WireFormat::AnthropicMessages;
}
api_provider
.kind()
.and_then(|kind| {
@@ -1516,6 +1556,22 @@ fn provider_default_wire_format(api_provider: ApiProvider) -> WireFormat {
})
}
fn wire_config_prefers_anthropic(wire: Option<&str>) -> bool {
let Some(raw) = wire.map(str::trim).filter(|value| !value.is_empty()) else {
return false;
};
let normalized = raw.to_ascii_lowercase().replace(['_', ' '], "-");
matches!(
normalized.as_str(),
"anthropic"
| "anthropic-messages"
| "messages"
| "claude"
| "anthropic-compatible"
| "anthropic-compat"
)
}
fn api_provider_skips_models_probe(api_provider: ApiProvider) -> bool {
matches!(api_provider, ApiProvider::DeepseekAnthropic)
}
+161 -20
View File
@@ -241,12 +241,45 @@ impl ApiProvider {
self.credential_help().credential_url
}
/// All providers in stable `ProviderKind::ALL` order.
/// All providers including legacy dual-wire / plan-variant kinds.
///
/// Prefer [`Self::catalog`] for pickers and other user-facing lists.
#[must_use]
pub fn all() -> &'static [Self] {
&Self::FROM_KIND_LOOKUP
}
/// User-facing catalog surface: one identity per vendor.
///
/// Matches `ProviderKind::ALL` — dialect is `providers.<id>.wire`, plan is
/// `mode` / base_url (Z.ai / Xiaomi shape), not extra ProviderKinds.
#[must_use]
pub fn catalog() -> &'static [Self] {
static CATALOG: std::sync::OnceLock<Vec<ApiProvider>> = std::sync::OnceLock::new();
CATALOG
.get_or_init(|| {
codewhale_config::ProviderKind::ALL
.iter()
.copied()
.map(Self::from_kind)
.collect()
})
.as_slice()
}
/// Collapse legacy dialect/plan kinds onto the vendor primary for UI.
#[must_use]
pub fn catalog_identity(self) -> Self {
match self {
Self::DeepseekAnthropic => Self::Deepseek,
Self::MinimaxAnthropic => Self::Minimax,
Self::ModelstudioTokenPlanAnthropic
| Self::ModelstudioCodingPlan
| Self::ModelstudioCodingPlanAnthropic => Self::ModelstudioTokenPlan,
other => other,
}
}
/// `ApiProvider` discriminant → `ProviderKind` lookup.
/// Index 1 is `None` for the legacy `DeepseekCN` variant.
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 42] = [
@@ -2929,6 +2962,19 @@ pub struct ProviderConfig {
)]
pub context_window: Option<u32>,
pub mode: Option<String>,
/// Dual-wire dialect toggle: `openai` (default) or `anthropic`.
/// Not a separate catalog provider — config only (DeepSeek / MiniMax /
/// Model Studio).
#[serde(
default,
alias = "apiStyle",
alias = "api_style",
alias = "protocol",
alias = "wire_format",
alias = "wireFormat",
alias = "dialect"
)]
pub wire: Option<String>,
#[serde(alias = "authMode")]
pub auth_mode: Option<String>,
/// Validated basename of the active Codewhale-owned xAI OAuth generation.
@@ -5113,20 +5159,35 @@ impl Config {
let configured_base_url = provider_base
.or(root_base)
.or_else(|| provider_env_base_url_override(provider));
let entry = self.provider_config_for(provider);
let mode = entry.and_then(|e| e.mode.as_deref());
let wire = entry.and_then(|e| e.wire.as_deref());
let base = if provider == ApiProvider::XiaomiMimo {
let config_api_key = self
.provider_config_for(provider)
.and_then(|entry| entry.api_key.as_deref())
.filter(|value| {
classify_config_api_key_value(value) == ConfigApiKeyValueKind::Literal
});
let mode = self
.provider_config_for(provider)
.and_then(|entry| entry.mode.as_deref());
let config_api_key = entry.and_then(|e| e.api_key.as_deref()).filter(|value| {
classify_config_api_key_value(value) == ConfigApiKeyValueKind::Literal
});
let env_api_key =
xiaomi_mimo_env_api_key_for_runtime(mode, configured_base_url.as_deref());
let api_key = config_api_key.or(env_api_key.as_deref());
resolve_xiaomi_mimo_base_url(configured_base_url, api_key, mode)
} else if matches!(
provider,
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
| ApiProvider::ModelstudioCodingPlanAnthropic
) {
resolve_modelstudio_base_url_for_tui(configured_base_url, provider, mode, wire)
} else if matches!(
provider,
ApiProvider::Minimax | ApiProvider::MinimaxAnthropic
) {
resolve_minimax_base_url_for_tui(configured_base_url, provider, wire)
} else if matches!(
provider,
ApiProvider::Deepseek | ApiProvider::DeepseekAnthropic
) {
resolve_deepseek_base_url_for_tui(configured_base_url, provider, wire)
} else {
configured_base_url
.or_else(|| self.route_owned_generic_env_base_url(provider, identity))
@@ -5178,18 +5239,12 @@ impl Config {
ApiProvider::Meta => DEFAULT_META_BASE_URL,
ApiProvider::Xai => DEFAULT_XAI_BASE_URL,
ApiProvider::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
ApiProvider::ModelstudioTokenPlan => {
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
| ApiProvider::ModelstudioCodingPlanAnthropic => {
DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL
}
ApiProvider::ModelstudioTokenPlanAnthropic => {
MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL
}
ApiProvider::ModelstudioCodingPlan => {
DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL
}
ApiProvider::ModelstudioCodingPlanAnthropic => {
MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL
}
// No built-in endpoint; descriptor placeholder keeps the
// fallback total. A real custom route configures
// `[providers.<name>] base_url` which wins above (#1519).
@@ -8106,6 +8161,91 @@ fn xiaomi_mimo_env_api_key_for_runtime(
xiaomi_mimo_env_var(TOKEN_PLAN_ENV_VARS).or_else(|| xiaomi_mimo_env_var(STANDARD_ENV_VARS))
}
fn wire_config_prefers_anthropic(wire: Option<&str>) -> bool {
let Some(raw) = wire.map(str::trim).filter(|value| !value.is_empty()) else {
return false;
};
let normalized = raw.to_ascii_lowercase().replace(['_', ' '], "-");
matches!(
normalized.as_str(),
"anthropic"
| "anthropic-messages"
| "messages"
| "claude"
| "anthropic-compatible"
| "anthropic-compat"
)
}
fn modelstudio_mode_is_coding_plan(provider: ApiProvider, mode: Option<&str>) -> bool {
if matches!(
provider,
ApiProvider::ModelstudioCodingPlan | ApiProvider::ModelstudioCodingPlanAnthropic
) {
return true;
}
let Some(raw) = mode.map(str::trim).filter(|value| !value.is_empty()) else {
return false;
};
let normalized = raw.to_ascii_lowercase().replace(['_', ' '], "-");
matches!(
normalized.as_str(),
"coding-plan" | "coding" | "codingplan" | "dashscope-coding" | "code"
)
}
fn resolve_modelstudio_base_url_for_tui(
configured: Option<String>,
provider: ApiProvider,
mode: Option<&str>,
wire: Option<&str>,
) -> String {
if let Some(url) = configured.filter(|value| !value.trim().is_empty()) {
return url;
}
let coding = modelstudio_mode_is_coding_plan(provider, mode);
let anthropic = matches!(
provider,
ApiProvider::ModelstudioTokenPlanAnthropic | ApiProvider::ModelstudioCodingPlanAnthropic
) || wire_config_prefers_anthropic(wire);
match (coding, anthropic) {
(true, true) => MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL.to_string(),
(true, false) => DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL.to_string(),
(false, true) => MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL.to_string(),
(false, false) => DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL.to_string(),
}
}
fn resolve_minimax_base_url_for_tui(
configured: Option<String>,
provider: ApiProvider,
wire: Option<&str>,
) -> String {
if let Some(url) = configured.filter(|value| !value.trim().is_empty()) {
return url;
}
if matches!(provider, ApiProvider::MinimaxAnthropic) || wire_config_prefers_anthropic(wire) {
DEFAULT_MINIMAX_ANTHROPIC_BASE_URL.to_string()
} else {
DEFAULT_MINIMAX_BASE_URL.to_string()
}
}
fn resolve_deepseek_base_url_for_tui(
configured: Option<String>,
provider: ApiProvider,
wire: Option<&str>,
) -> String {
if let Some(url) = configured.filter(|value| !value.trim().is_empty()) {
return url;
}
if matches!(provider, ApiProvider::DeepseekAnthropic) || wire_config_prefers_anthropic(wire) {
DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL.to_string()
} else {
DEFAULT_DEEPSEEK_BASE_URL.to_string()
}
}
fn resolve_xiaomi_mimo_base_url(
configured: Option<String>,
api_key: Option<&str>,
@@ -8889,6 +9029,7 @@ fn merge_provider_config(base: ProviderConfig, override_cfg: ProviderConfig) ->
model: override_cfg.model.or(base.model),
context_window: override_cfg.context_window.or(base.context_window),
mode: override_cfg.mode.or(base.mode),
wire: override_cfg.wire.or(base.wire),
auth_mode: override_cfg.auth_mode.or(base.auth_mode),
oauth_credential_generation: override_cfg
.oauth_credential_generation
+21
View File
@@ -350,6 +350,14 @@ fn known_context_window_for_model(model_lower: &str) -> Option<u32> {
"minimax/minimax-m3" | "minimax-m3" | "qwen/qwen3.6-flash" | "qwen/qwen3.6-plus" => {
Some(1_000_000)
}
// Alibaba Cloud Model Studio (Token Plan console + curated catalog,
// verified 2026-08-03): ~1M context. Never fall through to the 128K
// legacy default — that number is the generation ceiling, not the window.
"qwen3.8-max"
| "qwen3.8-max-preview"
| "qwen3.7-plus"
| "qwen3.7-max"
| "qwen3.6-flash" => Some(1_000_000),
"nvidia/nemotron-3-ultra-550b-a55b" | "nvidia/nemotron-3-ultra-550b-a55b:free" => {
Some(1_000_000)
}
@@ -425,6 +433,9 @@ pub fn max_output_tokens_for_model(model: &str) -> Option<u32> {
| "qwen/qwen3.6-flash"
| "qwen/qwen3.6-max-preview"
| "qwen/qwen3.6-plus" => Some(65_536),
// Model Studio: 128K is the generation ceiling, not the context window.
"qwen3.8-max" | "qwen3.8-max-preview" => Some(131_072),
"qwen3.7-plus" | "qwen3.7-max" | "qwen3.6-flash" => Some(65_536),
"z-ai/glm-5.1" | "z-ai/glm-5.2" | "z-ai/glm-5-turbo" | "glm-5.1" | "glm-5.2"
| "glm-5-turbo" => Some(131_072),
"xiaomi/mimo-v2.5-pro"
@@ -892,6 +903,16 @@ mod tests {
assert!(model_supports_reasoning("muse-spark-1.1"));
}
#[test]
fn modelstudio_qwen38_max_is_1m_context_not_128k() {
// Owner Token Plan console + curated catalog (2026-08-03). The 128K
// figure is max output, not the window — never collapse them.
for model in ["qwen3.8-max", "qwen3.8-max-preview"] {
assert_eq!(context_window_for_model(model), Some(1_000_000), "{model}");
assert_eq!(max_output_tokens_for_model(model), Some(131_072), "{model}");
}
}
#[test]
fn model_metadata_catalog_override_flows_through_models_chokepoint() {
let _lock = crate::model_catalog::test_catalog_lock();
+19 -5
View File
@@ -1430,14 +1430,18 @@ impl ProviderPickerView {
// lost in the list.
let runtime_status = runtime_status.as_ref();
let custom_rows = custom_provider_dashboard_rows(active, config, runtime_status);
let mut rows: Vec<ProviderDashboardRow> = ApiProvider::all()
// Catalog surface = ProviderKind::ALL (one identity per vendor). Dual
// dialect / plan-variant kinds stay resolvable but are not separate
// rows; plan is mode/base_url and dialect is providers.<id>.wire.
let catalog_active = active.catalog_identity();
let mut rows: Vec<ProviderDashboardRow> = ApiProvider::catalog()
.iter()
.copied()
.filter(|provider| *provider != ApiProvider::Custom || custom_rows.is_empty())
.map(|p| {
ProviderDashboardRow::from_config_with_runtime_status(
p,
active,
catalog_active,
config,
runtime_status,
)
@@ -3869,10 +3873,20 @@ mod tests {
.map(|row| row.display_name.as_str())
.collect();
// Every built-in provider is present, none dropped (#3076 reorders, it
// does not filter).
assert_eq!(names.len(), ApiProvider::all().len());
// Catalog surface: one identity per vendor (not dual-wire / plan kinds).
assert_eq!(names.len(), ApiProvider::catalog().len());
assert!(names.contains(&"DeepSeek"));
assert!(names.contains(&"Alibaba Cloud Model Studio"));
// Dialect is wire config — no second MiniMax / Model Studio rows.
assert_eq!(
names
.iter()
.filter(|name| name.contains("Alibaba Cloud Model Studio"))
.count(),
1
);
assert_eq!(names.iter().filter(|name| **name == "MiniMax").count(), 1);
assert_eq!(names.iter().filter(|name| **name == "DeepSeek").count(), 1);
// Providers are presented in neutral case-insensitive alphabetical
// order by display name (#3076), not `ApiProvider::all()` order.