Wire K3 thinking-effort levels end to end (fixes 'model does not support reasoning effort')
The live /models wire (verified against api.kimi.com) marks every Kimi
Code model supports_thinking_type: "only" and gives K3 a think_efforts
block {support, valid_efforts: [low, high, max], default_effort: max} —
both of which the F4 sync discarded, hardcoding
supports_reasoning_effort: false. Every effort selection was therefore
rejected with 'current model does not support reasoning effort'.
- kigi-models: WireModel gains supports_thinking_type + WireThinkEfforts;
"only" forces the always_thinking capability.
- models_fetch: think_efforts maps into the catalog entry — wire tokens
stay the option ids/labels (max/Max), canonical values map via the
ReasoningEffort parser (max → Xhigh), default_effort marks the default.
This lights up the existing /model <model> [effort] two-phase completion
and the /effort menu with the server's own vocabulary.
- kimi_compat: the effort level rides the wire as thinking.effort
({"type": "enabled", "effort": "low"} is accepted live; invalid
levels are a 400). Only the canonical-vs-wire spelling divergence
(xhigh → max) is renamed; levels pass through verbatim so a contract
violation surfaces instead of being clamped away.
Live acceptance: kigi -m kimi-code/k3 --reasoning-effort max -p ... round
trips against api.kimi.com, and the refreshed models_cache.json carries
low/high/max with the max default.
This commit is contained in:
@@ -32,12 +32,21 @@ pub(crate) fn adapt_chat_completions_body(body: &mut Value) {
|
||||
/// Map the OpenAI-style `reasoning_effort` knob onto Kimi's `thinking`
|
||||
/// request field and drop `reasoning_effort` from the wire.
|
||||
///
|
||||
/// kimi-cli controls thinking exclusively through the request body's
|
||||
/// kimi-cli 1.49.0 controls thinking through the request body's
|
||||
/// `thinking: {"type": "enabled" | "disabled"}` field
|
||||
/// (packages/kosong/src/kosong/chat_provider/kimi.py:214-223 `with_thinking`:
|
||||
/// `"enabled" if effort != "off" else "disabled"`; wired by
|
||||
/// src/kimi_cli/llm.py:475-481). When no effort is configured, nothing is
|
||||
/// sent and the server default applies (llm.py:482 "leave as-is").
|
||||
///
|
||||
/// Models with selectable levels (the `/models` `think_efforts` block, e.g.
|
||||
/// K3's low/high/max) additionally take the level as `thinking.effort` —
|
||||
/// verified against the live api.kimi.com: `{"type": "enabled", "effort":
|
||||
/// "low"}` is accepted, values outside `valid_efforts` are a 400. The
|
||||
/// catalog gates efforts to that per-model list, so this layer only renames
|
||||
/// the one canonical-vs-wire divergence (`xhigh` → `max`) and passes the
|
||||
/// level through verbatim — inventing or clamping a level here would hide a
|
||||
/// real contract violation.
|
||||
fn adapt_thinking(body: &mut Value) {
|
||||
let Some(obj) = body.as_object_mut() else {
|
||||
return;
|
||||
@@ -45,11 +54,22 @@ fn adapt_thinking(body: &mut Value) {
|
||||
let Some(effort) = obj.remove("reasoning_effort") else {
|
||||
return;
|
||||
};
|
||||
let enabled = effort.as_str() != Some("none");
|
||||
obj.insert(
|
||||
"thinking".to_owned(),
|
||||
serde_json::json!({ "type": if enabled { "enabled" } else { "disabled" } }),
|
||||
let effort = effort.as_str().map(str::to_owned);
|
||||
let enabled = effort.as_deref() != Some("none");
|
||||
let mut thinking = serde_json::Map::new();
|
||||
thinking.insert(
|
||||
"type".to_owned(),
|
||||
Value::String(if enabled { "enabled" } else { "disabled" }.to_owned()),
|
||||
);
|
||||
if enabled && let Some(level) = effort {
|
||||
let wire_level = if level == "xhigh" {
|
||||
"max".to_owned()
|
||||
} else {
|
||||
level
|
||||
};
|
||||
thinking.insert("effort".to_owned(), Value::String(wire_level));
|
||||
}
|
||||
obj.insert("thinking".to_owned(), Value::Object(thinking));
|
||||
}
|
||||
|
||||
/// Message-level adaptations:
|
||||
@@ -262,12 +282,27 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_maps_to_kimi_thinking_field() {
|
||||
// Level rides along as thinking.effort (live wire: 200 with
|
||||
// {"type": "enabled", "effort": "low"}).
|
||||
let mut body = json!({ "model": "kimi-for-coding", "reasoning_effort": "high" });
|
||||
adapt_chat_completions_body(&mut body);
|
||||
assert_eq!(body.get("reasoning_effort"), None);
|
||||
assert_eq!(body["thinking"], json!({ "type": "enabled" }));
|
||||
assert_eq!(
|
||||
body["thinking"],
|
||||
json!({ "type": "enabled", "effort": "high" })
|
||||
);
|
||||
|
||||
// kimi.py:218: "off" (our ReasoningEffort::None) → disabled.
|
||||
// Canonical `xhigh` is spelled `max` on the Kimi wire (the K3
|
||||
// valid_efforts vocabulary is low/high/max).
|
||||
let mut body = json!({ "model": "k3", "reasoning_effort": "xhigh" });
|
||||
adapt_chat_completions_body(&mut body);
|
||||
assert_eq!(
|
||||
body["thinking"],
|
||||
json!({ "type": "enabled", "effort": "max" })
|
||||
);
|
||||
|
||||
// kimi.py:218: "off" (our ReasoningEffort::None) → disabled, and no
|
||||
// effort key (a disabled+effort combination would be contradictory).
|
||||
let mut body = json!({ "reasoning_effort": "none" });
|
||||
adapt_chat_completions_body(&mut body);
|
||||
assert_eq!(body["thinking"], json!({ "type": "disabled" }));
|
||||
|
||||
@@ -382,9 +382,12 @@ async fn request_carries_bearer_kigi_ua_and_kimi_dialect_body() {
|
||||
assert_eq!(body["stream"], json!(true));
|
||||
assert_eq!(body["stream_options"], json!({ "include_usage": true }));
|
||||
|
||||
// -- Thinking mapping (kimi.py:214-223): effort → thinking, no
|
||||
// reasoning_effort on the wire.
|
||||
assert_eq!(body["thinking"], json!({ "type": "enabled" }));
|
||||
// -- Thinking mapping (kimi.py:214-223 + live think_efforts wire):
|
||||
// effort → thinking {type, effort}, no reasoning_effort on the wire.
|
||||
assert_eq!(
|
||||
body["thinking"],
|
||||
json!({ "type": "enabled", "effort": "high" })
|
||||
);
|
||||
assert_eq!(body.get("reasoning_effort"), None);
|
||||
|
||||
// -- Message adaptations.
|
||||
|
||||
Reference in New Issue
Block a user