diff --git a/open-sse/config/kiroConstants.js b/open-sse/config/kiroConstants.js index e6408da1..f24cce04 100644 --- a/open-sse/config/kiroConstants.js +++ b/open-sse/config/kiroConstants.js @@ -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); } /** diff --git a/open-sse/providers/thinkingLevels.js b/open-sse/providers/thinkingLevels.js index 0b020100..5ecf2854 100644 --- a/open-sse/providers/thinkingLevels.js +++ b/open-sse/providers/thinkingLevels.js @@ -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"] }, diff --git a/open-sse/translator/concerns/thinkingUnified.js b/open-sse/translator/concerns/thinkingUnified.js index 5ce73b32..4bcacd3b 100644 --- a/open-sse/translator/concerns/thinkingUnified.js +++ b/open-sse/translator/concerns/thinkingUnified.js @@ -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": { diff --git a/tests/unit/thinking-levels-kiro.test.js b/tests/unit/thinking-levels-kiro.test.js index edb3b7aa..b1ff2acb 100644 --- a/tests/unit/thinking-levels-kiro.test.js +++ b/tests/unit/thinking-levels-kiro.test.js @@ -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"); + }); });