fix(catalog): take the account's advertised limits into the route

The Codewhale account listing (`GET /v1/models` on api.codewhale.net)
advertises `codewhale.limits` (maxInputTokens, maxOutputTokens,
maxContextTokens) for a managed model, but the listing parser set no
limits on the live row, so a managed model such as codewhale/Qwen3.8-27B
resolved with no route limits and the model-only fallback sent
max_tokens 64000 against a 16384-token context with a 1024-token output
allowance. The parser now carries the advertised numbers as the row's
limit (zero and absent values stay unknown; rows without the field are
unchanged), and route resolution applies them like any live catalog
limit: the request output cap becomes min(requested, 1024) and the input
budget honours the window. Nothing is claimed the account service did not
state. Found by the member-testing rollout with a real catalog capture
(artifacts/qwen-member-testing-20261021/private-rollout/founder-live-catalog.json).

Verification: client::test_cases_05, client::catalog_tests,
client::test_cases_03, route_budget::tests and tui::persistence_actor:
64 passed, 0 failed; clippy with CI flags clean. A fresh exec process
still starts with no account rows (persisted account-scoped rows are
discarded by design), so this reaches processes that refresh the account
catalog themselves (the desktop's Engine, interactive sessions); the exec
path is a separate fix; the existing parser test's fixture gains a
row with limits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-21 08:59:20 -07:00
1 parent 292fe1c033
commit fe2f1d8d03
2 files changed
+46 -5

No files matched your search

+22 -1
View File
@@ -5014,6 +5014,17 @@ fn codewhale_catalog_offerings_from_body(
protocol: Option<String>,
#[serde(default)]
default: bool,
#[serde(default)]
limits: Option<RowLimits>,
}
#[derive(serde::Deserialize)]
struct RowLimits {
#[serde(default, rename = "maxInputTokens")]
max_input_tokens: Option<u64>,
#[serde(default, rename = "maxOutputTokens")]
max_output_tokens: Option<u64>,
#[serde(default, rename = "maxContextTokens")]
max_context_tokens: Option<u64>,
}
let listing: Listing =
@@ -5040,6 +5051,16 @@ fn codewhale_catalog_offerings_from_body(
Some("responses") => "responses",
_ => codewhale_config::route::codewhale_endpoint_key_for_model(&id),
};
let limit = row
.codewhale
.as_ref()
.and_then(|meta| meta.limits.as_ref())
.map(|limits| codewhale_config::models_dev::ModelsDevLimit {
context: limits.max_context_tokens.filter(|tokens| *tokens > 0),
input: limits.max_input_tokens.filter(|tokens| *tokens > 0),
output: limits.max_output_tokens.filter(|tokens| *tokens > 0),
})
.filter(|limit| *limit != codewhale_config::models_dev::ModelsDevLimit::default());
offerings.push(CatalogOffering {
cost_source: None,
modalities_source: None,
@@ -5049,7 +5070,7 @@ fn codewhale_catalog_offerings_from_body(
endpoint_key: endpoint_key.to_string(),
default_for_provider: row.codewhale.is_some_and(|meta| meta.default),
family: None,
limit: None,
limit,
cost: None,
modalities: None,
attachment: None,
+24 -4
View File
@@ -1066,7 +1066,12 @@
"protocol": "some-future-wire", "usable": true}},
{"id": "deepseek/deepseek-v4-pro", "object": "model"},
{"id": " ", "object": "model"},
{"id": "anthropic/claude-opus-4-8", "object": "model"}
{"id": "anthropic/claude-opus-4-8", "object": "model"},
{"id": "codewhale/Qwen3.8-27B", "object": "model", "owned_by": "codewhale",
"codewhale": {"provider": "codewhale", "model": "Qwen3.8-27B",
"protocol": "chat-completions", "usable": true,
"limits": {"maxInputTokens": 7168, "maxOutputTokens": 1024,
"maxContextTokens": 16384}}}
]
}"#;
@@ -1076,7 +1081,7 @@
.iter()
.map(|row| (row.wire_model_id.as_str(), row))
.collect();
assert_eq!(rows.len(), 5, "blank and duplicate ids are dropped");
assert_eq!(rows.len(), 6, "blank and duplicate ids are dropped");
// The protocol comes from the response, not from a compiled roster.
assert_eq!(by_id["deepseek/deepseek-v4-pro"].endpoint_key, "chat");
assert!(by_id["deepseek/deepseek-v4-pro"].default_for_provider);
@@ -1090,10 +1095,25 @@
// A row with no `codewhale` block still resolves through the namespace
// rule rather than being dropped: the account listed it.
assert_eq!(by_id["anthropic/claude-opus-4-8"].endpoint_key, "messages");
// Nothing is claimed that the account service did not state.
// Nothing is claimed that the account service did not state: only a
// row that advertises `codewhale.limits` carries limits, and they are
// the advertised numbers.
assert!(
rows.iter()
.all(|row| row.canonical_model.is_none() && row.limit.is_none() && row.cost.is_none())
.all(|row| row.canonical_model.is_none() && row.cost.is_none())
);
assert!(
rows.iter()
.filter(|row| row.wire_model_id != "codewhale/Qwen3.8-27B")
.all(|row| row.limit.is_none())
);
let qwen = by_id["codewhale/Qwen3.8-27B"]
.limit
.as_ref()
.expect("advertised limits");
assert_eq!(
(qwen.context, qwen.input, qwen.output),
(Some(16384), Some(7168), Some(1024))
);
assert_eq!(