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:
1 parent
068ce87d20
commit
7894f3d36a
4 files changed
+56
-14
No files matched your search
@@ -171,7 +171,22 @@ export function resolveKiroThinkingBudget(body, headers, model) {
|
|||||||
return null;
|
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 =
|
const effort =
|
||||||
body?.output_config?.effort ??
|
body?.output_config?.effort ??
|
||||||
body?.reasoning_effort ??
|
body?.reasoning_effort ??
|
||||||
@@ -179,7 +194,8 @@ export function extractKiroEffortLevel(body) {
|
|||||||
if (typeof effort !== "string") return null;
|
if (typeof effort !== "string") return null;
|
||||||
const normalized = effort.toLowerCase();
|
const normalized = effort.toLowerCase();
|
||||||
if (normalized === "none" || normalized === "off" || normalized === "disabled") return null;
|
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;
|
if (["low", "medium", "high"].includes(normalized)) return normalized;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -199,10 +215,10 @@ function extractKiroGptEffortLevel(body) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildKiroAdditionalModelRequestFields(body, effortPath = "output_config") {
|
export function buildKiroAdditionalModelRequestFields(body, effortPath = "output_config", model) {
|
||||||
const effort = effortPath === "reasoning"
|
const effort = effortPath === "reasoning"
|
||||||
? extractKiroGptEffortLevel(body)
|
? extractKiroGptEffortLevel(body)
|
||||||
: extractKiroEffortLevel(body);
|
: extractKiroEffortLevel(body, model);
|
||||||
if (!effort) return undefined;
|
if (!effort) return undefined;
|
||||||
if (effortPath === "reasoning") {
|
if (effortPath === "reasoning") {
|
||||||
// Mirrors Kiro CLI/KAS buildEffortRequestFields("reasoning") for GPT.
|
// Mirrors Kiro CLI/KAS buildEffortRequestFields("reasoning") for GPT.
|
||||||
@@ -222,11 +238,9 @@ export function resolveKiroEffortPath(model) {
|
|||||||
return "reasoning";
|
return "reasoning";
|
||||||
}
|
}
|
||||||
if (!normalized.includes("claude")) return null;
|
if (!normalized.includes("claude")) return null;
|
||||||
const match = normalized.match(/(?:^|[/.])claude(?:[/.][a-z]+)*[/.](\d+)(?:[/.](\d+))?(?:[/.]|$)/);
|
const v = parseClaudeVersion(model);
|
||||||
if (!match) return null;
|
if (!v) return null;
|
||||||
const [, majorText, minorText] = match;
|
const { major, minor } = v;
|
||||||
const major = Number(majorText);
|
|
||||||
const minor = minorText === undefined ? null : Number(minorText);
|
|
||||||
const dateSuffixMinor = minor !== null && minor >= 1000;
|
const dateSuffixMinor = minor !== null && minor >= 1000;
|
||||||
// Kiro rejected additionalModelRequestFields on legacy 4.5 models in live smoke.
|
// Kiro rejected additionalModelRequestFields on legacy 4.5 models in live smoke.
|
||||||
// Default future Claude/Kiro models to supported so new model releases do not
|
// 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) {
|
export function buildKiroAdditionalModelRequestFieldsForModel(body, model) {
|
||||||
const effortPath = resolveKiroEffortPath(model);
|
const effortPath = resolveKiroEffortPath(model);
|
||||||
if (!effortPath) return undefined;
|
if (!effortPath) return undefined;
|
||||||
return buildKiroAdditionalModelRequestFields(body, effortPath);
|
return buildKiroAdditionalModelRequestFields(body, effortPath, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ const L = {
|
|||||||
base: ["none", "low", "medium", "high"], // qwen, step, hunyuan, gemini-budget
|
base: ["none", "low", "medium", "high"], // qwen, step, hunyuan, gemini-budget
|
||||||
onOff: ["none", "thinking"], // zai (binary), minimax (adaptive)
|
onOff: ["none", "thinking"], // zai (binary), minimax (adaptive)
|
||||||
openai: ["none", "minimal", "low", "medium", "high", "xhigh"], // GPT-5.x / o-series (no "max")
|
openai: ["none", "minimal", "low", "medium", "high", "xhigh"], // GPT-5.x / o-series (no "max")
|
||||||
levelMax: ["none", "low", "medium", "high", "max"], // claude-adaptive, kimi
|
levelMax: ["none", "low", "medium", "high", "max"], // kimi
|
||||||
budgetX: ["none", "low", "medium", "high", "xhigh", "max"], // claude-budget
|
budgetX: ["none", "low", "medium", "high", "xhigh", "max"], // claude-budget, claude-adaptive
|
||||||
gemini: ["minimal", "low", "medium", "high"], // gemini-3 thinkingLevel (no disable)
|
gemini: ["minimal", "low", "medium", "high"], // gemini-3 thinkingLevel (no disable)
|
||||||
hiMax: ["none", "high", "max"], // deepseek (low/med→high, xhigh→max)
|
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).
|
// thinkingFormat → valid selectable levels (source of truth for UI options).
|
||||||
const FORMAT_LEVELS = {
|
const FORMAT_LEVELS = {
|
||||||
openai: L.openai,
|
openai: L.openai,
|
||||||
"claude-adaptive": L.levelMax,
|
"claude-adaptive": L.budgetX,
|
||||||
"claude-budget": L.budgetX,
|
"claude-budget": L.budgetX,
|
||||||
"gemini-level": L.gemini,
|
"gemini-level": L.gemini,
|
||||||
"gemini-budget": L.base,
|
"gemini-budget": L.base,
|
||||||
@@ -35,8 +35,13 @@ const FORMAT_LEVELS = {
|
|||||||
|
|
||||||
const CODEX_GPT_5_6_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
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.
|
// Model-name pattern overrides (glob, first match wins) — more precise than format default.
|
||||||
const PATTERN_THINKING = [
|
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-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-sol*", levels: [...CODEX_GPT_5_6_LEVELS, "ultra"] },
|
||||||
{ provider: "codex", pattern: "*gpt-5.6-terra*", 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 } : {}) };
|
if (canDisable) body.thinking = { type: "adaptive", ...(display ? { display } : {}) };
|
||||||
else delete body.thinking;
|
else delete body.thinking;
|
||||||
const level = toLevel(eff);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case "claude-budget": {
|
case "claude-budget": {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { getThinkingLevels } from "../../open-sse/providers/thinkingLevels.js";
|
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", () => {
|
describe("getThinkingLevels for Kiro", () => {
|
||||||
it("does not advertise native intensity for legacy Kiro models", () => {
|
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", () => {
|
it("advertises native levels for supported Kiro models", () => {
|
||||||
expect(getThinkingLevels("kiro", "claude-sonnet-5")).toContain("high");
|
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");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user