feat(config): preserve exact endpoint model declarations through runtime

Persist user-declared model labels, limits, capabilities and USD rate metadata
through the existing catalog/config, preserving literal wire identity and
unknown fields. Carry sourced limits and frozen estimates through selection,
reload, dispatch and subagents without granting provider capability authority.

Validation:
- config fixtures: 8 passed, 0 failed
- config route tests: 96 passed, 0 failed, 1 ignored
- config catalog tests: 25 passed, 0 failed
- TUI all test targets: cargo check passed; native execution deferred to
  coordinator's integrated native build window
- public-base resolver probe: expected failure, unknown vs declared context
- npm test: FAILED, missing script
- npm run check:web: FAILED, missing script
- git diff --check: passed

No provider calls, availability qualification, GUI, public push or release.
This commit is contained in:
CodeWhale Bot committed 2026-09-08 11:39:45 -07:00
1 parent 565bf625a9
commit c2d150e51a
27 files changed
+1969 -215

No files matched your search

+2
View File
@@ -44,6 +44,8 @@ use serde_json::Value;
use crate::models_dev::{ModelsDevCatalog, ModelsDevCost, ModelsDevLimit, ModelsDevModalities};
use crate::route::{ModelId, ProviderId, ProviderModelOffering, RouteLimits, WireModelId};
pub mod configured;
/// Provenance of a catalog row. Drives layer precedence and UI provenance.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
+197
View File
@@ -0,0 +1,197 @@
//! Persisted, operator-declared input to the existing catalog. These records
//! describe one exact route; they never create credentials or model aliases.
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Deserializer, Serialize};
use super::{CatalogOffering, CatalogSource, base_url_fingerprint};
use crate::models_dev::{ModelsDevCost, ModelsDevLimit, ModelsDevModalities};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConfiguredModel {
pub provider: String,
pub base_url: String,
/// Exact, case-sensitive wire identity. The label is never sent instead.
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_limit")]
pub limit: Option<ModelsDevLimit>,
/// USD per million tokens, using the same shape as the catalog.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_cost")]
pub cost: Option<ModelsDevCost>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_modalities")]
pub modalities: Option<ModelsDevModalities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attachment: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured_output: Option<bool>,
/// Keep future metadata intact through typed config saves.
#[serde(flatten)]
pub extras: BTreeMap<String, toml::Value>,
}
impl ConfiguredModel {
pub fn matches_route(&self, provider: &str, base_url: &str) -> bool {
!base_url.contains(['@', '?', '#'])
&& self.provider == provider
&& base_url_fingerprint(&self.base_url) == base_url_fingerprint(base_url)
}
/// Unknown fields stay unknown; no sibling, alias, or provider fact is
/// inherited. Source is assigned here and cannot be supplied by the file.
pub fn to_catalog_offering(&self) -> CatalogOffering {
CatalogOffering {
provider: self.provider.clone(),
wire_model_id: self.id.clone(),
endpoint_key: "chat".into(),
limit: self.limit.clone(),
cost: self.cost.clone(),
modalities: self.modalities.clone(),
attachment: self.attachment,
reasoning: self.reasoning,
tool_call: self.tool_call,
structured_output: self.structured_output,
source: CatalogSource::ConfigOverride,
..CatalogOffering::default()
}
}
}
pub fn validate_configured_models(models: &[ConfiguredModel]) -> anyhow::Result<()> {
let mut identities = BTreeSet::new();
for (index, model) in models.iter().enumerate() {
// Error messages name fields, never echo untrusted values or URLs.
let invalid = |field| anyhow::anyhow!("custom_models[{index}].{field} is invalid");
for (field, value) in [("provider", &model.provider), ("id", &model.id)] {
if value.is_empty()
|| value.len() > 256
|| value.chars().any(char::is_whitespace)
|| value.chars().any(char::is_control)
{
return Err(invalid(field));
}
}
if model.id.eq_ignore_ascii_case("auto") {
return Err(invalid("id"));
}
let Some((scheme, rest)) = model.base_url.split_once("://") else {
return Err(invalid("base_url"));
};
if !matches!(scheme.to_ascii_lowercase().as_str(), "http" | "https")
|| rest.split('/').next().is_none_or(str::is_empty)
|| model.base_url.contains(['@', '?', '#'])
|| model
.base_url
.chars()
.any(|c| c.is_whitespace() || c.is_control())
{
return Err(invalid("base_url"));
}
if model
.display_name
.as_ref()
.is_some_and(|name| name.trim().is_empty() || name.chars().any(char::is_control))
{
return Err(invalid("display_name"));
}
if let Some(limit) = &model.limit {
if [limit.context, limit.input, limit.output]
.into_iter()
.flatten()
.any(|value| value == 0 || value > u64::from(u32::MAX))
|| limit.context.is_some_and(|context| {
limit.input.is_some_and(|input| input > context)
|| limit.output.is_some_and(|output| output > context)
})
{
return Err(invalid("limit"));
}
}
if model
.cost
.as_ref()
.is_some_and(|cost| !crate::pricing::catalog_cost_is_valid(cost))
{
return Err(invalid("cost"));
}
if model.extras.keys().any(|key| {
matches!(
key.as_str(),
"source"
| "canonical_model"
| "aliases"
| "api_key"
| "auth"
| "headers"
| "endpoint_key"
| "default_for_provider"
)
}) {
return Err(invalid("metadata authority"));
}
if !identities.insert((
model.provider.clone(),
base_url_fingerprint(&model.base_url),
model.id.clone(),
)) {
return Err(invalid("duplicate route"));
}
}
Ok(())
}
pub fn deserialize_configured_models<'de, D>(
deserializer: D,
) -> Result<Option<Vec<ConfiguredModel>>, D::Error>
where
D: Deserializer<'de>,
{
let models = Option::<Vec<ConfiguredModel>>::deserialize(deserializer)?;
validate_configured_models(models.as_deref().unwrap_or_default())
.map_err(serde::de::Error::custom)?;
Ok(models)
}
// Nested units and limits have precise meanings. Reject unrecognized keys
// instead of silently dropping a currency, tier, or other pricing condition.
fn deserialize_known<'de, D: Deserializer<'de>, T: serde::de::DeserializeOwned>(
d: D,
fields: &[&str],
) -> Result<Option<T>, D::Error> {
let value = Option::<toml::Value>::deserialize(d)?;
value
.map(|value| {
if value
.as_table()
.is_none_or(|table| table.keys().any(|key| !fields.contains(&key.as_str())))
{
return Err(serde::de::Error::custom(
"unsupported nested custom model metadata field",
));
}
value
.try_into()
.map_err(|_| serde::de::Error::custom("invalid custom model metadata"))
})
.transpose()
}
fn deserialize_limit<'de, D: Deserializer<'de>>(d: D) -> Result<Option<ModelsDevLimit>, D::Error> {
deserialize_known(d, &["context", "input", "output"])
}
fn deserialize_cost<'de, D: Deserializer<'de>>(d: D) -> Result<Option<ModelsDevCost>, D::Error> {
deserialize_known(d, &["input", "output", "cache_read", "cache_write"])
}
fn deserialize_modalities<'de, D: Deserializer<'de>>(
d: D,
) -> Result<Option<ModelsDevModalities>, D::Error> {
deserialize_known(d, &["input", "output"])
}
+10
View File
@@ -894,6 +894,13 @@ pub struct ConfigToml {
pub tools: Option<ToolsToml>,
#[serde(default, skip_serializing_if = "ProvidersToml::is_empty")]
pub providers: ProvidersToml,
/// Operator declarations for exact provider/endpoint/model tuples.
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "catalog::configured::deserialize_configured_models"
)]
pub custom_models: Option<Vec<catalog::configured::ConfiguredModel>>,
/// Provider fallback chain (#2574). TUI runtime code may advance through
/// these providers after recoverable provider errors; config resolution
/// itself still reports the selected primary provider.
@@ -5430,6 +5437,9 @@ impl ConfigStore {
/// [`persistence::SetupTransaction`] alongside sibling files and keep the
/// comment-preserving write atomic with the rest of the transaction.
pub fn rendered_body(&self) -> Result<String> {
catalog::configured::validate_configured_models(
self.config.custom_models.as_deref().unwrap_or_default(),
)?;
let mut serialized =
toml::to_string_pretty(&self.config).context("failed to serialize config")?;
if let Some(provider_id) = self.config.named_custom_provider_id() {
+2
View File
@@ -88,6 +88,8 @@ pub enum LimitField {
pub enum OverrideSource {
/// Operator-configured context window.
UserContextWindow,
/// Limits declared by the operator for an exact model and endpoint.
UserModelMetadata,
/// Catalog limits describe the public API offering, not the account-scoped
/// Codex route; the API-only limits are stripped.
CodexPublicApiLimitStrip,
+154 -2
View File
@@ -68,6 +68,7 @@ pub struct RouteRequest {
#[derive(Debug, Clone)]
pub struct RouteResolver {
offerings: Vec<ProviderModelOffering>,
configured_offerings: Vec<(String, ProviderModelOffering)>,
}
/// Offering-owned facts selected within one provider scope before the final
@@ -136,7 +137,73 @@ impl RouteResolver {
/// changing route-resolution semantics.
#[must_use]
pub fn from_offerings(offerings: Vec<ProviderModelOffering>) -> Self {
Self { offerings }
Self {
offerings,
configured_offerings: Vec::new(),
}
}
/// Add validated operator declarations for this exact route. This does
/// not grant provider-catalog authority or establish model availability.
pub fn with_configured_models(
mut self,
models: &[crate::catalog::configured::ConfiguredModel],
identity: &str,
provider: ProviderKind,
base_url: &str,
) -> Self {
if crate::catalog::configured::validate_configured_models(models).is_err() {
return self;
}
for model in models
.iter()
.filter(|model| model.matches_route(identity, base_url))
{
let mut offering = model.to_catalog_offering().to_offering();
offering.provider = ProviderId::from(provider.as_str());
// Preserve an adapter's established protocol for an existing ID.
// The label/config declaration cannot choose a new wire dialect.
if let Some(existing) = self.offerings.iter().find(|row| {
row.provider == offering.provider && row.wire_model_id == offering.wire_model_id
}) {
offering.endpoint_key.clone_from(&existing.endpoint_key);
offering.default_for_provider = existing.default_for_provider;
} else if ProviderDescriptor::for_kind(provider).wire_policy() == WirePolicy::ModelAware
&& provider != ProviderKind::Deepseek
{
continue;
}
// Positive declarations are not verified executable capabilities.
// Explicit negatives can still restrict a route conservatively.
let declared = offering.capabilities;
offering.capabilities = RouteCapabilities {
attachments: if model.attachment == Some(false) {
declared.attachments
} else {
Default::default()
},
image_input: if declared.image_input == super::CapabilityState::Unsupported {
declared.image_input
} else {
Default::default()
},
reasoning: declared.reasoning,
native_tool_calls: if model.tool_call == Some(false) {
declared.native_tool_calls
} else {
Default::default()
},
structured_output: if model.structured_output == Some(false) {
declared.structured_output
} else {
Default::default()
},
..RouteCapabilities::default()
};
self.configured_offerings
.push((crate::catalog::base_url_fingerprint(base_url), offering));
}
self
}
/// Resolve a request into an executable route candidate.
@@ -167,6 +234,45 @@ impl RouteResolver {
&self,
req: &RouteRequest,
endpoint_catalog_authoritative: bool,
) -> Result<ReadyRouteCandidate, RouteError> {
if self.configured_offerings.is_empty() {
return self.resolve_scoped(req, endpoint_catalog_authoritative);
}
let mut scoped = self.clone();
scoped.configured_offerings.clear();
let provider = req.explicit_provider.unwrap_or_default();
let base_url = req
.base_url_override
.as_deref()
.unwrap_or_else(|| ProviderDescriptor::for_kind(provider).default_base_url());
let fingerprint = crate::catalog::base_url_fingerprint(base_url);
let selected_id = req
.model_selector
.as_ref()
.map(LogicalModelRef::raw)
.or_else(|| req.saved_provider_model.as_ref().map(WireModelId::as_str));
for (endpoint, offering) in &self.configured_offerings {
if !base_url.contains(['@', '?', '#'])
&& *endpoint == fingerprint
&& offering.provider.as_str() == provider.as_str()
&& selected_id == Some(offering.wire_model_id.as_str())
{
scoped.offerings.retain(|row| {
row.provider != offering.provider || row.wire_model_id != offering.wire_model_id
});
scoped.offerings.push(offering.clone());
scoped
.configured_offerings
.push((endpoint.clone(), offering.clone()));
}
}
scoped.resolve_scoped(req, endpoint_catalog_authoritative)
}
fn resolve_scoped(
&self,
req: &RouteRequest,
endpoint_catalog_authoritative: bool,
) -> Result<ReadyRouteCandidate, RouteError> {
// 1. Provider scope from explicit choice only; default otherwise.
// The provider is NEVER inferred from a model prefix.
@@ -271,6 +377,43 @@ impl RouteResolver {
selected.capabilities = RouteCapabilities::default();
selected.pricing = PricingSku::UnknownOrStale;
}
let base_url = req
.base_url_override
.as_deref()
.unwrap_or_else(|| descriptor.default_base_url());
let mut declared_limit_overrides = Vec::new();
if let Some((_, offering)) =
self.configured_offerings
.iter()
.find(|(fingerprint, offering)| {
!base_url.contains(['@', '?', '#'])
&& offering.provider == provider_id
&& offering.wire_model_id == selected.wire_model_id
&& req
.model_selector
.as_ref()
.map(LogicalModelRef::raw)
.or_else(|| req.saved_provider_model.as_ref().map(WireModelId::as_str))
== Some(offering.wire_model_id.as_str())
&& *fingerprint == crate::catalog::base_url_fingerprint(base_url)
})
{
selected.canonical_model = None;
selected.limits = offering.limits;
selected.capabilities = offering.capabilities;
selected.pricing = offering.pricing.clone();
for (field, value) in [
(LimitField::ContextTokens, offering.limits.context_tokens),
(LimitField::InputTokens, offering.limits.input_tokens),
(LimitField::OutputTokens, offering.limits.output_tokens),
] {
declared_limit_overrides.push(SourcedLimitOverride {
field,
value,
source: super::OverrideSource::UserModelMetadata,
});
}
}
if provider_kind == ProviderKind::Zai {
let effective_base_url = req
.base_url_override
@@ -351,7 +494,10 @@ impl RouteResolver {
// no offering was matched or the offering carried no price.
Some(selected.pricing),
validation,
req.limit_overrides.clone(),
declared_limit_overrides
.into_iter()
.chain(req.limit_overrides.iter().cloned())
.collect(),
))
}
@@ -382,6 +528,12 @@ impl RouteResolver {
.strip_prefix("opencode/")
.or_else(|| logical_model.raw().strip_prefix("opencode-zen/"))
.unwrap_or_else(|| logical_model.raw())
} else if self.configured_offerings.iter().any(|(_, offering)| {
offering.provider == *provider_id
&& offering.wire_model_id.as_str() == logical_model.raw()
}) {
// An explicitly declared wire ID is not a convenience selector.
logical_model.raw()
} else if provider_kind == ProviderKind::Concentrate {
// Concentrate's own namespace is not part of its wire ids.
// `concentrate/auto` is the explicit spelling for the gateway's
+292
View File
@@ -0,0 +1,292 @@
use codewhale_config::catalog::configured::{ConfiguredModel, validate_configured_models};
use codewhale_config::route::{
CapabilityState, LogicalModelRef, OverrideSource, RouteRequest, RouteResolver,
};
use codewhale_config::{ConfigStore, ConfigToml, ProviderKind};
const FIXTURE: &str = include_str!("fixtures/custom_models.toml");
const ID: &str = "deepseek-v4.1-flash-expires-on-0910";
const BASE: &str = "https://models.example.test/v1";
fn models() -> Vec<ConfiguredModel> {
toml::from_str::<ConfigToml>(FIXTURE)
.unwrap()
.custom_models
.unwrap()
}
fn request(base: &str, id: &str) -> RouteRequest {
RouteRequest {
explicit_provider: Some(ProviderKind::Deepseek),
model_selector: Some(LogicalModelRef::from(id)),
base_url_override: Some(base.into()),
..RouteRequest::default()
}
}
#[test]
fn persisted_model_roundtrip_reload_drives_immutable_route() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, FIXTURE).unwrap();
let mut store = ConfigStore::load(Some(path.clone())).unwrap();
let original = store.config.custom_models.clone();
store.config.telemetry = Some(true);
store.save().unwrap();
let mut reloaded = ConfigStore::load(Some(path)).unwrap();
assert_eq!(reloaded.config.custom_models, original);
let definitions = reloaded.config.custom_models.as_ref().unwrap();
assert_eq!(
definitions[0].display_name.as_deref(),
Some("Temporary preview")
);
assert_eq!(
definitions[0].extras["future_note"].as_str(),
Some("preserve this metadata")
);
let resolver = RouteResolver::new().with_configured_models(
definitions,
"deepseek",
ProviderKind::Deepseek,
BASE,
);
let old = resolver.resolve(&request(BASE, ID)).unwrap();
assert_eq!(old.wire_model_id().as_str(), ID);
assert_eq!(old.limits().context_tokens, Some(96000));
assert_eq!(old.limits().input_tokens, Some(88000));
assert_eq!(old.limits().output_tokens, Some(8000));
assert_eq!(old.capabilities().image_input, CapabilityState::Unknown);
assert_eq!(
old.capabilities().native_tool_calls,
CapabilityState::Unknown
);
assert_eq!(
old.capabilities().structured_output,
CapabilityState::Unsupported
);
assert!(
old.applied_limit_overrides()
.iter()
.all(|entry| entry.source == OverrideSource::UserModelMetadata)
);
reloaded.config.custom_models.as_mut().unwrap()[0]
.limit
.as_mut()
.unwrap()
.context = Some(128000);
reloaded.save().unwrap();
reloaded.reload().unwrap();
let next = RouteResolver::new()
.with_configured_models(
reloaded.config.custom_models.as_ref().unwrap(),
"deepseek",
ProviderKind::Deepseek,
BASE,
)
.resolve(&request(BASE, ID))
.unwrap();
assert_eq!(next.limits().context_tokens, Some(128000));
assert_eq!(old.limits().context_tokens, Some(96000));
}
#[test]
fn exact_identity_and_endpoint_do_not_leak_declarations() {
let resolver = RouteResolver::new().with_configured_models(
&models(),
"deepseek",
ProviderKind::Deepseek,
BASE,
);
for base in [
"https://other.example.test/v1",
"http://models.example.test/v1",
"https://models.example.test:444/v1",
"https://models.example.test/V1",
] {
let route = resolver.resolve(&request(base, ID)).unwrap();
assert_eq!(route.limits().context_tokens, None, "{base}");
}
for id in ["deepseek-v4.1-flash", "DEEPSEEK-V4.1-FLASH-EXPIRES-ON-0910"] {
assert_eq!(
resolver
.resolve(&request(BASE, id))
.unwrap()
.limits()
.context_tokens,
None,
"{id}"
);
}
assert_eq!(
resolver
.resolve(&request("https://MODELS.example.test:443/v1/", ID))
.unwrap()
.limits()
.context_tokens,
Some(96000)
);
let wrong_identity = RouteResolver::new().with_configured_models(
&models(),
"another-provider",
ProviderKind::Deepseek,
BASE,
);
assert_eq!(
wrong_identity
.resolve(&request(BASE, ID))
.unwrap()
.limits()
.context_tokens,
None
);
}
#[test]
fn unknown_fields_are_not_filled_from_a_known_sibling() {
let mut definitions = models();
definitions[0].limit = None;
definitions[0].cost = None;
definitions[0].reasoning = None;
definitions[0].modalities = None;
definitions[0].tool_call = None;
let offering = definitions[0].to_catalog_offering();
assert!(offering.limit.is_none());
assert!(offering.cost.is_none());
assert!(offering.reasoning.is_none());
let route = RouteResolver::new()
.with_configured_models(&definitions, "deepseek", ProviderKind::Deepseek, BASE)
.resolve(&request(BASE, ID))
.unwrap();
assert_eq!(route.limits().context_tokens, None);
assert_eq!(route.capabilities().reasoning, CapabilityState::Unknown);
assert_eq!(
route.capabilities().native_tool_calls,
CapabilityState::Unknown
);
}
#[test]
fn declarations_cannot_expand_closed_protocol_rosters() {
let mut definitions = models();
definitions[0].provider = "opencode-zen".into();
definitions[0].id = "unknown-protocol-model".into();
let resolver = RouteResolver::new().with_configured_models(
&definitions,
"opencode-zen",
ProviderKind::OpencodeZen,
BASE,
);
let mut req = request(BASE, "unknown-protocol-model");
req.explicit_provider = Some(ProviderKind::OpencodeZen);
assert!(resolver.resolve(&req).is_err());
}
#[test]
fn invalid_limits_prices_units_and_authority_fail_closed() {
for (before, after) in [
("context = 96000", "context = 0"),
("context = 96000", "context = 4294967296"),
("output = 8000", "output = 97000"),
("input = 0.4", "input = -1.0"),
("input = 0.4", "input = nan"),
("input = 0.4", "input = inf"),
("input = 0.4", "input = 0.4, currency = 'CNY'"),
("input = 0.4", "input = 0.4, unit = 'per_token'"),
("future_note =", "source ="),
("future_note =", "canonical_model ="),
("future_note =", "api_key ="),
(ID, "auto"),
(BASE, "https://user:[email protected]/v1"),
(BASE, "https://models.example.test/v1?key=secret"),
] {
assert!(
toml::from_str::<ConfigToml>(&FIXTURE.replace(before, after)).is_err(),
"{before} -> {after}"
);
}
let mut duplicate = models();
let mut other = duplicate[0].clone();
other.base_url = "https://MODELS.example.test:443/v1/".into();
duplicate.push(other);
assert!(validate_configured_models(&duplicate).is_err());
}
#[test]
fn project_config_cannot_replace_user_model_declarations() {
let mut user: ConfigToml = toml::from_str(FIXTURE).unwrap();
let project: ConfigToml =
toml::from_str(&FIXTURE.replace("context = 96000", "context = 256000")).unwrap();
user.merge_project_overrides(project);
assert_eq!(
user.custom_models.unwrap()[0]
.limit
.as_ref()
.unwrap()
.context,
Some(96000)
);
}
#[test]
fn declared_wire_ids_are_not_convenience_aliases() {
for (kind, provider, base, id) in [
(
ProviderKind::Together,
"together",
"https://api.together.xyz/v1",
"inkling",
),
(
ProviderKind::Openrouter,
"openrouter",
"https://openrouter.ai/api/v1",
"qwen3.7-plus",
),
(
ProviderKind::Concentrate,
"concentrate",
"https://api.concentrate.ai/v1",
"concentrate/example",
),
(
ProviderKind::Deepseek,
"deepseek",
"https://api.deepseek.com",
"deepseek-v4pro",
),
] {
let mut definitions = models();
definitions[0].provider = provider.into();
definitions[0].base_url = base.into();
definitions[0].id = id.into();
let resolver =
RouteResolver::new().with_configured_models(&definitions, provider, kind, base);
let mut req = request(base, id);
req.explicit_provider = Some(kind);
let candidate = resolver.resolve(&req).unwrap();
assert_eq!(candidate.wire_model_id().as_str(), id);
assert_eq!(candidate.limits().output_tokens, Some(8000));
req.base_url_override = Some("https://elsewhere.example.test/v1".into());
assert_eq!(resolver.resolve(&req).unwrap().limits().output_tokens, None);
}
}
#[test]
fn persisted_metadata_survives_unrelated_save() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, FIXTURE).unwrap();
let mut store = ConfigStore::load(Some(path.clone())).unwrap();
store.config.telemetry = Some(true);
store.save().unwrap();
let persisted: toml::Value = toml::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
assert_eq!(
persisted
.get("custom_models")
.and_then(|models| models.as_array())
.and_then(|models| models.first())
.and_then(|model| model.get("id"))
.and_then(|id| id.as_str()),
Some(ID)
);
}
+20
View File
@@ -0,0 +1,20 @@
provider = "deepseek"
default_text_model = "deepseek-v4.1-flash-expires-on-0910"
telemetry = false
[[custom_models]]
provider = "deepseek"
base_url = "https://models.example.test/v1"
id = "deepseek-v4.1-flash-expires-on-0910"
display_name = "Temporary preview"
reasoning = false
tool_call = true
attachment = true
structured_output = false
future_note = "preserve this metadata"
limit = { context = 96000, input = 88000, output = 8000 }
cost = { input = 0.4, output = 1.6, cache_read = 0.1 }
modalities = { input = ["text", "image"], output = ["text"] }
[providers.deepseek]
base_url = "https://models.example.test/v1"
+230 -43
View File
@@ -294,6 +294,7 @@ pub struct DeepSeekClient {
openrouter_vendor: Option<String>,
billing_surface: Option<String>,
billing_mode: crate::cost_status::RouteBillingMode,
configured_models: Arc<Vec<codewhale_config::catalog::configured::ConfiguredModel>>,
/// Non-secret limits frozen from the same resolved candidate as the
/// endpoint and wire model. Auxiliary calls carry only this client, so
/// they must not reconstruct output caps with `None` and discard a custom
@@ -581,6 +582,7 @@ impl Clone for DeepSeekClient {
openrouter_vendor: self.openrouter_vendor.clone(),
billing_surface: self.billing_surface.clone(),
billing_mode: self.billing_mode,
configured_models: Arc::clone(&self.configured_models),
route_limits: self.route_limits,
codex_account_id: self.codex_account_id.clone(),
wire_format: self.wire_format,
@@ -1577,6 +1579,7 @@ impl DeepSeekClient {
openrouter_vendor,
billing_surface,
billing_mode,
configured_models: Arc::new(config.custom_models.clone().unwrap_or_default()),
route_limits,
codex_account_id,
wire_format,
@@ -1677,6 +1680,58 @@ impl DeepSeekClient {
redact_model_bound_text(text, &self.model_bound_secret_values)
}
/// Alternate models share this client's frozen endpoint and declarations.
/// Resolution still owns protocol admission, including closed rosters.
pub(crate) fn resolve_model_route(&self, model: &str) -> Result<ReadyRouteCandidate> {
static RESOLVER: OnceLock<RouteResolver> = OnceLock::new();
let resolver = RESOLVER.get_or_init(RouteResolver::new);
let request = RouteRequest {
explicit_provider: self.api_provider.kind(),
model_selector: Some(LogicalModelRef::from(model)),
saved_provider_model: None,
base_url_override: Some(self.base_url.clone()),
limit_overrides: Vec::new(),
};
if self.configured_models.is_empty() || self.api_provider == ApiProvider::OpenaiCodex {
resolver.resolve(&request)
} else {
resolver
.clone()
.with_configured_models(
&self.configured_models,
&self.provider_identity,
self.api_provider.kind().unwrap_or_default(),
&self.base_url,
)
.resolve(&request)
}
.map_err(anyhow::Error::msg)
}
fn declared_wire_model<'a>(&self, model: &'a str) -> Option<&'a str> {
if self.api_provider == ApiProvider::OpenaiCodex
|| !self.configured_models.iter().any(|row| {
row.id == model && row.matches_route(&self.provider_identity, &self.base_url)
})
{
return None;
}
// A declaration cannot admit a new model to a closed protocol roster,
// or defeat a required protocol alias such as OpenCode Go's allowlist.
self.resolve_model_route(model)
.ok()
.filter(|candidate| candidate.wire_model_id().as_str() == model)
.map(|_| model)
}
fn wire_model_for_route(&self, model: &str) -> String {
self.declared_wire_model(model)
.map(str::to_string)
.unwrap_or_else(|| {
wire_model_for_provider_route(self.api_provider, &self.base_url, model)
})
}
/// Resolve `model` through the central route resolver and rebuild this
/// client whenever its exact wire identity, limits, or protocol differs
/// from the route bound at construction (#5042). `Ok(None)` means the
@@ -1693,17 +1748,7 @@ impl DeepSeekClient {
if model == self.default_model {
return Ok(None);
}
static RESOLVER: OnceLock<RouteResolver> = OnceLock::new();
let candidate = RESOLVER
.get_or_init(RouteResolver::new)
.resolve(&RouteRequest {
explicit_provider: self.api_provider.kind(),
model_selector: Some(LogicalModelRef::from(model)),
saved_provider_model: None,
base_url_override: Some(self.base_url.clone()),
limit_overrides: Vec::new(),
})
.map_err(anyhow::Error::msg)?;
let candidate = self.resolve_model_route(model)?;
let candidate_limits = crate::route_budget::known_route_limits(candidate.limits());
if candidate.protocol() == self.wire_format
&& candidate.wire_model_id().as_str() == self.default_model
@@ -1731,7 +1776,9 @@ impl DeepSeekClient {
self.wire_format
)
})?;
Self::from_candidate(config, &candidate).map(Some)
let mut rebound = Self::from_candidate(config, &candidate)?;
rebound.configured_models = Arc::clone(&self.configured_models);
Ok(Some(rebound))
}
fn bind_request_to_protocol(
@@ -1750,18 +1797,11 @@ impl DeepSeekClient {
return Ok((request, self.route_limits));
}
static RESOLVER: OnceLock<RouteResolver> = OnceLock::new();
let candidate = match RESOLVER
.get_or_init(RouteResolver::new)
.resolve(&RouteRequest {
explicit_provider: self.api_provider.kind(),
model_selector: Some(LogicalModelRef::from(request.model.as_str())),
saved_provider_model: None,
base_url_override: Some(self.base_url.clone()),
limit_overrides: Vec::new(),
}) {
let candidate = match self.resolve_model_route(&request.model) {
Ok(candidate) => candidate,
Err(error) if model_aware => return Err(anyhow::Error::msg(error)),
Err(error) if model_aware || self.api_provider == ApiProvider::OpencodeGo => {
return Err(error);
}
Err(_) => {
// A fixed-protocol gateway may legitimately accept an id that
// is newer than our offline catalog. Preserve the caller's
@@ -2357,6 +2397,7 @@ impl DeepSeekClient {
let (request, request_route_limits) =
self.bind_request_to_protocol(self.prepare_model_bound_request(request))?;
let mut request = clamp_output_cap(request, request_route_limits);
let declared_wire_model = self.declared_wire_model(&request.model).map(str::to_string);
if self.is_local_ds4_model(&request.model)
&& let Some(tools) = request.tools.as_mut()
{
@@ -2380,6 +2421,10 @@ impl DeepSeekClient {
&self.base_url,
stream,
)?;
if let Some(model) = &declared_wire_model {
wire.model.clone_from(model);
wire.body["model"] = json!(model);
}
self.apply_provider_routing(&mut wire.body);
let url = chat_completions_url(
self.chat_transport_base_url(),
@@ -2406,7 +2451,10 @@ impl DeepSeekClient {
.with_omitted_tool_names(wire.omitted_tool_names))
}
WireFormat::AnthropicMessages => {
let body = self.build_anthropic_body(&request, stream);
let mut body = self.build_anthropic_body(&request, stream);
if let Some(model) = &declared_wire_model {
body["model"] = json!(model);
}
let url = anthropic::anthropic_messages_url(&self.base_url);
let shape = if self.api_provider == ApiProvider::OpencodeZen {
RouteShape::OpencodeZen
@@ -2431,8 +2479,11 @@ impl DeepSeekClient {
))
}
WireFormat::Responses => {
let body =
let mut body =
responses::build_responses_body_for_provider(&request, self.api_provider);
if let Some(model) = &declared_wire_model {
body["model"] = json!(model);
}
let is_codex = self.api_provider == ApiProvider::OpenaiCodex;
let url = if is_codex {
format!("{}{}", self.base_url, responses::CODEX_RESPONSES_PATH)
@@ -2507,16 +2558,7 @@ impl DeepSeekClient {
let route_limits = if requested_model.trim() == self.default_model {
self.route_limits
} else {
static RESOLVER: OnceLock<RouteResolver> = OnceLock::new();
RESOLVER
.get_or_init(RouteResolver::new)
.resolve(&RouteRequest {
explicit_provider: self.api_provider.kind(),
model_selector: Some(LogicalModelRef::from(requested_model)),
saved_provider_model: None,
base_url_override: Some(self.base_url.clone()),
limit_overrides: Vec::new(),
})
self.resolve_model_route(requested_model)
.ok()
.and_then(|candidate| crate::route_budget::known_route_limits(candidate.limits()))
};
@@ -2529,8 +2571,7 @@ impl DeepSeekClient {
requested_model: &str,
route_limits: Option<RouteLimits>,
) -> u32 {
let wire_model =
wire_model_for_provider_route(self.api_provider, &self.base_url, requested_model);
let wire_model = self.wire_model_for_route(requested_model);
crate::route_budget::effective_max_output_tokens_for_route(
self.api_provider,
&wire_model,
@@ -2571,19 +2612,28 @@ impl DeepSeekClient {
requested_model: &str,
dispatched_at: chrono::DateTime<chrono::Utc>,
) -> crate::cost_status::EffectiveRouteEnvelope {
let model =
wire_model_for_provider_route(self.api_provider, &self.base_url, requested_model);
let model = self.wire_model_for_route(requested_model);
let endpoint_fingerprint = crate::cost_status::endpoint_fingerprint(&self.base_url);
let provider_live_pricing = u64::try_from(dispatched_at.timestamp())
.ok()
.and_then(|at| {
crate::provider_catalog_live::fresh_dispatch_pricing_quote_at(
crate::provider_catalog_live::configured_dispatch_pricing_quote_at(
&self.configured_models,
self.api_provider,
&self.provider_identity,
&model,
&self.base_url,
at,
)
.or_else(|| {
crate::provider_catalog_live::fresh_dispatch_pricing_quote_at(
self.api_provider,
&self.provider_identity,
&model,
&self.base_url,
at,
)
})
});
crate::cost_status::EffectiveRouteEnvelope {
openrouter_vendor: self.openrouter_vendor.clone(),
@@ -2693,7 +2743,7 @@ impl DeepSeekClient {
let route = self.effective_route_envelope(model, chrono::Utc::now());
let _inference = self.acquire_remote_control_inference_permit().await;
let _permit = self.acquire_provider_request_permit().await;
let model = wire_model_for_provider_route(self.api_provider, &self.base_url, model);
let model = self.wire_model_for_route(model);
let max_tokens = self.effective_max_output_tokens(&model);
if self.wire_format != WireFormat::ChatCompletions {
// Non-Chat dialects reuse the prepared-request seam so translation
@@ -3213,7 +3263,7 @@ impl DeepSeekClient {
}
let audio_format = normalize_audio_format(&request.audio_format);
let model = wire_model_for_provider_route(self.api_provider, &self.base_url, &model);
let model = self.wire_model_for_route(&model);
let model_lower = model.to_ascii_lowercase();
let instruction = request
.instruction
@@ -4917,7 +4967,7 @@ impl DeepSeekClient {
);
}
let url = api_url_with_suffix(&self.base_url, "beta/completions", None);
let model = wire_model_for_provider_route(self.api_provider, &self.base_url, model);
let model = self.wire_model_for_route(model);
let max_tokens = max_tokens.min(self.effective_max_output_tokens(&model));
let body = json!({
"model": model,
@@ -13612,6 +13662,143 @@ mod tests {
}
}
#[cfg(test)]
mod configured_model_client_tests {
use super::*;
use crate::config::{ProviderConfig, ProvidersConfig};
fn config() -> Config {
Config {
provider: Some("deepseek".into()),
providers: Some(ProvidersConfig {
deepseek: ProviderConfig {
api_key: Some("configured-model-local-fixture".into()),
base_url: Some("https://api.deepseek.com".into()),
..ProviderConfig::default()
},
..ProvidersConfig::default()
}),
custom_models: Some(vec![
toml::from_str(
r#"
provider = "deepseek"
base_url = "https://api.deepseek.com"
id = "deepseek-v4pro"
limit = { context = 1024, output = 32 }
cost = { input = 1.0, output = 2.0 }
"#,
)
.unwrap(),
]),
..Config::default()
}
}
#[test]
fn alternate_declared_model_freezes_wire_limits_and_price() {
let _env = crate::test_support::lock_test_env();
let mut config = config();
let client = DeepSeekClient::from_parts(
"https://api.deepseek.com".into(),
"initial-model".into(),
WireFormat::ChatCompletions,
None,
&config,
)
.unwrap();
let model = "deepseek-v4pro";
config.providers.as_mut().unwrap().deepseek.model = Some(model.into());
crate::config::normalize_model_config_for_test(&mut config);
assert_eq!(config.default_model(), model);
assert_eq!(
config.providers.as_ref().unwrap().deepseek.model.as_deref(),
Some(model)
);
config.custom_models.as_mut().unwrap()[0]
.limit
.as_mut()
.unwrap()
.output = Some(512);
config.custom_models.as_mut().unwrap()[0]
.cost
.as_mut()
.unwrap()
.output = Some(99.0);
assert_eq!(client.effective_max_output_tokens(model), 32);
let prepared = client
.prepare_outbound_request(
translation_message_request("hello", model.into(), "English", 4096),
false,
)
.unwrap();
assert_eq!(prepared.wire_model, model);
assert_eq!(prepared.body["model"], model);
assert_eq!(prepared.body["max_tokens"], 32);
let rebound = client
.rebound_for_model_protocol(Some(&config), model)
.unwrap()
.unwrap();
assert_eq!(rebound.default_model, model);
assert_eq!(rebound.route_limits.unwrap().output_tokens, Some(32));
let envelope = rebound.effective_route_envelope(model, chrono::Utc::now());
assert_eq!(envelope.model, model);
let quote = serde_json::to_value(envelope.provider_live_pricing.unwrap()).unwrap();
assert_eq!(quote["output_per_million"], "2");
let mut wrong_endpoint = client.clone();
wrong_endpoint.base_url = "https://other.example.test/v1".into();
assert_eq!(wrong_endpoint.declared_wire_model(model), None);
assert_ne!(wrong_endpoint.effective_max_output_tokens(model), 32);
let mut wrong_identity = client;
wrong_identity.provider_identity = "other-provider".into();
assert_eq!(wrong_identity.declared_wire_model(model), None);
}
#[test]
fn declaration_does_not_open_opencode_go_protocol_roster() {
let _env = crate::test_support::lock_test_env();
let mut config = config();
config.provider = Some("opencode-go".into());
config.providers.as_mut().unwrap().opencode_go = ProviderConfig {
api_key: Some("configured-model-local-fixture".into()),
..ProviderConfig::default()
};
let base_url = ApiProvider::OpencodeGo.default_base_url();
let declaration = &mut config.custom_models.as_mut().unwrap()[0];
declaration.provider = "opencode-go".into();
declaration.base_url = base_url.into();
declaration.id = "claude-sonnet-unproven".into();
let client = DeepSeekClient::from_parts(
base_url.into(),
config.default_model(),
WireFormat::ChatCompletions,
None,
&config,
)
.unwrap();
assert_eq!(client.declared_wire_model("claude-sonnet-unproven"), None);
assert!(
client
.rebound_for_model_protocol(None, "claude-sonnet-unproven")
.is_err()
);
assert!(
client
.prepare_outbound_request(
translation_message_request(
"hello",
"claude-sonnet-unproven".into(),
"English",
64
),
false,
)
.is_err()
);
}
}
#[cfg(test)]
mod openrouter_vendor_tests {
use super::*;
@@ -452,7 +452,8 @@ fn context_window_source_label(
tr(
locale,
match source {
crate::route_runtime::ContextWindowSource::Configured => {
crate::route_runtime::ContextWindowSource::Configured
| crate::route_runtime::ContextWindowSource::UserDeclared => {
MessageId::StatusContextSourceConfigured
}
crate::route_runtime::ContextWindowSource::ProviderReported => {
+28 -2
View File
@@ -300,7 +300,21 @@ pub fn model(app: &mut App, model_name: Option<&str>) -> CommandResult {
AppAction::UpdateCompaction(app.compaction_config()),
);
}
let model_id = if app.accepts_custom_model_ids() {
let declared = app.api_provider != ApiProvider::OpenaiCodex
&& codewhale_config::catalog::configured::validate_configured_models(
&app.configured_models,
)
.is_ok()
&& app.configured_models.iter().any(|model| {
model.id == name
&& model.matches_route(
app.provider_identity_for_persistence(),
&app.active_route_base_url,
)
});
let model_id = if declared {
name.to_string()
} else if app.accepts_custom_model_ids() {
let Some(model_id) = normalize_custom_model_id(name) else {
return CommandResult::error(format!(
"Invalid model '{name}'. Expected a non-empty model ID."
@@ -321,7 +335,19 @@ pub fn model(app: &mut App, model_name: Option<&str>) -> CommandResult {
app.api_provider,
ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::Zai
);
let route_resolution = if strict_direct_custom_endpoint {
let route_resolution = if declared {
match crate::route_runtime::resolve_declared_model_candidate(
app.api_provider,
app.provider_identity_for_persistence(),
&model_id,
&app.active_route_base_url,
app.active_context_window_override,
&app.configured_models,
) {
Ok(resolution) => Some(resolution),
Err(reason) => return CommandResult::error(reason),
}
} else if strict_direct_custom_endpoint {
None
} else {
// `/model` normally resolves against the active provider's
+49 -4
View File
@@ -101,7 +101,7 @@ fn cache_write_summary(app: &App, locale: Locale) -> String {
/// Show session cost breakdown.
///
/// The figure is an **estimate** computed from provider-reported usage and
/// published rates; it is never an invoice. Turns whose route produced no
/// the recorded pricing sources; it is never an invoice. Turns whose route produced no
/// authoritative price are missing from it entirely, so the coverage of the
/// number is reported alongside it rather than left implicit (#4318).
pub fn cost(app: &mut App) -> CommandResult {
@@ -116,7 +116,16 @@ pub fn cost(app: &mut App) -> CommandResult {
} else {
MessageId::CmdCostReport
};
let mut report = tr(locale, headline).replace("{cost}", &cost_report_amount(app, locale));
let mut report = if has_declared_estimates(app) {
// Like the diagnostic breakdown below, state the actual accounting
// basis without the legacy templates' published-rate assertion.
format!(
"Session cost estimate (priced subtotal): {}",
cost_report_amount(app, locale)
)
} else {
tr(locale, headline).replace("{cost}", &cost_report_amount(app, locale))
};
if priced > 0 || has_saved_legacy_subtotal {
report.push_str(&cost_breakdown_report(app));
}
@@ -268,6 +277,12 @@ fn joined(values: &std::collections::BTreeSet<String>) -> String {
.join(", ")
}
fn has_declared_estimates(app: &App) -> bool {
app.session
.cost_pricing_provenances
.contains("user_override")
}
/// The honesty block appended to `/cost` and `/tokens`: what the estimate covers
/// and what it cannot.
///
@@ -276,13 +291,23 @@ fn joined(values: &std::collections::BTreeSet<String>) -> String {
pub(crate) fn cost_coverage_report(app: &App, locale: Locale) -> String {
let (priced, unpriced) = cost_coverage_counts(app);
let mut out = String::from("\n\n");
out.push_str(&tr(locale, MessageId::CmdCostEstimateOnly));
let declared = has_declared_estimates(app);
if declared {
out.push_str("Includes user-declared, unverified price estimates calculated from recorded usage. These amounts do not establish provider prices, billing mode, or an invoice.");
} else {
out.push_str(&tr(locale, MessageId::CmdCostEstimateOnly));
}
out.push('\n');
if app.session.cost_coverage_unknown_legacy {
// A restored pre-coverage session has real money and no evidence of what
// it covers. Saying "0 of 0 priced" here would assert the total is
// complete, so the unknown state is stated instead.
out.push_str(&tr(locale, MessageId::CmdCostCoverageUnknownLegacy));
} else if declared {
out.push_str(&format!(
"Coverage: {priced} of {} tracked turns priced or estimated.",
priced.saturating_add(unpriced)
));
} else {
out.push_str(
&tr(locale, MessageId::CmdCostCoverage)
@@ -296,8 +321,14 @@ pub(crate) fn cost_coverage_report(app: &App, locale: Locale) -> String {
crate::pricing::CostCurrency::Cny => &app.session.cost_cny_unpriced_reasons,
};
out.push('\n');
let excluded = if declared {
"Excluded: {unpriced} turns have incomplete prices ({reasons}); their cost is unknown."
.to_string()
} else {
tr(locale, MessageId::CmdCostUnpricedTurns).to_string()
};
out.push_str(
&tr(locale, MessageId::CmdCostUnpricedTurns)
&excluded
.replace("{unpriced}", &unpriced.to_string())
.replace(
"{reasons}",
@@ -488,6 +519,20 @@ mod cost_breakdown_tests {
}
}
#[test]
fn configured_model_cost_copy_does_not_claim_published_rates() {
let _env = crate::test_support::lock_test_env();
let mut app = test_app();
app.session
.cost_pricing_provenances
.insert("user_override".into());
app.session.cost_priced_turns = 1;
let report = cost_coverage_report(&app, Locale::En);
assert!(report.contains("user-declared, unverified price estimates"));
assert!(!report.contains("published rates"));
assert!(!report.contains("money-metered"));
}
/// The decomposition's terms are exactly the headline's inputs, so their
/// sum reproduces the headline — including when the #244 monotonic floor,
/// not the live accumulators, is the number on display (#4939).
+71 -23
View File
@@ -803,11 +803,10 @@ pub fn provider_capability_with_wire(
} else {
None
};
let is_v4_pro = model_lower.contains("v4-pro") || model_lower == "deepseek-v4pro";
let is_v4_flash = model_lower.contains("v4-flash")
|| model_lower == "deepseek-v4flash"
|| model_lower == "deepseek-v4"
|| alias_deprecation.is_some();
let exact_deepseek = canonical_official_deepseek_model_id(&model_lower);
let is_v4_pro = exact_deepseek == Some("deepseek-v4-pro");
let is_v4_flash =
exact_deepseek == Some(DEEPSEEK_ALIAS_REPLACEMENT) || alias_deprecation.is_some();
let is_reasoner = matches!(provider, ApiProvider::WanjieArk)
&& (model_lower.contains("reasoner") || model_lower.contains("r1"));
@@ -1057,23 +1056,7 @@ pub fn validate_route(provider: ApiProvider, model: &str) -> Result<(), String>
Ok(())
}
fn canonical_official_deepseek_model_id(model: &str) -> Option<&'static str> {
match model.trim().to_ascii_lowercase().as_str() {
"deepseek-v4-pro"
| "deepseek-v4pro"
| "deepseek-ai/deepseek-v4-pro"
| "deepseek-ai/deepseek-v4pro"
| "deepseek/deepseek-v4-pro"
| "deepseek/deepseek-v4pro" => Some("deepseek-v4-pro"),
"deepseek-v4-flash"
| "deepseek-v4flash"
| "deepseek-ai/deepseek-v4-flash"
| "deepseek-ai/deepseek-v4flash"
| "deepseek/deepseek-v4-flash"
| "deepseek/deepseek-v4flash" => Some("deepseek-v4-flash"),
_ => None,
}
}
use crate::models::canonical_official_deepseek_model_id;
/// Resolve model names accepted by DeepSeek's first-party endpoints.
///
@@ -2822,6 +2805,12 @@ impl TranscriptConfig {
/// Resolved CLI configuration, including defaults and environment overrides.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Config {
/// Persisted exact-route declarations, separate from provider credentials.
#[serde(
default,
deserialize_with = "codewhale_config::catalog::configured::deserialize_configured_models"
)]
pub custom_models: Option<Vec<codewhale_config::catalog::configured::ConfiguredModel>>,
/// Single-token inputs that cancel the active turn before dispatch.
#[serde(default)]
pub stop_words: Option<Vec<String>>,
@@ -4858,6 +4847,9 @@ impl Config {
/// Validate that critical config fields are present.
pub fn validate(&self) -> Result<()> {
codewhale_config::catalog::configured::validate_configured_models(
self.custom_models.as_deref().unwrap_or_default(),
)?;
self.openrouter_vendor()?;
if self
.provider
@@ -5842,6 +5834,7 @@ impl Config {
/// controls. Provider tables carry their endpoint, auth, headers, TLS,
/// model-passthrough, and per-route limits as one atomic registry.
pub(crate) fn refresh_provider_routes_from(&mut self, fresh: &Self) {
self.custom_models.clone_from(&fresh.custom_models);
self.provider.clone_from(&fresh.provider);
self.api_key.clone_from(&fresh.api_key);
self.base_url.clone_from(&fresh.base_url);
@@ -6002,11 +5995,22 @@ impl Config {
#[must_use]
pub fn default_model(&self) -> String {
let provider = self.api_provider();
let declared = |model: &str| {
crate::provider_lake::configured_model_for_route(
self,
provider,
&self.provider_identity_for(provider),
&self.deepseek_base_url(),
model,
)
.is_some()
};
if let Some(model) =
self.provider_config_string_with_runtime_fallback(provider, |entry| entry.model.clone())
{
let model = model.trim();
if provider_passes_model_through(provider)
if declared(model)
|| provider_passes_model_through(provider)
|| self.active_provider_preserves_custom_base_url_model()
{
return model.to_string();
@@ -6025,6 +6029,13 @@ impl Config {
return model.to_string();
}
}
if let Some(model) = self
.default_text_model
.as_deref()
.filter(|model| declared(model))
{
return model.to_string();
}
let moonshot_config = (provider == ApiProvider::Moonshot)
.then(|| self.provider_config())
.flatten();
@@ -9623,12 +9634,34 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
fn normalize_model_config(config: &mut Config) {
let provider = config.api_provider();
let base_url = config.deepseek_base_url();
let mut declared = Vec::new();
for provider in ApiProvider::all()
.iter()
.copied()
.chain([ApiProvider::DeepseekCN])
{
for model in config.custom_models.as_deref().unwrap_or_default() {
if crate::provider_lake::configured_model_for_route(
config,
provider,
&config.provider_identity_for(provider),
&config.base_url_for_route(provider),
&model.id,
)
.is_some()
{
declared.push((provider, model.id.clone()));
}
}
}
let is_declared = |provider, model: &str| declared.contains(&(provider, model.to_string()));
config.migrated_deepseek_model_alias = if matches!(
provider,
ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::DeepseekAnthropic
) {
config
.active_configured_model_id()
.filter(|model| !is_declared(provider, model))
.map(str::to_ascii_lowercase)
.filter(|model| deepseek_alias_deprecation(model).is_some())
.filter(|model| {
@@ -9654,6 +9687,7 @@ fn normalize_model_config(config: &mut Config) {
}
if let Some(model) = config.default_text_model.as_deref()
&& !is_declared(provider, model)
&& !provider_passes_model_through(config.api_provider())
&& !config.active_provider_preserves_custom_base_url_model()
&& let Some(normalized) = normalize_model_for_provider(config.api_provider(), model)
@@ -9664,12 +9698,14 @@ fn normalize_model_config(config: &mut Config) {
if let Some(providers) = config.providers.as_mut() {
if let Some(model) = providers.deepseek.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::Deepseek, &providers.deepseek)
&& !is_declared(ApiProvider::Deepseek, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Deepseek, model)
{
providers.deepseek.model = Some(normalized);
}
if let Some(model) = providers.deepseek_cn.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::DeepseekCN, &providers.deepseek_cn)
&& !is_declared(ApiProvider::DeepseekCN, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::DeepseekCN, model)
{
providers.deepseek_cn.model = Some(normalized);
@@ -9679,6 +9715,7 @@ fn normalize_model_config(config: &mut Config) {
ApiProvider::DeepseekAnthropic,
&providers.deepseek_anthropic,
)
&& !is_declared(ApiProvider::DeepseekAnthropic, model)
&& let Some(normalized) =
normalize_model_for_provider(ApiProvider::DeepseekAnthropic, model)
{
@@ -9686,24 +9723,28 @@ fn normalize_model_config(config: &mut Config) {
}
if let Some(model) = providers.nvidia_nim.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::NvidiaNim, &providers.nvidia_nim)
&& !is_declared(ApiProvider::NvidiaNim, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::NvidiaNim, model)
{
providers.nvidia_nim.model = Some(normalized);
}
if let Some(model) = providers.openrouter.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::Openrouter, &providers.openrouter)
&& !is_declared(ApiProvider::Openrouter, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Openrouter, model)
{
providers.openrouter.model = Some(normalized);
}
if let Some(model) = providers.novita.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::Novita, &providers.novita)
&& !is_declared(ApiProvider::Novita, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Novita, model)
{
providers.novita.model = Some(normalized);
}
if let Some(model) = providers.fireworks.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::Fireworks, &providers.fireworks)
&& !is_declared(ApiProvider::Fireworks, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Fireworks, model)
{
providers.fireworks.model = Some(normalized);
@@ -9713,6 +9754,7 @@ fn normalize_model_config(config: &mut Config) {
ApiProvider::Siliconflow,
&providers.siliconflow,
)
&& !is_declared(ApiProvider::Siliconflow, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Siliconflow, model)
{
providers.siliconflow.model = Some(normalized);
@@ -9722,6 +9764,7 @@ fn normalize_model_config(config: &mut Config) {
ApiProvider::SiliconflowCn,
&providers.siliconflow_cn,
)
&& !is_declared(ApiProvider::SiliconflowCn, model)
&& let Some(normalized) =
normalize_model_for_provider(ApiProvider::SiliconflowCn, model)
{
@@ -9729,24 +9772,28 @@ fn normalize_model_config(config: &mut Config) {
}
if let Some(model) = providers.moonshot.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::Moonshot, &providers.moonshot)
&& !is_declared(ApiProvider::Moonshot, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Moonshot, model)
{
providers.moonshot.model = Some(normalized);
}
if let Some(model) = providers.sglang.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::Sglang, &providers.sglang)
&& !is_declared(ApiProvider::Sglang, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Sglang, model)
{
providers.sglang.model = Some(normalized);
}
if let Some(model) = providers.vllm.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::Vllm, &providers.vllm)
&& !is_declared(ApiProvider::Vllm, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Vllm, model)
{
providers.vllm.model = Some(normalized);
}
if let Some(model) = providers.deepinfra.model.as_deref()
&& !provider_entry_uses_custom_base_url(ApiProvider::Deepinfra, &providers.deepinfra)
&& !is_declared(ApiProvider::Deepinfra, model)
&& let Some(normalized) = normalize_model_for_provider(ApiProvider::Deepinfra, model)
{
providers.deepinfra.model = Some(normalized);
@@ -10678,6 +10725,7 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
// Captured before the struct literal moves the field out of `override_cfg`.
let override_defines_root_base_url = override_cfg.base_url.is_some();
Config {
custom_models: override_cfg.custom_models.or(base.custom_models),
provider: override_cfg.provider.or(base.provider),
telemetry: override_cfg.telemetry.or(base.telemetry),
api_key: override_cfg.api_key.or(base.api_key),
+152 -8
View File
@@ -194,13 +194,26 @@ impl EffectiveRouteEnvelope {
u64::try_from(dispatched_at.timestamp())
.ok()
.and_then(|at| {
crate::provider_catalog_live::fresh_dispatch_pricing_quote_at(
provider,
&provider_identity,
&model,
base_url,
at,
)
config
.and_then(|config| {
crate::provider_catalog_live::configured_dispatch_pricing_quote_at(
config.custom_models.as_deref().unwrap_or_default(),
provider,
&provider_identity,
&model,
base_url,
at,
)
})
.or_else(|| {
crate::provider_catalog_live::fresh_dispatch_pricing_quote_at(
provider,
&provider_identity,
&model,
base_url,
at,
)
})
})
});
Self {
@@ -232,11 +245,29 @@ impl EffectiveRouteEnvelope {
Some(&self.provider_identity),
self.endpoint_fingerprint.as_deref(),
);
let declared_estimate = self.provider_live_pricing.as_ref().is_some_and(|quote| {
quote.provenance == codewhale_config::pricing::PricingProvenance::UserOverride
&& self
.endpoint_fingerprint
.as_deref()
.zip(u64::try_from(self.dispatched_at.timestamp()).ok())
.is_some_and(|(fingerprint, at)| {
quote
.pricing_for_route(
self.provider,
&self.provider_identity,
&self.model,
fingerprint,
at,
)
.is_some()
})
});
match self.billing_mode {
RouteBillingMode::Subscription | RouteBillingMode::Local => {
return TurnCostAudit::unpriced(crate::pricing::UnpricedReason::NotMoneyMetered);
}
RouteBillingMode::Unknown if !reviewed_custom_metered => {
RouteBillingMode::Unknown if !reviewed_custom_metered && !declared_estimate => {
return TurnCostAudit::unpriced(
crate::pricing::UnpricedReason::UnknownBillingBasis,
);
@@ -2100,6 +2131,119 @@ pub(crate) fn test_scope() -> TestCostScope {
mod tests {
use super::*;
fn configured_fixture_receipt() -> (crate::config::Config, EffectiveRouteEnvelope, Usage) {
let config = toml::from_str(include_str!(
"../../config/tests/fixtures/custom_models.toml"
))
.unwrap();
let receipt = EffectiveRouteEnvelope::capture(
Some(&config),
ApiProvider::Deepseek,
"deepseek",
"deepseek-v4.1-flash-expires-on-0910",
Some("https://models.example.test/v1"),
Utc::now(),
);
let usage = Usage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Usage::default()
};
(config, receipt, usage)
}
#[test]
fn configured_model_estimate_is_frozen_and_exactly_bound() {
let (mut config, receipt, usage) = configured_fixture_receipt();
let audit = receipt.audit(&usage);
assert_eq!(
audit.provenance,
Some(codewhale_config::pricing::PricingProvenance::UserOverride)
);
assert!((audit.estimate.unwrap().usd - 2.0).abs() < 1e-12);
let frozen: EffectiveRouteEnvelope =
serde_json::from_str(&serde_json::to_string(&receipt).unwrap()).unwrap();
config.custom_models.as_mut().unwrap()[0]
.cost
.as_mut()
.unwrap()
.input = Some(9.0);
assert!((frozen.audit(&usage).estimate.unwrap().usd - 2.0).abs() < 1e-12);
for (field, value) in [
("model", "deepseek-v4.1-flash"),
("identity", "other-provider"),
("endpoint", "https://other.example.test/v1"),
] {
let mut wrong = frozen.clone();
match field {
"model" => wrong.model = value.into(),
"identity" => wrong.provider_identity = value.into(),
_ => wrong.endpoint_fingerprint = endpoint_fingerprint(value),
}
assert!(wrong.audit(&usage).estimate.is_none(), "{field}");
}
for billing in [RouteBillingMode::Local, RouteBillingMode::Subscription] {
let mut nonmoney = frozen.clone();
nonmoney.billing_mode = billing;
assert_eq!(
nonmoney.audit(&usage).unpriced_reason,
Some(crate::pricing::UnpricedReason::NotMoneyMetered)
);
}
let mut cached = usage.clone();
cached.prompt_cache_write_tokens = Some(500);
assert!(frozen.audit(&cached).estimate.is_none());
}
#[test]
fn configured_model_missing_prices_stay_unknown_and_vendor_pin_still_wins() {
let (mut config, receipt, usage) = configured_fixture_receipt();
config.custom_models.as_mut().unwrap()[0].cost = None;
let unknown = EffectiveRouteEnvelope::capture(
Some(&config),
receipt.provider,
receipt.provider_identity.clone(),
receipt.model.clone(),
Some("https://models.example.test/v1"),
receipt.dispatched_at,
);
assert!(unknown.provider_live_pricing.is_some());
assert!(unknown.audit(&usage).estimate.is_none());
let mut pinned = receipt;
pinned.provider = ApiProvider::Openrouter;
pinned.openrouter_vendor = Some("exact-upstream".into());
pinned.billing_mode = RouteBillingMode::Metered;
assert_eq!(
pinned.audit(&usage).unpriced_reason,
Some(crate::pricing::UnpricedReason::RoutingDependentPrice)
);
}
#[test]
fn configured_model_client_keeps_its_metadata_snapshot_after_reload() {
let (mut config, _, usage) = configured_fixture_receipt();
config.api_key = Some("fixture-not-a-provider-credential".into());
let id = "deepseek-v4.1-flash-expires-on-0910";
let route =
crate::route_runtime::resolve_runtime_route(&config, ApiProvider::Deepseek, Some(id))
.unwrap();
let client =
crate::client::DeepSeekClient::from_candidate(&config, &route.candidate).unwrap();
config.custom_models.as_mut().unwrap()[0]
.cost
.as_mut()
.unwrap()
.input = Some(9.0);
let envelope = client.effective_route_envelope(id, Utc::now());
assert!((envelope.audit(&usage).estimate.unwrap().usd - 2.0).abs() < 1e-12);
assert_eq!(
client
.effective_route_envelope("other-model", Utc::now())
.provider_live_pricing,
None
);
}
struct ProviderCatalogTestReset;
impl Drop for ProviderCatalogTestReset {
+46 -11
View File
@@ -102,6 +102,20 @@ impl ModelInventory {
for model in models_for_provider(config, active_provider, provider) {
push_model(&mut models, provider, &model);
}
for declaration in config.custom_models.as_deref().unwrap_or_default() {
if crate::provider_lake::configured_model_for_route(
config,
provider,
&config.provider_identity_for(provider),
&config.base_url_for_route(provider),
&declaration.id,
)
.is_some()
&& !models.contains(&declaration.id)
{
models.push(declaration.id.clone());
}
}
if models.is_empty() {
push_model(&mut models, provider, &default_model);
}
@@ -122,7 +136,7 @@ impl ModelInventory {
{
if let Some(context_window) = route.candidate.limits().context_tokens {
capability.context_window = context_window.min(u64::from(u32::MAX)) as u32;
context_window_unverified = false;
context_window_unverified = !route.context_window.source.is_verified();
}
// A concrete offering maximum is a stronger fact than the
// static compatibility matrix — and is the only way a
@@ -136,14 +150,36 @@ impl ModelInventory {
{
capability.max_output = Some(max_output);
}
let user_declared =
route
.candidate
.applied_limit_overrides()
.iter()
.any(|entry| {
entry.source
== codewhale_config::route::OverrideSource::UserModelMetadata
});
if user_declared {
context_window_unverified = !route.context_window.source.is_verified();
capability.context_window = route.context_window.tokens;
capability.max_output = route
.candidate
.limits()
.output_tokens
.and_then(|value| u32::try_from(value).ok());
capability.thinking_supported = route.candidate.capabilities().reasoning
== codewhale_config::route::CapabilityState::Supported;
}
// Do not promote bare `k3` into the global capability
// catalog. Its thinking trace contract belongs only to
// Kimi Code's exact membership-plan route.
if crate::config::is_exact_kimi_code_k3_route(
provider,
&route.candidate.endpoint().base_url,
route.candidate.wire_model_id().as_str(),
) {
if !user_declared
&& crate::config::is_exact_kimi_code_k3_route(
provider,
&route.candidate.endpoint().base_url,
route.candidate.wire_model_id().as_str(),
)
{
capability.thinking_supported = true;
}
}
@@ -162,8 +198,7 @@ impl ModelInventory {
}
// Unready routes stay visible (annotated) so an operator can
// override explicitly, but they are never a silent default.
let default_for_provider =
readiness.can_attempt() && model.eq_ignore_ascii_case(&default_model);
let default_for_provider = readiness.can_attempt() && model == default_model;
if default_for_provider {
tags.push("default");
}
@@ -251,9 +286,9 @@ impl ModelInventory {
provider: ApiProvider,
model: &str,
) -> Option<&ModelRouteCandidate> {
self.candidates.iter().find(|candidate| {
candidate.provider == provider && candidate.model.eq_ignore_ascii_case(model.trim())
})
self.candidates
.iter()
.find(|candidate| candidate.provider == provider && candidate.model == model.trim())
}
pub(crate) fn active_default(&self) -> Option<&ModelRouteCandidate> {
+35 -29
View File
@@ -36,6 +36,24 @@ pub const DIRECT_KIMI_K3_MAX_OUTPUT_TOKENS: u32 = 1_048_576;
/// models resolve to their own scaled value via
/// `compaction_threshold_for_model` (#664).
pub const DEFAULT_COMPACTION_TOKEN_THRESHOLD: usize = 102_400;
pub(crate) fn canonical_official_deepseek_model_id(model: &str) -> Option<&'static str> {
match model.trim().to_ascii_lowercase().as_str() {
"deepseek-v4-pro"
| "deepseek-v4pro"
| "deepseek-ai/deepseek-v4-pro"
| "deepseek-ai/deepseek-v4pro"
| "deepseek/deepseek-v4-pro"
| "deepseek/deepseek-v4pro" => Some("deepseek-v4-pro"),
"deepseek-v4-flash"
| "deepseek-v4flash"
| "deepseek-ai/deepseek-v4-flash"
| "deepseek-ai/deepseek-v4flash"
| "deepseek/deepseek-v4-flash"
| "deepseek/deepseek-v4flash" => Some("deepseek-v4-flash"),
_ => None,
}
}
#[cfg(test)]
const COMPACTION_THRESHOLD_PERCENT: u32 = 80;
@@ -175,15 +193,12 @@ pub fn context_window_for_model(model: &str) -> Option<u32> {
if let Some(window) = known_context_window_for_model(&lower) {
return Some(window);
}
if canonical_official_deepseek_model_id(&lower).is_some() {
return Some(DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS);
}
if let Some(explicit_window) = explicit_context_window_hint(&lower) {
return Some(explicit_window);
}
if lower.contains("deepseek") {
if lower.contains("v4") {
return Some(DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS);
}
return Some(LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS);
}
if is_openai_gpt_55_api_model(&lower) || is_openai_gpt_56_api_model(&lower) {
return Some(1_050_000);
}
@@ -437,7 +452,7 @@ pub fn model_supports_reasoning(model: &str) -> bool {
return supports_reasoning;
}
let lower = model.to_lowercase();
if lower.contains("deepseek") && lower.contains("v4") {
if canonical_official_deepseek_model_id(&lower).is_some() {
return true;
}
// #3016 plus the 2026 Kimi Code K2.7 update: Moonshot-native Kimi IDs,
@@ -860,28 +875,19 @@ mod tests {
}
#[test]
fn v4_snapshots_preserve_context_window() {
// v-series snapshots get 1M context since they contain "v4"
assert_eq!(
context_window_for_model("deepseek-v4-flash-20260423"),
Some(DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS)
);
assert_eq!(
context_window_for_model("deepseek-v4-pro-20260423"),
Some(DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS)
);
}
#[test]
fn unknown_legacy_deepseek_models_map_to_128k_context_window() {
assert_eq!(
context_window_for_model("deepseek-coder"),
Some(LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS)
);
assert_eq!(
context_window_for_model("deepseek-v3.2-0324"),
Some(LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS)
);
fn unrecognized_deepseek_models_do_not_inherit_sibling_metadata() {
for model in [
"deepseek-v4-flash-20260423",
"deepseek-v4-pro-20260423",
"deepseek-coder",
"deepseek-v3.2-0324",
"deepseek-v4.1-flash-expires-on-0910",
] {
assert_eq!(context_window_for_model(model), None, "{model}");
}
assert!(!model_supports_reasoning(
"deepseek-v4.1-flash-expires-on-0910"
));
}
#[test]
+23 -1
View File
@@ -1929,6 +1929,21 @@ pub(crate) fn audit_turn_cost_for_route_on_endpoint_for_identity_at(
usage: &Usage,
recorded_at: DateTime<Utc>,
) -> TurnCostAudit {
let declared_pricing = match provider_live_pricing {
Some(quote) if quote.provenance == PricingProvenance::UserOverride => {
let pricing = provider_identity
.zip(endpoint_fingerprint)
.zip(u64::try_from(recorded_at.timestamp()).ok())
.and_then(|((identity, fingerprint), at)| {
quote.pricing_for_route(provider, identity, model, fingerprint, at)
});
let Some(pricing) = pricing else {
return TurnCostAudit::unpriced(UnpricedReason::UnverifiedLivePricing);
};
Some(pricing)
}
_ => None,
};
let reviewed_custom_metered =
reviewed_custom_route_is_metered(provider, provider_identity, endpoint_fingerprint);
let reviewed_provider_live =
@@ -1940,7 +1955,11 @@ pub(crate) fn audit_turn_cost_for_route_on_endpoint_for_identity_at(
EndpointMetering::ExactSubscription | EndpointMetering::LocalNoBill => {
return TurnCostAudit::unpriced(UnpricedReason::NotMoneyMetered);
}
EndpointMetering::Unknown if billing_surface.is_some() && !reviewed_custom_metered => {
EndpointMetering::Unknown
if billing_surface.is_some()
&& !reviewed_custom_metered
&& declared_pricing.is_none() =>
{
return TurnCostAudit::unpriced(UnpricedReason::UnknownBillingBasis);
}
EndpointMetering::Unknown | EndpointMetering::Money => {}
@@ -1948,6 +1967,9 @@ pub(crate) fn audit_turn_cost_for_route_on_endpoint_for_identity_at(
if !usage_cache_partition_is_consistent(usage) {
return TurnCostAudit::unpriced(UnpricedReason::InconsistentUsage);
}
if let Some(pricing) = declared_pricing {
return audit_offering_pricing(pricing, usage);
}
if provider == ApiProvider::Stepfun {
return match pricing_for_billing_surface(provider, model, billing_surface) {
// StepFun's hand row publishes no cache-write rate, so a turn that
+46 -1
View File
@@ -350,6 +350,7 @@ impl ProviderLivePricingQuote {
return None;
}
match (&self.provenance, &self.cloud_facts) {
(PricingProvenance::UserOverride, None) => {}
(PricingProvenance::ProviderLive, None)
if dispatched_at_unix.saturating_sub(self.catalog_fetched_at)
< DEFAULT_PROVIDER_CATALOG_TTL_SECS
@@ -388,13 +389,16 @@ impl ProviderLivePricingQuote {
}
// A reviewed per-token route needs both ordinary request classes. Cache
// classes remain optional and fail closed later if a turn used them.
if self.cloud_facts.is_none() && (cost.input.is_none() || cost.output.is_none()) {
if self.provenance == PricingProvenance::ProviderLive
&& (cost.input.is_none() || cost.output.is_none())
{
return None;
}
if cost.input.is_none()
&& cost.output.is_none()
&& cost.cache_read.is_none()
&& cost.cache_write.is_none()
&& self.provenance != PricingProvenance::UserOverride
{
return None;
}
@@ -1300,6 +1304,47 @@ fn cloud_pricing_scope(provider: ApiProvider, identity: &str, base_url: &str) ->
/// Capture the effective mutable price authority once. Provider-owned live
/// prices retain priority; signed cloud prices are admitted only on the exact
/// canonical official route. The historical wire field name remains stable.
pub(crate) fn configured_dispatch_pricing_quote_at(
models: &[codewhale_config::catalog::configured::ConfiguredModel],
provider: ApiProvider,
identity: &str,
model: &str,
base_url: &str,
dispatched_at: u64,
) -> Option<ProviderLivePricingQuote> {
if provider == ApiProvider::OpenaiCodex {
return None;
}
codewhale_config::catalog::configured::validate_configured_models(models).ok()?;
let declared = models
.iter()
.find(|row| row.id == model && row.matches_route(identity, base_url))?;
let cost = declared.cost.clone().unwrap_or_default();
let pricing = OfferingPricing {
provider: identity.to_string(),
wire_model_id: model.to_string(),
canonical_model: None,
currency: Currency::Usd,
input_per_million: cost.input,
output_per_million: cost.output,
cache_read_per_million: cost.cache_read,
cache_write_per_million: cost.cache_write,
provenance: PricingProvenance::UserOverride,
effective_at: None,
endpoint_fingerprint: Some(base_url_fingerprint(base_url)),
};
// Freeze even an unpriced declaration: missing rates must not fall through
// to a same-named bundled or subsequently refreshed price.
ProviderLivePricingQuote::from_pricing(
provider,
identity,
model,
&base_url_fingerprint(base_url),
dispatched_at,
&pricing,
)
}
pub(crate) fn fresh_dispatch_pricing_quote_at(
provider: ApiProvider,
provider_identity: &str,
+58 -2
View File
@@ -981,6 +981,60 @@ pub(crate) fn catalog_offering_for_route(
Some(offering)
}
pub(crate) fn configured_model_for_route<'a>(
config: &'a Config,
provider: ApiProvider,
identity: &str,
base_url: &str,
model: &str,
) -> Option<&'a codewhale_config::catalog::configured::ConfiguredModel> {
// Account-owned OAuth rosters retain their separate authority.
if provider == ApiProvider::OpenaiCodex {
return None;
}
let models = config.custom_models.as_deref()?;
codewhale_config::catalog::configured::validate_configured_models(models).ok()?;
models
.iter()
.find(|row| row.id == model && row.matches_route(identity, base_url))
}
/// Config declarations take precedence only for the exact selected route.
pub(crate) fn configured_catalog_offering_for_route(
config: &Config,
provider: ApiProvider,
identity: &str,
base_url: &str,
model: &str,
) -> Option<CatalogOffering> {
configured_model_for_route(config, provider, identity, base_url, model)
.map(|row| row.to_catalog_offering())
.or_else(|| catalog_offering_for_route(provider, identity, base_url, model))
}
pub(crate) fn configured_catalog_models_for_route(
config: &Config,
provider: ApiProvider,
identity: &str,
base_url: &str,
) -> Vec<String> {
let mut ids = catalog_models_for_route(provider, identity, base_url);
if provider != ApiProvider::OpenaiCodex {
let models = config.custom_models.as_deref().unwrap_or_default();
if codewhale_config::catalog::configured::validate_configured_models(models).is_ok() {
for model in models
.iter()
.filter(|row| row.matches_route(identity, base_url))
{
if !ids.contains(&model.id) {
ids.push(model.id.clone());
}
}
}
}
ids
}
/// Look up the **bundled-snapshot** offering for `(provider, wire_model_id)`,
/// ignoring any live rows merged over it.
///
@@ -1034,7 +1088,8 @@ pub fn models_for_provider(
provider: ApiProvider,
) -> Vec<String> {
if provider_is_configured_for_active(config, provider, active) {
catalog_models_for_route(
configured_catalog_models_for_route(
config,
provider,
&config.provider_identity_for(provider),
&config.base_url_for_route(provider),
@@ -1515,7 +1570,8 @@ pub(crate) async fn run_models(
let identity = &identities[0];
let mut route_config = config.clone();
route_config.scope_to_provider_identity(identity);
let mut models = catalog_models_for_route(
let mut models = configured_catalog_models_for_route(
config,
identity.provider,
&identity.key,
&route_config.deepseek_base_url(),
+133 -9
View File
@@ -22,6 +22,7 @@ use crate::models::DIRECT_KIMI_K3_MAX_OUTPUT_TOKENS;
#[serde(rename_all = "snake_case")]
pub(crate) enum ContextWindowSource {
Configured,
UserDeclared,
ProviderReported,
StaticKimiCodeSafeFloor,
Catalog,
@@ -37,8 +38,9 @@ impl ContextWindowSource {
/// Every rung, in precedence order. The name-suffix hint sits between
/// catalog data and the conservative fallback: any concrete fact about
/// the route beats a naming convention.
pub(crate) const ALL: [Self; 6] = [
pub(crate) const ALL: [Self; 7] = [
Self::Configured,
Self::UserDeclared,
Self::ProviderReported,
Self::StaticKimiCodeSafeFloor,
Self::Catalog,
@@ -50,6 +52,7 @@ impl ContextWindowSource {
pub(crate) const fn label(self) -> &'static str {
match self {
Self::Configured => "configured",
Self::UserDeclared => "user declared",
Self::ProviderReported => "provider-reported",
Self::StaticKimiCodeSafeFloor => "static Kimi Code safe floor",
Self::Catalog => "catalog",
@@ -73,7 +76,10 @@ impl ContextWindowSource {
/// #5441).
#[must_use]
pub(crate) const fn is_verified(self) -> bool {
!matches!(self, Self::NameSuffixHint | Self::Fallback)
!matches!(
self,
Self::NameSuffixHint | Self::Fallback | Self::UserDeclared
)
}
/// Suffix every rendered window carries: verified rungs stay bare,
@@ -398,6 +404,7 @@ pub(crate) fn validate_unpinned_model_provider(
/// Resolve a provider-less fixed model to the provider's exact wire id before
/// child admission. This shares the runtime resolver used by Fleet receipts,
/// including aggregator alias translation, without making a live request.
#[cfg(test)]
pub(crate) fn resolve_unpinned_model_candidate(
provider: ApiProvider,
model: &str,
@@ -418,6 +425,35 @@ pub(crate) fn resolve_unpinned_model_candidate(
/// Code bare-K3 endpoint, only at the documented 1M entitlement, and only
/// while fresh; this prevents generic Moonshot or stale metadata from being
/// inherited by a membership-plan route.
/// Resolve a manual selection from the App's loaded, non-secret metadata
/// snapshot. This shares the same scoped resolver and limit precedence as
/// config-backed runtime selection, without loading credentials while typing.
pub(crate) fn resolve_declared_model_candidate(
provider: ApiProvider,
identity: &str,
model: &str,
base_url: &str,
context_window: Option<u32>,
models: &[codewhale_config::catalog::configured::ConfiguredModel],
) -> Result<RouteCandidateResolution, String> {
let resolver = RouteResolver::new().with_configured_models(
models,
identity,
provider.kind().unwrap_or_default(),
base_url,
);
resolve_route_candidate_with_catalog_resolver(
provider,
Some(model),
None,
Some(base_url.into()),
context_window,
None,
&resolver,
false,
)
}
pub(crate) fn resolve_route_candidate_with_context_metadata(
provider: ApiProvider,
model_selector: Option<&str>,
@@ -518,13 +554,23 @@ fn plan_limit_overrides(
provider_reported_context: Option<ProviderReportedKimiCodeContext>,
) -> LimitOverridePlan {
let mut overrides = Vec::new();
let declared_field = |field| {
resolved
.applied_limit_overrides()
.iter()
.rev()
.find(|entry| entry.field == field)
.is_some_and(|entry| entry.source == OverrideSource::UserModelMetadata)
};
let configured = context_window_override.filter(|window| *window > 0);
let mut effective_context = resolved.limits().context_tokens;
if is_exact_direct_moonshot_k3_route(
provider,
&resolved.endpoint().base_url,
resolved.wire_model_id().as_str(),
) {
if !declared_field(LimitField::OutputTokens)
&& is_exact_direct_moonshot_k3_route(
provider,
&resolved.endpoint().base_url,
resolved.wire_model_id().as_str(),
)
{
overrides.push(SourcedLimitOverride {
field: LimitField::OutputTokens,
value: Some(u64::from(DIRECT_KIMI_K3_MAX_OUTPUT_TOKENS)),
@@ -579,6 +625,25 @@ fn plan_limit_overrides(
};
}
// Exact operator metadata wins over inferred/catalog/provider-family facts.
// A missing field remains unknown, with only the conservative budget floor.
if declared_field(LimitField::ContextTokens) {
let context_window = effective_context
.and_then(|tokens| u32::try_from(tokens).ok())
.map(|tokens| ContextWindowResolution {
tokens,
source: ContextWindowSource::UserDeclared,
})
.unwrap_or(ContextWindowResolution {
tokens: 128_000,
source: ContextWindowSource::Fallback,
});
return LimitOverridePlan {
overrides,
context_window,
};
}
let is_exact_kimi_code_k3 = is_exact_kimi_code_bare_k3_route(
provider,
&resolved.endpoint().base_url,
@@ -700,12 +765,18 @@ pub(crate) fn resolve_runtime_route_for_identity(
let resolution = if provider != ApiProvider::OpenaiCodex {
let status =
crate::provider_catalog_live::status_for_route(provider, &identity.key, &base_url);
let catalog = crate::provider_lake::runtime_catalog_resolver_for_identity(
let mut catalog = crate::provider_lake::runtime_catalog_resolver_for_identity(
provider,
Some(&identity.key),
&base_url,
status,
);
catalog.resolver = catalog.resolver.with_configured_models(
route_config.custom_models.as_deref().unwrap_or_default(),
&identity.key,
provider.kind().unwrap_or_default(),
&base_url,
);
// Local Ollama's placeholder is never an executable model. Resolve
// an unset/auto/placeholder selection from this endpoint's fresh roster,
// while preserving an explicit or saved real tag verbatim.
@@ -840,6 +911,57 @@ mod tests {
use super::*;
use crate::config::{DEFAULT_TEXT_MODEL, DEFAULT_ZAI_MODEL, ProviderConfig, ProvidersConfig};
#[test]
fn configured_model_limits_precede_provider_defaults() {
let _env = crate::test_support::lock_test_env();
let _catalog = crate::provider_lake::lock_live_snapshot();
for (provider, identity, base, model) in [
(
ApiProvider::Moonshot,
"moonshot",
"https://api.moonshot.ai/v1",
"kimi-k3",
),
(
ApiProvider::Moonshot,
"moonshot",
"https://api.kimi.com/coding/v1",
"k3",
),
(
ApiProvider::DeepseekCN,
"deepseek-cn",
"https://models.example.test/v1",
"deepseek-v4.1-flash-expires-on-0910",
),
] {
let mut config: Config = toml::from_str(include_str!(
"../../config/tests/fixtures/custom_models.toml"
))
.unwrap();
config.provider = Some(identity.into());
config.base_url = Some(base.into());
config.providers = None;
let declaration = &mut config.custom_models.as_mut().unwrap()[0];
declaration.provider = identity.into();
declaration.base_url = base.into();
declaration.id = model.into();
let route = resolve_runtime_route(&config, provider, Some(model)).unwrap();
assert_eq!(route.model, model);
assert_eq!(route.candidate.limits().context_tokens, Some(96000));
assert_eq!(route.candidate.limits().output_tokens, Some(8000));
assert_eq!(
route.context_window.source,
ContextWindowSource::UserDeclared
);
config.custom_models.as_mut().unwrap()[0].limit = None;
let unknown = resolve_runtime_route(&config, provider, Some(model)).unwrap();
assert_eq!(unknown.candidate.limits().context_tokens, None);
assert_eq!(unknown.candidate.limits().output_tokens, None);
assert_eq!(unknown.context_window.source, ContextWindowSource::Fallback);
}
}
/// Every rung keeps its own label and round-trips through it, and only
/// the guesses read as unverified. Two rungs sharing a label would let a
/// guess be displayed as evidence.
@@ -859,7 +981,9 @@ mod tests {
source.is_verified(),
!matches!(
source,
ContextWindowSource::Fallback | ContextWindowSource::NameSuffixHint
ContextWindowSource::Fallback
| ContextWindowSource::NameSuffixHint
| ContextWindowSource::UserDeclared
),
"{source:?} misreports whether its window rests on route evidence"
);
+21 -3
View File
@@ -6320,6 +6320,20 @@ fn provider_models_for_api(
push_unique_model(&mut models, &model);
}
}
for model in config.custom_models.as_deref().unwrap_or_default() {
if crate::provider_lake::configured_model_for_route(
config,
provider,
&config.provider_identity_for(provider),
&config.base_url_for_route(provider),
&model.id,
)
.is_some()
&& !models.contains(&model.id)
{
models.push(model.id.clone());
}
}
if provider == ApiProvider::Ollama {
models.retain(|model| !crate::config::is_unresolved_local_ollama_model(model));
}
@@ -6538,9 +6552,13 @@ async fn list_providers(
let default_model = provider_default_model_for_api(&config, active_provider, api_provider);
let identity = config.provider_identity_for(api_provider);
let base_url = config.base_url_for_route_identity(api_provider, &identity);
let has_model_catalog =
!crate::provider_lake::catalog_models_for_route(api_provider, &identity, &base_url)
.is_empty();
let has_model_catalog = !crate::provider_lake::configured_catalog_models_for_route(
&config,
api_provider,
&identity,
&base_url,
)
.is_empty();
providers.push(ProviderEntry {
id: api_provider.as_str().to_string(),
model_provider_id: (api_provider == active_provider)
+77 -23
View File
@@ -13400,13 +13400,7 @@ fn enforce_fleet_member_route_requirements(
}
let member_id = crate::fleet::identity::FleetMemberIdentity::from_member(member).member_id;
let candidate = crate::route_runtime::resolve_route_candidate(
runtime.client.api_provider(),
Some(model),
None,
Some(runtime.client.base_url().to_string()),
None,
)
let candidate = runtime.client.resolve_model_route(model)
.map_err(|error| {
ToolError::execution_failed(format!(
"Fleet member '{member_id}' requirements could not be checked against its exact child route: {}",
@@ -13964,7 +13958,15 @@ fn resolve_fixed_spawn_model_route(
return Ok(());
};
let provider = runtime.client.api_provider();
let candidate = runtime.client.resolve_model_route(model);
let declared = candidate.as_ref().is_ok_and(|candidate| {
candidate
.applied_limit_overrides()
.iter()
.any(|entry| entry.source == codewhale_config::route::OverrideSource::UserModelMetadata)
});
if providerless
&& !declared
&& let Err(reason) = crate::route_runtime::validate_unpinned_model_provider(
provider,
model,
@@ -13990,22 +13992,7 @@ fn resolve_fixed_spawn_model_route(
selection.source = SpawnRouteSource::RunModel;
return Ok(());
}
let candidate = if providerless {
crate::route_runtime::resolve_unpinned_model_candidate(
provider,
model,
runtime.client.base_url(),
)
} else {
crate::route_runtime::resolve_route_candidate(
provider,
Some(model),
None,
Some(runtime.client.base_url().to_string()),
None,
)
}
.map_err(ToolError::invalid_input)?;
let candidate = candidate.map_err(|error| ToolError::invalid_input(error.to_string()))?;
selection.model_route = ModelRoute::Fixed(candidate.wire_model_id().as_str().to_string());
Ok(())
}
@@ -16797,3 +16784,70 @@ mod tests;
#[cfg(test)]
pub(crate) use tests::kimi_general_child_request_tools_fixture;
#[cfg(test)]
#[test]
fn configured_model_subagent_keeps_exact_id_and_negative_capability() {
let _env = crate::test_support::lock_test_env();
let mut runtime = tests::stub_runtime();
let mut config = crate::config::Config {
provider: Some("deepseek".into()),
api_key: Some("configured-model-local-fixture".into()),
custom_models: Some(vec![
toml::from_str(
r#"
provider = "deepseek"
base_url = "https://api.deepseek.com"
id = "deepseek-v4pro"
modalities = { input = ["text"], output = ["text"] }
tool_call = false
limit = { context = 4096, output = 64 }
"#,
)
.unwrap(),
]),
..crate::config::Config::default()
};
runtime.client = DeepSeekClient::new(&config).unwrap();
config.custom_models.as_mut().unwrap()[0]
.modalities
.as_mut()
.unwrap()
.input
.push("image".into());
runtime.api_config = Some(std::sync::Arc::new(config));
let mut selection = SpawnModelSelection {
model_route: ModelRoute::Fixed("deepseek-v4pro".into()),
source: SpawnRouteSource::TaskModel,
};
resolve_fixed_spawn_model_route(&runtime, &mut selection, true).unwrap();
assert_eq!(
selection.model_route,
ModelRoute::Fixed("deepseek-v4pro".into())
);
let candidate = runtime
.client
.resolve_model_route("deepseek-v4pro")
.unwrap();
assert_eq!(
candidate.capabilities().image_input,
codewhale_config::route::CapabilityState::Unsupported
);
assert_eq!(
candidate.capabilities().native_tool_calls,
codewhale_config::route::CapabilityState::Unsupported
);
let member = crate::fleet::profile::AgentProfile {
id: "metadata-fixture".into(),
display_name: None,
description: None,
requires: vec!["vision".into()],
profile: codewhale_config::FleetProfile::default(),
source: std::path::PathBuf::new(),
origin: crate::fleet::profile::ProfileOrigin::Config,
plugin_authority: None,
};
let error = enforce_fleet_member_route_requirements(Some(&member), &runtime, "deepseek-v4pro")
.unwrap_err();
assert!(error.to_string().contains("unsupported"));
}
+3
View File
@@ -1561,6 +1561,9 @@ pub struct App {
/// The catalog remains separately discoverable and selecting from it adds
/// to this set rather than replacing earlier enabled choices.
pub enabled_provider_models: HashMap<String, Vec<String>>,
/// Non-secret declarations from the loaded config snapshot. Completion
/// reads this snapshot without reloading credentials on each keystroke.
pub configured_models: Vec<codewhale_config::catalog::configured::ConfiguredModel>,
/// Exact provider/model pins loaded from settings, in user order.
pub pinned_models: Vec<crate::settings::PinnedModel>,
/// When true, the model is auto-selected based on request complexity
+3 -8
View File
@@ -398,16 +398,10 @@ impl App {
},
)
} else {
let saved_provider_model = config
.provider_config_for(provider)
.and_then(|provider| provider.model.as_deref());
crate::route_runtime::resolve_route_candidate_with_context_metadata(
crate::route_runtime::resolve_runtime_route(
&effective_auth_config,
provider,
Some(&model),
saved_provider_model,
Some(configured_route_base_url.clone()),
active_context_window_override,
None,
)
.map(|resolution| {
(
@@ -802,6 +796,7 @@ impl App {
model,
provider_models,
enabled_provider_models,
configured_models: config.custom_models.clone().unwrap_or_default(),
pinned_models: settings.pinned_models.clone(),
auto_model,
last_effective_model: None,
+234 -4
View File
@@ -305,6 +305,8 @@ struct ModelPickerRow {
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct EffectivePickerMetadata {
display_name: Option<String>,
declared_input_price: Option<String>,
context_window: Option<u32>,
/// The context window came through the legacy provider fallback rather
/// than an offering, catalog row, roster, or operator override — shown,
@@ -317,6 +319,7 @@ struct EffectivePickerMetadata {
max_output_unverified: bool,
tool_calls: Option<bool>,
reasoning: bool,
reasoning_unknown: bool,
vision: SupportState,
pricing: PickerPricing,
source: Option<CatalogSource>,
@@ -544,7 +547,12 @@ impl ModelPickerView {
let mut rows: Vec<_> = visible
.iter()
.map(|row| PaneRow {
primary: row.id.clone(),
primary: row
.metadata
.display_name
.as_ref()
.map(|label| format!("{label} ({})", row.id))
.unwrap_or_else(|| row.id.clone()),
route: row
.provider
.map(|provider| {
@@ -1597,6 +1605,14 @@ fn picker_model_rows_for_app(app: &App, config: &Config) -> Vec<ModelPickerRow>
} else {
provider_scoped_model_ids_for_app(app, false)
};
if let Some(enabled) = app
.enabled_provider_models
.get(app.provider_identity_for_persistence())
{
for id in enabled {
push_model_id(&mut active_model_ids, id);
}
}
push_configured_provider_model(&mut active_model_ids, config, app.api_provider);
push_provider_model_rows(
&mut rows,
@@ -1637,6 +1653,11 @@ fn picker_model_rows_for_app(app: &App, config: &Config) -> Vec<ModelPickerRow>
),
);
}
if let Some(enabled) = app.enabled_provider_models.get(provider.as_str()) {
for id in enabled {
push_model_id(&mut model_ids, id);
}
}
push_configured_provider_model(&mut model_ids, config, provider);
push_provider_model_rows(
&mut rows,
@@ -1704,6 +1725,9 @@ fn picker_model_rows_for_app(app: &App, config: &Config) -> Vec<ModelPickerRow>
}
fn model_row_enabled_for_app(app: &App, config: &Config, row: &ModelPickerRow) -> bool {
if matches!(row.metadata.source, Some(CatalogSource::ConfigOverride)) {
return true;
}
let Some(provider) = row.provider else {
return true;
};
@@ -1761,12 +1785,23 @@ fn push_provider_model_rows(
rows: &mut Vec<ModelPickerRow>,
provider: ApiProvider,
provider_identity: Option<&str>,
model_ids: Vec<String>,
mut model_ids: Vec<String>,
active_provider: ApiProvider,
config: &Config,
codex_roster: &CodexModelRoster,
provider_health: &crate::provider_readiness::ProviderReadinessSnapshot,
) {
let identity = provider_identity
.map(str::to_string)
.unwrap_or_else(|| config.provider_identity_for(provider));
let base_url = config.base_url_for_route_identity(provider, &identity);
for id in crate::provider_lake::configured_catalog_models_for_route(
config, provider, &identity, &base_url,
) {
if !model_ids.contains(&id) {
model_ids.push(id);
}
}
for id in model_ids {
if id == "auto" {
continue;
@@ -1806,6 +1841,9 @@ fn push_provider_model_rows(
codex_freshness,
provider_catalog_receipt.as_ref(),
);
if metadata.display_name.is_some() {
hint = format!("{id} · {hint}");
}
hint = format!("{readiness_label} · {hint}");
if provider != active_provider {
hint = format!("switch route · {hint}");
@@ -1965,6 +2003,22 @@ fn provider_scoped_model_ids_for_app(app: &App, include_current_model: bool) ->
push_model_id(&mut models, &id);
}
if app.api_provider != ApiProvider::OpenaiCodex
&& codewhale_config::catalog::configured::validate_configured_models(&app.configured_models)
.is_ok()
{
for declaration in app.configured_models.iter().filter(|declaration| {
declaration.matches_route(
app.provider_identity_for_persistence(),
&app.active_route_base_url,
)
}) {
if !models.contains(&declaration.id) {
models.push(declaration.id.clone());
}
}
}
if let Some(model) = app
.provider_models
.get(app.provider_identity_for_persistence())
@@ -2119,6 +2173,7 @@ fn model_row_matches_query(
|| matches(provider.display_name())
});
provider_matches
|| row.metadata.display_name.as_deref().is_some_and(matches)
|| matches(&row.id)
|| ((row.provider.is_none() || row.provider == Some(initial_provider))
&& matches(&row.hint))
@@ -2252,6 +2307,45 @@ fn catalog_family_for_identity(
}
fn model_row_meta_chips(row: &ModelPickerRow) -> Vec<String> {
if row.metadata.source == Some(CatalogSource::ConfigOverride) {
// The qualifier comes first so compact rows cannot shed it while
// keeping an unverified declaration visible as a provider fact.
return vec![
"user declared (unverified)".to_string(),
row.metadata
.context_window
.map(|value| format_picker_context_window(u64::from(value)))
.unwrap_or_else(|| "context unknown".into()),
row.metadata
.max_output
.map(|value| format!("{value} out"))
.unwrap_or_else(|| "output unknown".into()),
match &row.metadata.pricing {
PickerPricing::Known(price) => format!("estimate {price}"),
_ => "price unknown".into(),
},
if row.metadata.reasoning_unknown {
"reasoning unknown"
} else if row.metadata.reasoning {
"reasoning"
} else {
"no reasoning"
}
.into(),
match row.metadata.tool_calls {
Some(true) => "tools declared",
Some(false) => "no tools",
None => "tools unknown",
}
.into(),
match row.metadata.vision {
SupportState::Supported => "vision declared",
SupportState::Unsupported => "text only",
SupportState::Unknown => "vision unknown",
}
.into(),
];
}
let mut chips = Vec::new();
if let Some(context_window) = row.metadata.context_window {
chips.push(format_picker_context_window(u64::from(context_window)));
@@ -2557,6 +2651,13 @@ fn context_tokens(row: &ModelPickerRow) -> u64 {
}
fn input_price_per_million(row: &ModelPickerRow) -> Option<f64> {
if matches!(row.metadata.source, Some(CatalogSource::ConfigOverride)) {
return row
.metadata
.declared_input_price
.as_ref()
.and_then(|price| price.parse().ok());
}
if matches!(row.metadata.pricing, PickerPricing::Unavailable) {
return None;
}
@@ -2620,7 +2721,9 @@ fn effective_picker_metadata_with_codex(
.map(str::to_string)
.unwrap_or_else(|| config.provider_identity_for(provider));
let base_url = config.base_url_for_route_identity(provider, &identity);
crate::provider_lake::catalog_offering_for_route(provider, &identity, &base_url, id)
crate::provider_lake::configured_catalog_offering_for_route(
config, provider, &identity, &base_url, id,
)
});
let card = offering.as_ref().map(ModelReferenceCard::from_offering);
let registry = model_registry::lookup(id);
@@ -2641,6 +2744,9 @@ fn effective_picker_metadata_with_codex(
} else {
PickerPricing::Unknown
},
display_name: None,
reasoning_unknown: false,
declared_input_price: None,
source: None,
};
};
@@ -2659,6 +2765,42 @@ fn effective_picker_metadata_with_codex(
config.context_window_for_provider_config(provider)
};
let base_url = config.base_url_for_route_identity(provider, &identity);
if let Some(declared) =
crate::provider_lake::configured_model_for_route(config, provider, &identity, &base_url, id)
{
return EffectivePickerMetadata {
display_name: declared.display_name.clone(),
declared_input_price: declared
.cost
.as_ref()
.and_then(|cost| cost.input)
.map(|price| price.to_string()),
context_window: context_override.or_else(|| {
declared
.limit
.as_ref()
.and_then(|limit| limit.context)
.and_then(|value| u32::try_from(value).ok())
}),
max_output: declared
.limit
.as_ref()
.and_then(|limit| limit.output)
.and_then(|value| u32::try_from(value).ok()),
tool_calls: declared.tool_call,
reasoning: declared.reasoning.unwrap_or(false),
reasoning_unknown: declared.reasoning.is_none(),
vision: codewhale_config::models_dev::image_input_support(declared.modalities.as_ref()),
pricing: card
.as_ref()
.filter(|card| card.price_label() != "unknown")
.map_or(PickerPricing::Unknown, |card| {
PickerPricing::Known(card.price_label())
}),
source: Some(CatalogSource::ConfigOverride),
..EffectivePickerMetadata::default()
};
}
if offering.is_none()
&& provider != ApiProvider::OpenaiCodex
&& provider.kind().is_none_or(|kind| {
@@ -2787,6 +2929,9 @@ fn effective_picker_metadata_with_codex(
reasoning,
vision,
pricing,
display_name: None,
reasoning_unknown: false,
declared_input_price: None,
source: card.map(|card| card.source),
}
}
@@ -2897,7 +3042,7 @@ fn render_picker_model_hint(
}
Some(CatalogSource::CloudFacts { .. }) => parts.push("signed facts".to_string()),
Some(CatalogSource::ConfigOverride | CatalogSource::UserOverride) => {
parts.push("override".to_string())
parts.push("user declared (unverified)".to_string())
}
None => {}
}
@@ -3645,6 +3790,91 @@ fn default_picker_effort_idx(
mod tests {
use super::*;
#[test]
fn configured_model_picker_and_runtime_share_exact_persisted_metadata() {
let _env = crate::test_support::lock_test_env();
let _catalog = crate::provider_lake::lock_live_snapshot();
let mut config: Config = toml::from_str(include_str!(
"../../../config/tests/fixtures/custom_models.toml"
))
.unwrap();
let id = "deepseek-v4.1-flash-expires-on-0910";
let base = "https://models.example.test/v1";
let metadata = effective_picker_metadata(&config, Some(ApiProvider::Deepseek), id);
assert_eq!(metadata.display_name.as_deref(), Some("Temporary preview"));
assert_eq!(metadata.context_window, Some(96000));
assert_eq!(metadata.max_output, Some(8000));
assert_eq!(metadata.tool_calls, Some(true));
assert_eq!(metadata.source, Some(CatalogSource::ConfigOverride));
let mut row = model_row(ApiProvider::Deepseek, true);
row.id = id.into();
row.metadata = metadata.clone();
let chips = model_row_meta_chips(&row);
assert_eq!(chips[0], "user declared (unverified)");
assert!(chips.iter().any(|chip| chip.starts_with("estimate ")));
assert!(fit_meta_chips(&chips, 30).starts_with("user declared"));
assert!(
render_picker_model_hint(id, Some(ApiProvider::Deepseek), &metadata, None, None)
.contains("user declared")
);
assert!(
crate::provider_lake::configured_catalog_models_for_route(
&config,
ApiProvider::Deepseek,
"deepseek",
base
)
.iter()
.any(|model| model == id)
);
let route =
crate::route_runtime::resolve_runtime_route(&config, ApiProvider::Deepseek, Some(id))
.unwrap();
assert_eq!(route.model, id);
assert_eq!(Some(route.context_window.tokens), metadata.context_window);
assert_eq!(
route.context_window.source,
crate::route_runtime::ContextWindowSource::UserDeclared
);
assert!(!route.context_window.source.is_verified());
assert_eq!(
route.candidate.capabilities().image_input,
SupportState::Unknown
);
assert_eq!(
route.candidate.capabilities().native_tool_calls,
SupportState::Unknown
);
assert_eq!(
crate::route_budget::route_output_limit_tokens(Some(route.candidate.limits())),
metadata.max_output
);
// /load replaces the same metadata alongside the provider route.
let mut reloaded = config.clone();
reloaded.custom_models.as_mut().unwrap()[0].reasoning = None;
reloaded.custom_models.as_mut().unwrap()[0].limit = None;
reloaded.custom_models.as_mut().unwrap()[0].cost = None;
reloaded.custom_models.as_mut().unwrap()[0].tool_call = None;
reloaded.custom_models.as_mut().unwrap()[0].modalities = None;
config.refresh_provider_routes_from(&reloaded);
let unknown = effective_picker_metadata(&config, Some(ApiProvider::Deepseek), id);
assert_eq!(unknown.context_window, None);
assert_eq!(unknown.max_output, None);
assert_eq!(unknown.tool_calls, None);
assert_eq!(unknown.vision, SupportState::Unknown);
assert_eq!(unknown.pricing, PickerPricing::Unknown);
row.metadata = unknown;
assert!(model_row_meta_chips(&row).contains(&"reasoning unknown".to_string()));
assert_eq!(
crate::route_budget::effective_max_output_tokens_for_route(
ApiProvider::Deepseek,
id,
None
),
8192
);
}
fn model_row(provider: ApiProvider, enabled: bool) -> ModelPickerRow {
ModelPickerRow {
id: "model".to_string(),
+78 -19
View File
@@ -179,6 +179,7 @@ enum ProviderListView {
}
pub struct ProviderPickerView {
route_config: Config,
rows: Vec<ProviderDashboardRow>,
selected_idx: usize,
stage: Stage,
@@ -685,18 +686,10 @@ impl ProviderDashboardRow {
// particular, Kimi Code's bare K3 model has a conservative 262K
// membership-plan baseline (or an explicit configured override), not
// the generic catalog's unknown-model fallback.
let route = crate::route_runtime::resolve_route_candidate_with_context_metadata(
let route = crate::route_runtime::resolve_runtime_route(
config,
provider,
configured_model.as_deref(),
None,
// The legacy CN alias shares DeepSeek's strict model contract.
// Passing its endpoint as a generic override would classify the
// route as custom and accidentally accept foreign model ids.
(provider != ApiProvider::DeepseekCN)
.then(|| configured_base_url.clone())
.flatten(),
config.context_window_for_provider_config(provider),
None,
);
let (
base_url,
@@ -784,6 +777,34 @@ impl ProviderDashboardRow {
capabilities.context_window = Some(context_window);
}
capabilities.context_window_source = route_context_window_source;
if let Some(declared) = crate::provider_lake::configured_model_for_route(
config,
provider,
&provider_id,
&base_url,
&default_route.wire_model,
) {
capabilities = ProviderCapabilityBadges::unknown();
capabilities.context_window = declared
.limit
.as_ref()
.and_then(|limit| limit.context)
.and_then(|value| u32::try_from(value).ok());
capabilities.max_output = declared
.limit
.as_ref()
.and_then(|limit| limit.output)
.and_then(|value| u32::try_from(value).ok());
capabilities.context_window_source = Some("user declared (unverified)".into());
messages.push("Model metadata is user declared; availability and capabilities have not been verified.".into());
}
let available_model_count = crate::provider_lake::configured_catalog_models_for_route(
config,
provider,
&provider_id,
&base_url,
)
.len();
// #5772: the status projection needs an ambient candidate path to
// report `ambient_path_changed`, so resolving it for a provider the
// user never consented to would derive (and then render) another CLI's
@@ -1443,6 +1464,33 @@ fn readiness_for(
/// Provider-agnostic fallbacks keep the label truthful when the catalog has
/// no row: self-hosted routes are local, Codex rides OAuth quota, and
/// everything else is honestly unknown.
fn configured_model_cost_label(config: &Config, row: &ProviderDashboardRow, model: &str) -> String {
let mut scoped = config.clone();
if let Ok(identity) = config
.resolve_persisted_provider_identity(Some(row.provider.as_str()), Some(&row.provider_id))
{
scoped.scope_to_provider_identity(&identity);
}
match crate::route_billing::for_route(&scoped, row.provider) {
crate::route_billing::BillingPresentation::Subscription(label) => return label.to_string(),
crate::route_billing::BillingPresentation::Local => return "local".to_string(),
_ => {}
}
if let Some(declared) = crate::provider_lake::configured_model_for_route(
config,
row.provider,
&row.provider_id,
&row.base_url,
model,
) {
let card = codewhale_config::model_reference::ModelReferenceCard::from_offering(
&declared.to_catalog_offering(),
);
return format!("{} · user estimate", card.price_label());
}
model_cost_label(row.provider, model)
}
fn model_cost_label(provider: ApiProvider, model: &str) -> String {
// OpenCode Go spends a subscription allowance, not per-token dollars, so
// a catalog token price would misreport it as metered spend (#4526).
@@ -1459,8 +1507,13 @@ fn model_cost_label(provider: ApiProvider, model: &str) -> String {
/// model sorts first so the eye lands on what Enter would use; the rest are
/// alphabetical. Falls back to the default route when the catalog has no rows
/// for the provider, so the pane never renders empty.
fn provider_pane_models(row: &ProviderDashboardRow, limit: usize) -> Vec<(String, String, bool)> {
let mut models = crate::provider_lake::catalog_models_for_route(
fn provider_pane_models(
config: &Config,
row: &ProviderDashboardRow,
limit: usize,
) -> Vec<(String, String, bool)> {
let mut models = crate::provider_lake::configured_catalog_models_for_route(
config,
row.provider,
&row.provider_id,
&row.base_url,
@@ -1481,7 +1534,7 @@ fn provider_pane_models(row: &ProviderDashboardRow, limit: usize) -> Vec<(String
.map(|model| {
let is_default =
model.eq_ignore_ascii_case(&default) || model.eq_ignore_ascii_case(&wire);
let price = model_cost_label(row.provider, &model);
let price = configured_model_cost_label(config, row, &model);
(model, price, is_default)
})
.collect()
@@ -1649,6 +1702,7 @@ impl ProviderPickerView {
ProviderListView::Catalog
};
let mut picker = Self {
route_config: config.clone(),
rows,
selected_idx,
stage: Stage::List,
@@ -2184,7 +2238,8 @@ impl ProviderPickerView {
route.logical_model.clone()
};
let row = &self.rows[self.selected_idx];
let mut models = crate::provider_lake::catalog_models_for_route(
let mut models = crate::provider_lake::configured_catalog_models_for_route(
&self.route_config,
provider,
&row.provider_id,
&row.base_url,
@@ -2931,7 +2986,7 @@ impl ProviderPickerView {
.fg(palette::TEXT_PRIMARY)
.add_modifier(Modifier::BOLD),
)));
let pane_models = provider_pane_models(row, 8);
let pane_models = provider_pane_models(&self.route_config, row, 8);
let name_budget = usize::from(inner.width).saturating_sub(22).max(8);
for (model, price, is_default) in &pane_models {
let name = crate::tui::ui_text::truncate_line_to_width(model, name_budget);
@@ -2951,7 +3006,8 @@ impl ProviderPickerView {
}
lines.push(Line::from(spans));
}
let total_models = crate::provider_lake::catalog_models_for_route(
let total_models = crate::provider_lake::configured_catalog_models_for_route(
&self.route_config,
row.provider,
&row.provider_id,
&row.base_url,
@@ -3510,7 +3566,6 @@ impl ProviderPickerView {
// and every visible row is clickable, so record this frame's geometry
// for hover + click handling.
self.model_row_hitboxes.borrow_mut().clear();
let model_provider = self.rows[self.selected_idx].provider;
let mut lines: Vec<Line> = Vec::with_capacity(visible_rows);
for (idx, model) in self
.model_options
@@ -3539,7 +3594,11 @@ impl ProviderPickerView {
""
};
// Slice D: cost moved off the provider level down to the model.
let price = model_cost_label(model_provider, model);
let price = configured_model_cost_label(
&self.route_config,
&self.rows[self.selected_idx],
model,
);
let mut spans = vec![
Span::styled(format!(" {arrow} {model}"), label_style),
Span::styled(
@@ -8238,7 +8297,7 @@ mod tests {
.iter()
.find(|row| row.provider == ApiProvider::Deepseek)
.expect("DeepSeek has a picker row");
let models = provider_pane_models(row, 8);
let models = provider_pane_models(&config, row, 8);
assert!(!models.is_empty(), "models pane must never render empty");
assert!(models.len() <= 8);
let (first, _, first_default) = &models[0];
+2 -11
View File
@@ -928,17 +928,7 @@ pub(crate) async fn apply_model_picker_choice(
let mut resolved_model = model.clone();
let mut route_base_url = config.deepseek_base_url();
if !model_is_auto {
let saved_provider_model = config
.provider_config_for(app.api_provider)
.and_then(|provider| provider.model.as_deref());
match crate::route_runtime::resolve_route_candidate_with_context_metadata(
app.api_provider,
Some(&model),
saved_provider_model,
Some(config.deepseek_base_url()),
config.context_window_for_provider_config(app.api_provider),
None,
) {
match crate::route_runtime::resolve_runtime_route(config, app.api_provider, Some(&model)) {
Ok(resolution) => {
resolved_model = resolution.candidate.wire_model_id().as_str().to_string();
route_base_url = resolution.candidate.endpoint().base_url.clone();
@@ -3785,6 +3775,7 @@ pub(crate) fn apply_loaded_session_config_snapshot(
&previous_workspace,
);
*config = next_config;
app.configured_models = config.custom_models.clone().unwrap_or_default();
crate::initialize_cloud_facts(config);
app.refresh_notification_settings(config);
Ok(respawn)
+1 -11
View File
@@ -1013,17 +1013,7 @@ pub(crate) fn resolve_loaded_session_route(app: &mut App, config: &Config) {
return;
}
let saved_provider_model = config
.provider_config_for(app.api_provider)
.and_then(|provider| provider.model.as_deref());
match crate::route_runtime::resolve_route_candidate_with_context_metadata(
app.api_provider,
Some(&app.model),
saved_provider_model,
Some(config.deepseek_base_url()),
context_override,
None,
) {
match crate::route_runtime::resolve_runtime_route(config, app.api_provider, Some(&app.model)) {
Ok(resolution) => {
app.set_active_route_resolution(
resolution.candidate.endpoint().base_url.clone(),