mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 17:10:50 -10:00
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:
1 parent
292fe1c033
commit
fe2f1d8d03
2 files changed
+46
-5
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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!(
|
||||
|
||||
Reference in new issue
Block a user