From cef5dd4d614016ece664687f8892c4abbfb2d5ee Mon Sep 17 00:00:00 2001 From: Edison42 Date: Mon, 20 Jul 2026 11:08:23 +0700 Subject: [PATCH] fix(kiro): map GPT-5.6 reasoning effort fields Route GPT-5.6 reasoning effort through Kiro's native reasoning.effort field instead of the legacy Claude output_config.effort path. GPT-5.6 models now emit reasoning.effort for low/medium/high/xhigh, with max mapped to the xhigh wire value. Preserve the Responses API reasoning.effort through the OpenAI intermediate by copying it to reasoning_effort before the field is dropped. Skip legacy thinking_mode prompt tags when a supported native GPT effort is emitted, while keeping the legacy fallback for unsupported values (auto/minimal/ultra) and explicit disable semantics (none/off/disabled). Claude adaptive effort continues to use thinking plus output_config.effort. --- open-sse/config/kiroConstants.js | 29 ++++- open-sse/translator/request/claude-to-kiro.js | 8 +- .../translator/request/openai-responses.js | 3 + open-sse/translator/request/openai-to-kiro.js | 12 +- tests/translator/bugs-kiro.test.js | 26 +++++ tests/translator/claude-kiro-direct.test.js | 44 +++++++- tests/unit/openai-to-kiro.test.js | 106 ++++++++++++++---- 7 files changed, 193 insertions(+), 35 deletions(-) diff --git a/open-sse/config/kiroConstants.js b/open-sse/config/kiroConstants.js index 120b5e38..511ae919 100644 --- a/open-sse/config/kiroConstants.js +++ b/open-sse/config/kiroConstants.js @@ -8,8 +8,8 @@ * - `-agentic` model suffix detection + chunked-write system prompt * - reasoning / thinking trigger detection (Anthropic-Beta header, * Claude `thinking`, OpenAI `reasoning_effort`, AMP/Cursor magic tag) - * - the `enabled` system-prompt injection - * that turns Kiro reasoning on + * - schema-specific native effort fields for supported GPT and Claude models + * - legacy `` system-prompt injection for other models * * Kiro upstream does not advertise `-agentic` model IDs; they are a 9router * fiction. The suffix is stripped before the request leaves this process. @@ -109,6 +109,7 @@ export function resolveKiroThinkingBudget(body, headers, model) { const cfg = extractThinking(body); if (cfg) { if (cfg.mode === "none") return null; + if (cfg.mode === "level" && cfg.level === "disabled") return null; if (cfg.mode === "budget") return cfg.budget; if (cfg.mode === "level") return effortToBudget(cfg.level) ?? KIRO_THINKING_BUDGET_DEFAULT; return KIRO_THINKING_BUDGET_DEFAULT; @@ -144,8 +145,25 @@ export function extractKiroEffortLevel(body) { return null; } +function extractKiroGptEffortLevel(body) { + const effort = + body?.output_config?.effort ?? + body?.reasoning_effort ?? + (typeof body?.reasoning === "object" ? body.reasoning?.effort : null); + if (typeof effort !== "string") return null; + const normalized = effort.toLowerCase(); + if (normalized === "max") return "xhigh"; + // Kiro CLI does not advertise an explicit GPT "none" wire value; omit it. + if (["low", "medium", "high", "xhigh"].includes(normalized)) { + return normalized; + } + return null; +} + export function buildKiroAdditionalModelRequestFields(body, effortPath = "output_config") { - const effort = extractKiroEffortLevel(body); + const effort = effortPath === "reasoning" + ? extractKiroGptEffortLevel(body) + : extractKiroEffortLevel(body); if (!effort) return undefined; if (effortPath === "reasoning") { // Mirrors Kiro CLI/KAS buildEffortRequestFields("reasoning") for GPT. @@ -183,6 +201,11 @@ export function supportsKiroAdditionalModelRequestFields(model) { return resolveKiroEffortPath(model) !== null; } +export function usesKiroNativeGptEffort(body, model) { + return resolveKiroEffortPath(model) === "reasoning" + && extractKiroGptEffortLevel(body) !== null; +} + export function buildKiroAdditionalModelRequestFieldsForModel(body, model) { const effortPath = resolveKiroEffortPath(model); if (!effortPath) return undefined; diff --git a/open-sse/translator/request/claude-to-kiro.js b/open-sse/translator/request/claude-to-kiro.js index bcb8b97f..98531c73 100644 --- a/open-sse/translator/request/claude-to-kiro.js +++ b/open-sse/translator/request/claude-to-kiro.js @@ -33,6 +33,7 @@ import { KIRO_AGENTIC_SYSTEM_PROMPT, resolveDefaultProfileArn, buildKiroAdditionalModelRequestFieldsForModel, + usesKiroNativeGptEffort, } from "../../config/kiroConstants.js"; import { DEFAULT_IMAGE_MIME } from "../schema/index.js"; import { ROLE, CLAUDE_BLOCK } from "../schema/index.js"; @@ -390,6 +391,8 @@ export function claudeToKiroRequest(model, body, stream, credentials) { const { upstream: upstreamModel, agentic } = resolveKiroModel(model); const thinkingBudget = resolveKiroThinkingBudget(body, credentials?.rawHeaders, model); + const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel); + const usesNativeGptEffort = usesKiroNativeGptEffort(body, upstreamModel); // Guard 1: no client tools → flatten all tool interactions to text. if (!clientProvidedTools) { @@ -421,7 +424,9 @@ export function claudeToKiroRequest(model, body, stream, credentials) { // enforce top-level systemPrompt for direct calls. const timestamp = new Date().toISOString(); const systemPromptParts = []; - if (thinkingBudget !== null) systemPromptParts.push(buildThinkingSystemPrefix(thinkingBudget)); + if (thinkingBudget !== null && !usesNativeGptEffort) { + systemPromptParts.push(buildThinkingSystemPrefix(thinkingBudget)); + } if (agentic) systemPromptParts.push(KIRO_AGENTIC_SYSTEM_PROMPT); const systemInstruction = extractClaudeSystemText(body.system); if (systemInstruction) systemPromptParts.push(systemInstruction); @@ -481,7 +486,6 @@ export function claudeToKiroRequest(model, body, stream, credentials) { if (profileArn) payload.profileArn = profileArn; if (systemPrompt) payload.systemPrompt = systemPrompt; - const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel); if (additionalModelRequestFields) { payload.additionalModelRequestFields = additionalModelRequestFields; } diff --git a/open-sse/translator/request/openai-responses.js b/open-sse/translator/request/openai-responses.js index 1fc05825..64740c12 100644 --- a/open-sse/translator/request/openai-responses.js +++ b/open-sse/translator/request/openai-responses.js @@ -200,6 +200,9 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) delete result.include; delete result.prompt_cache_key; delete result.store; + if (typeof result.reasoning?.effort === "string") { + result.reasoning_effort = result.reasoning.effort; + } delete result.reasoning; delete result.client_metadata; diff --git a/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js index b4918537..069feffa 100644 --- a/open-sse/translator/request/openai-to-kiro.js +++ b/open-sse/translator/request/openai-to-kiro.js @@ -13,7 +13,8 @@ import { buildThinkingSystemPrefix, KIRO_AGENTIC_SYSTEM_PROMPT, resolveDefaultProfileArn, - buildKiroAdditionalModelRequestFieldsForModel + buildKiroAdditionalModelRequestFieldsForModel, + usesKiroNativeGptEffort } from "../../config/kiroConstants.js"; import { parseDataUri } from "../concerns/image.js"; import { DEFAULT_IMAGE_MIME } from "../schema/index.js"; @@ -513,8 +514,8 @@ function convertMessages(messages, tools, model) { * * 2. Thinking / reasoning. Detection covers Anthropic-Beta header, Claude API * `thinking`, OpenAI `reasoning_effort`, AMP/Cursor magic tags, and model - * name hints. Kiro's prompt tags remain for compatibility, while supported - * models also receive the same schema-specific effort fields as Kiro CLI. + * name hints. Supported models receive Kiro's schema-specific effort fields; + * legacy prompt tags remain only for models that need them. */ export function openaiToKiroRequest(model, body, stream, credentials) { const messages = body.messages || []; @@ -525,6 +526,8 @@ export function openaiToKiroRequest(model, body, stream, credentials) { const { upstream: upstreamModel, agentic } = resolveKiroModel(model); const thinkingBudget = resolveKiroThinkingBudget(body, credentials?.rawHeaders, model); + const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel); + const usesNativeGptEffort = usesKiroNativeGptEffort(body, upstreamModel); const { history, currentMessage } = convertMessages(messages, tools, upstreamModel); @@ -552,7 +555,7 @@ export function openaiToKiroRequest(model, body, stream, credentials) { // too because the CodeWhisperer surface does not always enforce top-level // systemPrompt for direct calls. const systemPromptParts = []; - if (thinkingBudget !== null) { + if (thinkingBudget !== null && !usesNativeGptEffort) { systemPromptParts.push(buildThinkingSystemPrefix(thinkingBudget)); } if (agentic) { @@ -610,7 +613,6 @@ export function openaiToKiroRequest(model, body, stream, credentials) { payload.profileArn = profileArn; } if (systemPrompt) payload.systemPrompt = systemPrompt; - const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel); if (additionalModelRequestFields) { payload.additionalModelRequestFields = additionalModelRequestFields; } diff --git a/tests/translator/bugs-kiro.test.js b/tests/translator/bugs-kiro.test.js index 7f3677fa..c3f777d4 100644 --- a/tests/translator/bugs-kiro.test.js +++ b/tests/translator/bugs-kiro.test.js @@ -5,8 +5,34 @@ import { translateRequest } from "../../open-sse/translator/index.js"; import { FORMATS } from "../../open-sse/translator/formats.js"; const O2K = (body) => translateRequest(FORMATS.OPENAI, FORMATS.KIRO, "m", body, true, null, "kiro"); +const R2K = (model, body) => translateRequest( + FORMATS.OPENAI_RESPONSES, + FORMATS.KIRO, + model, + body, + true, + null, + "kiro" +); describe("OpenAI → Kiro", () => { + it.each([ + ["high", "gpt-5.6-sol"], + ["medium", "gpt-5.6-terra"], + ["low", "gpt-5.6-luna"], + ])("preserves Responses reasoning.effort %s through the full Kiro route", (effort, model) => { + const out = R2K(model, { + input: "Use the requested effort", + reasoning: { effort }, + }); + + expect(out.additionalModelRequestFields).toEqual({ + reasoning: { effort }, + }); + expect(out.systemPrompt || "").not.toContain(""); + expect(out.systemPrompt || "").not.toContain(""); + }); + // openai-to-kiro.js — safeJSONParse guards bad tool-call JSON (fixed in PR #1582) it("malformed tool arguments do not throw the whole request", () => { expect(() => diff --git a/tests/translator/claude-kiro-direct.test.js b/tests/translator/claude-kiro-direct.test.js index 00d77456..3c2d964b 100644 --- a/tests/translator/claude-kiro-direct.test.js +++ b/tests/translator/claude-kiro-direct.test.js @@ -112,12 +112,54 @@ describe("Claude → Kiro (direct route)", () => { expect(out.systemPrompt).toContain("24576"); }); - it("maps Claude-format effort to GPT-5.6 Kiro CLI reasoning fields", () => { + it("maps Claude-format effort to GPT-5.6 reasoning fields without legacy prompt tags", () => { const out = C2K({ output_config: { effort: "low" }, messages: [{ role: "user", content: "think lightly" }], }, null, "gpt-5.6-sol"); + expect(out.additionalModelRequestFields).toEqual({ + reasoning: { effort: "low" }, + }); + expect(out.systemPrompt || "").not.toContain(""); + expect(out.systemPrompt || "").not.toContain(""); + }); + + it.each(["auto", "minimal", "ultra"])( + "keeps the legacy thinking fallback for unsupported GPT-5.6 effort %s", + (effort) => { + const out = C2K({ + output_config: { effort }, + messages: [{ role: "user", content: "Use legacy thinking" }], + }, null, "gpt-5.6-sol"); + + expect(out.additionalModelRequestFields).toBeUndefined(); + expect(out.systemPrompt).toContain("enabled"); + expect(out.systemPrompt).toContain(""); + } + ); + + it.each(["none", "off", "disabled"])( + "keeps GPT-5.6 reasoning intentionally disabled for effort %s", + (effort) => { + const out = C2K({ + output_config: { effort }, + messages: [{ role: "user", content: "Do not reason" }], + }, null, "gpt-5.6-sol"); + + expect(out.additionalModelRequestFields).toBeUndefined(); + expect(out.systemPrompt || "").not.toContain(""); + expect(out.systemPrompt || "").not.toContain(""); + } + ); + + it("keeps explicit Claude effort ahead of an injected OpenAI effort", () => { + const out = C2K({ + output_config: { effort: "low" }, + reasoning_effort: "high", + messages: [{ role: "user", content: "honor the client effort" }], + }, null, "gpt-5.6-sol"); + expect(out.additionalModelRequestFields).toEqual({ reasoning: { effort: "low" }, }); diff --git a/tests/unit/openai-to-kiro.test.js b/tests/unit/openai-to-kiro.test.js index 13f67bef..dbaf7c85 100644 --- a/tests/unit/openai-to-kiro.test.js +++ b/tests/unit/openai-to-kiro.test.js @@ -316,42 +316,100 @@ describe("openaiToKiroRequest", () => { }); }); - it("maps GPT-5.6 reasoning.effort high to Kiro CLI reasoning fields", () => { + it.each([ + ["high", "gpt-5.6-sol"], + ["medium", "kiro/gpt-5.6-terra"], + ["low", "gpt-5.6-luna"], + ])("maps GPT-5.6 reasoning.effort %s without legacy prompt tags", (effort, model) => { const body = { - reasoning: { effort: "high" }, - messages: [{ role: "user", content: "Think deeply" }] + reasoning: { effort }, + messages: [{ role: "user", content: "Use the requested effort" }] + }; + + const result = openaiToKiroRequest(model, body, true, {}); + + expect(result.additionalModelRequestFields).toEqual({ + reasoning: { effort }, + }); + expect(systemPromptOf(result)).not.toContain(""); + expect(systemPromptOf(result)).not.toContain(""); + expect(contentOf(result)).not.toContain(""); + expect(contentOf(result)).not.toContain(""); + }); + + it.each([ + ["xhigh", "gpt-5.6-terra", "xhigh"], + ["max", "gpt-5.6-sol", "xhigh"], + ])("preserves GPT-5.6 effort %s as supported wire effort %s", (effort, model, wireEffort) => { + const body = { + reasoning: { effort }, + messages: [{ role: "user", content: "Use extended effort" }] + }; + + const result = openaiToKiroRequest(model, body, true, {}); + + expect(result.additionalModelRequestFields).toEqual({ + reasoning: { effort: wireEffort }, + }); + expect(systemPromptOf(result)).not.toContain(""); + expect(systemPromptOf(result)).not.toContain(""); + }); + + it("omits GPT-5.6 effort fields and legacy prompt tags when effort is absent", () => { + const body = { + messages: [{ role: "user", content: "No explicit reasoning effort" }] }; const result = openaiToKiroRequest("gpt-5.6-sol", body, true, {}); - expect(systemPromptOf(result)).toContain("24576"); - expect(result.additionalModelRequestFields).toEqual({ - reasoning: { effort: "high" }, - }); + expect(result.additionalModelRequestFields).toBeUndefined(); + expect(systemPromptOf(result)).not.toContain(""); + expect(systemPromptOf(result)).not.toContain(""); }); - it("maps prefixed GPT-5.6 reasoning_effort medium to Kiro CLI reasoning fields", () => { + it.each(["auto", "minimal", "ultra"])( + "keeps the legacy thinking fallback for unsupported GPT-5.6 effort %s", + (effort) => { + const body = { + reasoning: { effort }, + messages: [{ role: "user", content: "Use legacy thinking" }] + }; + + const result = openaiToKiroRequest("gpt-5.6-luna", body, true, {}); + + expect(result.additionalModelRequestFields).toBeUndefined(); + expect(systemPromptOf(result)).toContain("enabled"); + expect(systemPromptOf(result)).toContain(""); + } + ); + + it.each(["none", "off", "disabled"])( + "keeps GPT-5.6 reasoning intentionally disabled for effort %s", + (effort) => { + const body = { + reasoning: { effort }, + messages: [{ role: "user", content: "Do not reason" }] + }; + + const result = openaiToKiroRequest("gpt-5.6-luna", body, true, {}); + + expect(result.additionalModelRequestFields).toBeUndefined(); + expect(systemPromptOf(result)).not.toContain(""); + expect(systemPromptOf(result)).not.toContain(""); + } + ); + + it("keeps the thinking-alias fallback when GPT effort is blank", () => { const body = { - reasoning_effort: "medium", - messages: [{ role: "user", content: "Think normally" }] + reasoning: { effort: "" }, + messages: [{ role: "user", content: "Use the thinking alias" }] }; - const result = openaiToKiroRequest("kiro/gpt-5.6-terra", body, true, {}); - - expect(result.additionalModelRequestFields).toEqual({ - reasoning: { effort: "medium" }, - }); - }); - - it("does not forward unsupported GPT-5.6 effort values", () => { - const body = { - reasoning: { effort: "ultra" }, - messages: [{ role: "user", content: "Unknown effort" }] - }; - - const result = openaiToKiroRequest("gpt-5.6-luna", body, true, {}); + const result = openaiToKiroRequest("gpt-5.6-sol-thinking", body, true, {}); expect(result.additionalModelRequestFields).toBeUndefined(); + expect(systemPromptOf(result)).toContain("enabled"); + expect(systemPromptOf(result)).toContain(""); }); it("does not send additionalModelRequestFields for legacy Kiro model ids", () => {