From e6fcd5d252f84789c23bb8c72248e479bf40e5ac Mon Sep 17 00:00:00 2001 From: asto Date: Sun, 18 Oct 2026 03:12:27 +0800 Subject: [PATCH] fix(client): drop sampling params for the Claude 5 generation and Haiku 5.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Anthropic sampling restriction blankets every model released after Opus 4.6: the Messages API reference states those models "do not support" temperature/top_p beyond their backwards-compatible defaults (1.0 / >= 0.99), and the migration guides restate it per family — "Setting temperature, top_p, or top_k to any non-default value on Claude Opus 4.7 and later models, including Claude Opus 5.5, returns a 400 error" — covering the Claude 5 generation, Haiku 5.5 (defaults only: temperature 1, a lone top_p 0.99), and the Fable/Mythos lines. The gate only knew opus-4-7/4-8 and fable/mythos, so user-configured temperature/top_p reached Claude 5 and Haiku 5.5 models and 400'd the request. Dropping the params entirely is always safe; the gate extends its own family list and deliberately stays independent of model_supports_reasoning's exact-id list, which answers a different question (thinking capability, not sampling rejection). Pinned across opus-5(-5) plus a datestamped spelling, sonnet-5(-5), haiku-5-5, fable-5, and mythos-5, with the older opus-4-6/sonnet-4-6/ haiku-4-5 passthrough asserted. Docs re-checked 2026-10-08. Signed-off-by: asto --- crates/tui/src/client/anthropic.rs | 64 ++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/crates/tui/src/client/anthropic.rs b/crates/tui/src/client/anthropic.rs index e60803abfc..01c24f74dd 100644 --- a/crates/tui/src/client/anthropic.rs +++ b/crates/tui/src/client/anthropic.rs @@ -229,9 +229,11 @@ impl CodewhaleClient { _ => {} } - // Sampling parameters: Claude 4.7+ rejects temperature/top_p - // entirely; earlier models reject the two together. Send at most one - // (temperature wins), or neither for models that forbid them. + // Sampling parameters: every model from Claude 4.7 on (including the + // Claude 5 generation and Haiku 5.5) rejects non-default + // temperature/top_p; earlier models reject the two together. Send at + // most one (temperature wins), or neither for models that forbid + // them. if !anthropic_model_rejects_sampling(&request.model) { if let Some(temperature) = request.temperature { body["temperature"] = json!(temperature); @@ -650,11 +652,25 @@ fn assistant_tool_use_ids(message: &Value) -> Vec { .unwrap_or_default() } -/// Models that reject `temperature` / `top_p` outright (Claude 4.7+). +/// Models that reject non-default `temperature` / `top_p` outright (every +/// model released after Opus 4.6). The Messages API reference states those +/// models "do not support" setting `temperature` / `top_p` beyond their +/// backwards-compatible defaults (`temperature: 1.0`, `top_p >= 0.99`), and +/// the migration guides restate it per family: "Setting `temperature`, +/// `top_p`, or `top_k` to any non-default value on Claude Opus 4.7 and +/// later models, including Claude Opus 5.5, returns a 400 error". That +/// covers the Claude 5 generation (`claude-opus-5(-5)`, `claude-sonnet-5(-5)`) +/// and Haiku 5.5 (which admits only its defaults — `temperature: 1`, a lone +/// `top_p: 0.99`; `top_p: 1`, any `top_k`, or both params together 400), +/// besides the Fable/Mythos lines. Dropping the params entirely is always +/// safe; sending a forbidden one bricks the request. Re-checked 2026-10-08. fn anthropic_model_rejects_sampling(model: &str) -> bool { let lower = model.to_ascii_lowercase(); lower.contains("opus-4-7") || lower.contains("opus-4-8") + || ["opus-5", "sonnet-5", "haiku-5"] + .iter() + .any(|family| lower.contains(family)) || lower.contains("fable") || lower.contains("mythos") } @@ -1801,15 +1817,39 @@ mod tests { assert!(body.get("temperature").is_none(), "{body}"); assert!(body.get("top_p").is_none(), "{body}"); + // The Claude 5 generation and Haiku 5.5 reject sampling too. The + // datestamped spelling pins the substring match against snapshot + // suffixes. + for model in [ + "claude-opus-5", + "claude-opus-5-5", + "claude-opus-5-20260101", + "claude-sonnet-5", + "claude-sonnet-5-5", + "claude-haiku-5-5", + "claude-fable-5", + "claude-mythos-5", + ] { + let body = + client.build_anthropic_body(&request_with(model, None, Some(0.7), Some(0.9)), true); + assert!( + body.get("temperature").is_none() && body.get("top_p").is_none(), + "{model} must not receive sampling params: {body}" + ); + } + // Older models accept ONE of temperature / top_p (temperature wins). - let body = client.build_anthropic_body( - &request_with("claude-sonnet-4-6", None, Some(0.7), Some(0.9)), - true, - ); - assert_eq!( - body.get("temperature").and_then(Value::as_f64), - Some(f64::from(0.7f32)) - ); + // The pre-4.7 generations stay ungated, so a future over-broad arm + // like `contains("opus-4-")` cannot slip through. + for model in ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"] { + let body = + client.build_anthropic_body(&request_with(model, None, Some(0.7), Some(0.9)), true); + assert_eq!( + body.get("temperature").and_then(Value::as_f64), + Some(f64::from(0.7f32)), + "{model} keeps temperature" + ); + } assert!(body.get("top_p").is_none(), "never send both: {body}"); }