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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user