fix(thinking): add xhigh to claude-adaptive thinking levels

Expose xhigh in the level picker for claude-adaptive models (Opus 4.7+,
Sonnet 5, Opus 5/5.5, Fable) and make the wire actually send it instead
of silently clamping to high:

- thinkingLevels: claude-adaptive now uses the budgetX set; Opus/Sonnet
  4.6 keep low..max (both Anthropic and Kiro reject xhigh there)
- thinkingUnified: pass xhigh through when the model advertises it,
  clamp to high otherwise
- kiroConstants: pass xhigh/max through per Kiro docs + live
  additionalModelRequestFieldsSchema tiers; 4.6 models clamp xhigh to
  high (max stays valid). Fixes max being a silent no-op on Kiro.

Kimi stays on levelMax.
This commit is contained in:
Alecto1b authored and decolua committed 2026-10-01 10:05:35 +07:00
1 parent 068ce87d20
commit 7894f3d36a
4 files changed
+56 -14

No files matched your search

+24 -10
View File
@@ -171,7 +171,22 @@ export function resolveKiroThinkingBudget(body, headers, model) {
return null;
}
export function extractKiroEffortLevel(body) {
function parseClaudeVersion(model) {
if (typeof model !== "string") return null;
const normalized = model.toLowerCase().replace(/-/g, ".");
const match = normalized.match(/(?:^|[/.])claude(?:[/.][a-z]+)*[/.](\d+)(?:[/.](\d+))?(?:[/.]|$)/);
if (!match) return null;
return { major: Number(match[1]), minor: match[2] === undefined ? null : Number(match[2]) };
}
// Kiro effort tiers per model (kiro.dev docs + live additionalModelRequestFieldsSchema):
// 4.6 Claude models cap at low|medium|high|max; 4.7+ add xhigh. Unknown models stay conservative.
function kiroModelLacksXhigh(model) {
const v = parseClaudeVersion(model);
return !v || (v.major === 4 && v.minor !== null && v.minor <= 6);
}
export function extractKiroEffortLevel(body, model) {
const effort =
body?.output_config?.effort ??
body?.reasoning_effort ??
@@ -179,7 +194,8 @@ export function extractKiroEffortLevel(body) {
if (typeof effort !== "string") return null;
const normalized = effort.toLowerCase();
if (normalized === "none" || normalized === "off" || normalized === "disabled") return null;
if (normalized === "xhigh" || normalized === "max") return "high";
if (normalized === "xhigh") return kiroModelLacksXhigh(model) ? "high" : "xhigh";
if (normalized === "max") return "max";
if (["low", "medium", "high"].includes(normalized)) return normalized;
return null;
}
@@ -199,10 +215,10 @@ function extractKiroGptEffortLevel(body) {
return null;
}
export function buildKiroAdditionalModelRequestFields(body, effortPath = "output_config") {
export function buildKiroAdditionalModelRequestFields(body, effortPath = "output_config", model) {
const effort = effortPath === "reasoning"
? extractKiroGptEffortLevel(body)
: extractKiroEffortLevel(body);
: extractKiroEffortLevel(body, model);
if (!effort) return undefined;
if (effortPath === "reasoning") {
// Mirrors Kiro CLI/KAS buildEffortRequestFields("reasoning") for GPT.
@@ -222,11 +238,9 @@ export function resolveKiroEffortPath(model) {
return "reasoning";
}
if (!normalized.includes("claude")) return null;
const match = normalized.match(/(?:^|[/.])claude(?:[/.][a-z]+)*[/.](\d+)(?:[/.](\d+))?(?:[/.]|$)/);
if (!match) return null;
const [, majorText, minorText] = match;
const major = Number(majorText);
const minor = minorText === undefined ? null : Number(minorText);
const v = parseClaudeVersion(model);
if (!v) return null;
const { major, minor } = v;
const dateSuffixMinor = minor !== null && minor >= 1000;
// Kiro rejected additionalModelRequestFields on legacy 4.5 models in live smoke.
// Default future Claude/Kiro models to supported so new model releases do not
@@ -248,7 +262,7 @@ export function usesKiroNativeGptEffort(body, model) {
export function buildKiroAdditionalModelRequestFieldsForModel(body, model) {
const effortPath = resolveKiroEffortPath(model);
if (!effortPath) return undefined;
return buildKiroAdditionalModelRequestFields(body, effortPath);
return buildKiroAdditionalModelRequestFields(body, effortPath, model);
}
/**
+8 -3
View File
@@ -10,8 +10,8 @@ const L = {
base: ["none", "low", "medium", "high"], // qwen, step, hunyuan, gemini-budget
onOff: ["none", "thinking"], // zai (binary), minimax (adaptive)
openai: ["none", "minimal", "low", "medium", "high", "xhigh"], // GPT-5.x / o-series (no "max")
levelMax: ["none", "low", "medium", "high", "max"], // claude-adaptive, kimi
budgetX: ["none", "low", "medium", "high", "xhigh", "max"], // claude-budget
levelMax: ["none", "low", "medium", "high", "max"], // kimi
budgetX: ["none", "low", "medium", "high", "xhigh", "max"], // claude-budget, claude-adaptive
gemini: ["minimal", "low", "medium", "high"], // gemini-3 thinkingLevel (no disable)
hiMax: ["none", "high", "max"], // deepseek (low/med→high, xhigh→max)
};
@@ -19,7 +19,7 @@ const L = {
// thinkingFormat → valid selectable levels (source of truth for UI options).
const FORMAT_LEVELS = {
openai: L.openai,
"claude-adaptive": L.levelMax,
"claude-adaptive": L.budgetX,
"claude-budget": L.budgetX,
"gemini-level": L.gemini,
"gemini-budget": L.base,
@@ -35,8 +35,13 @@ const FORMAT_LEVELS = {
const CODEX_GPT_5_6_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
// Opus/Sonnet 4.6 lack xhigh (Anthropic + Kiro docs) — keep the 4-level+max set.
const CLAUDE_NO_XHIGH = ["none", "low", "medium", "high", "max"];
// Model-name pattern overrides (glob, first match wins) — more precise than format default.
const PATTERN_THINKING = [
{ pattern: "*claude*4.6*", levels: CLAUDE_NO_XHIGH },
{ pattern: "*claude*4-6*", levels: CLAUDE_NO_XHIGH },
{ provider: "codex", pattern: "*gpt-6*", levels: CODEX_GPT_5_6_LEVELS },
{ provider: "codex", pattern: "*gpt-5.6-sol*", levels: [...CODEX_GPT_5_6_LEVELS, "ultra"] },
{ provider: "codex", pattern: "*gpt-5.6-terra*", levels: [...CODEX_GPT_5_6_LEVELS, "ultra"] },
@@ -271,7 +271,9 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels, display) {
if (canDisable) body.thinking = { type: "adaptive", ...(display ? { display } : {}) };
else delete body.thinking;
const level = toLevel(eff);
body.output_config = { effort: level === "xhigh" || level === "auto" ? "high" : level };
// xhigh is model-gated (Opus/Sonnet 4.6 reject it) — clamp when not advertised.
body.output_config = { effort: level === "auto" ? "high"
: level === "xhigh" && !supportedLevels?.includes("xhigh") ? "high" : level };
break;
}
case "claude-budget": {
+21
View File
@@ -1,5 +1,7 @@
import { describe, it, expect } from "vitest";
import { getThinkingLevels } from "../../open-sse/providers/thinkingLevels.js";
import { buildKiroAdditionalModelRequestFieldsForModel } from "../../open-sse/config/kiroConstants.js";
import { applyThinking } from "../../open-sse/translator/concerns/thinkingUnified.js";
describe("getThinkingLevels for Kiro", () => {
it("does not advertise native intensity for legacy Kiro models", () => {
@@ -9,6 +11,25 @@ describe("getThinkingLevels for Kiro", () => {
it("advertises native levels for supported Kiro models", () => {
expect(getThinkingLevels("kiro", "claude-sonnet-5")).toContain("high");
expect(getThinkingLevels("kiro", "claude-sonnet-5")).toContain("xhigh");
expect(getThinkingLevels("kiro", "claude-sonnet-5")).toContain("max");
expect(getThinkingLevels("kiro", "gpt-5.6-sol")).toContain("xhigh");
});
it("omits xhigh on 4.6 models (upstream rejects it there)", () => {
for (const model of ["claude-opus-4.6", "claude-opus-4-6", "claude-sonnet-4.6"]) {
expect(getThinkingLevels("kiro", model)).not.toContain("xhigh");
expect(getThinkingLevels("kiro", model)).toContain("max");
}
});
it("passes xhigh/max through on the wire for 4.7+, clamps xhigh on 4.6", () => {
const xhigh = { output_config: { effort: "xhigh" } };
expect(buildKiroAdditionalModelRequestFieldsForModel(xhigh, "claude-sonnet-5")?.output_config?.effort).toBe("xhigh");
expect(buildKiroAdditionalModelRequestFieldsForModel({ output_config: { effort: "max" } }, "claude-opus-4.6")?.output_config?.effort).toBe("max");
expect(buildKiroAdditionalModelRequestFieldsForModel(xhigh, "claude-opus-4.6")?.output_config?.effort).toBe("high");
// Anthropic-wire path: suffix override sends real xhigh on 4.7+, high on 4.6.
expect(applyThinking("claude", "claude-opus-5.5(xhigh)", { messages: [] }, "claude").output_config?.effort).toBe("xhigh");
expect(applyThinking("claude", "claude-opus-4.6(xhigh)", { messages: [] }, "claude").output_config?.effort).toBe("high");
});
});