Split canonical ReasoningEffort::Max out of Xhigh (providers P0c-1)
OpenAI (Responses) and Anthropic (Messages) treat xhigh and max as
DISTINCT effort levels in 2026, and the Kimi K3 wire's top tier is max —
the old parse alias (max→Xhigh) conflated them. Canonical Max now exists:
parse/as_str/serde split, Messages mapping sends xhigh and max as their
own tokens (was Xhigh→"max"), and the K3 menu token max carries
canonical Max end to end.
Kimi wire is byte-identical in all four flows (menu pick, restored
legacy xhigh session, --reasoning-effort flag, /effort command) —
adversarially traced and pinned: kimi_compat's string-level xhigh→max
rename covers legacy tokens, max passes through verbatim.
From the review:
- Rollback safety: persisted reasoning_effort (session summaries, chat
history) deserializes leniently — unknown future tokens degrade to
None with a warning instead of hiding sessions or failing resume.
- Restore migration: a pre-split xhigh override onto a model whose menu
offers max but not xhigh (K3) migrates once, healing display/active-row
drift and re-persisting the live vocabulary.
- /effort max now rejects (with the offered list) on models whose menu
lacks a max row instead of silently applying xhigh; deliberate, tested.
- The interim Responses-backend Max→xhigh downgrade (async-openai has no
Max variant through 0.41) warns loudly; real max wiring lands with the
OpenAI provider cycle via post-serialize body patch.
- Two rusted ignored-e2e wire pins asserted the pre-adapt reasoning_effort
key (deleted by the body adapter since ea0ce9d); they now pin the real
thinking.effort=max shape.
This commit is contained in:
@@ -183,16 +183,47 @@ fn stashed_model_keeps_model_when_unsupported() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effort_only_accepts_max_as_xhigh() {
|
||||
fn effort_max_rejected_when_model_offers_no_max() {
|
||||
// Since the Xhigh/Max split, "max" is its own canonical level — a model
|
||||
// whose menu has no max-valued option rejects it with the offered list
|
||||
// (previously the parse alias silently rode it onto the xhigh option).
|
||||
let models = models_with_current(true);
|
||||
let out = take_deferred_model_switch(None, &models, Some("max"));
|
||||
assert_eq!(
|
||||
out,
|
||||
DeferredSwitchOutcome {
|
||||
switch: Some((
|
||||
models.current.clone().unwrap(),
|
||||
Some(ReasoningEffort::Xhigh)
|
||||
)),
|
||||
switch: None,
|
||||
effort_error: Some(EffortTokenError::UnknownToken {
|
||||
token: "max".into(),
|
||||
offered: vec!["deep".into(), "high".into()],
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effort_only_accepts_canonical_max_when_offered() {
|
||||
// K3-shaped menu: the "max" wire token carries canonical Max.
|
||||
let id = acp::ModelId::new(Arc::from("k3"));
|
||||
let meta = serde_json::json!({
|
||||
"supportsReasoningEffort": true,
|
||||
"reasoningEffort": "max",
|
||||
"reasoningEfforts": [
|
||||
{ "id": "low", "value": "low", "label": "Low" },
|
||||
{ "id": "high", "value": "high", "label": "High" },
|
||||
{ "id": "max", "value": "max", "label": "Max" },
|
||||
],
|
||||
});
|
||||
let info = acp::ModelInfo::new(id.clone(), id.0.to_string()).meta(meta.as_object().cloned());
|
||||
let mut models = ModelState::default();
|
||||
models.available.insert(id.clone(), info);
|
||||
models.current = Some(id);
|
||||
models.reasoning_effort = Some(ReasoningEffort::High);
|
||||
let out = take_deferred_model_switch(None, &models, Some("max"));
|
||||
assert_eq!(
|
||||
out,
|
||||
DeferredSwitchOutcome {
|
||||
switch: Some((models.current.clone().unwrap(), Some(ReasoningEffort::Max))),
|
||||
effort_error: None,
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user