From 2ff11246ae54fc7c8160a3d723b94a432e0c6806 Mon Sep 17 00:00:00 2001 From: EdisonPVE Date: Thu, 18 Jun 2026 15:14:42 +0700 Subject: [PATCH] fix(kiro): honor thinking effort budgets Resolve the Kiro thinking budget from client intent (OpenAI reasoning_effort, OpenAI Responses reasoning.effort, Claude output_config.effort, and Claude thinking.budget_tokens) by reusing the shared thinkingUnified extractThinking parser, then inject the resolved budget into the Kiro thinking system prefix. Explicit none/off/disabled stops the prefix injection; synthetic -thinking aliases keep the default budget. Co-authored-by: Cursor --- open-sse/config/kiroConstants.js | 92 +++++++++---------- open-sse/translator/request/claude-to-kiro.js | 13 +-- open-sse/translator/request/openai-to-kiro.js | 10 +- tests/translator/claude-kiro-direct.test.js | 11 +++ tests/unit/openai-to-kiro.test.js | 82 +++++++++++++++++ 5 files changed, 147 insertions(+), 61 deletions(-) diff --git a/open-sse/config/kiroConstants.js b/open-sse/config/kiroConstants.js index 707e46db..3ff6acbb 100644 --- a/open-sse/config/kiroConstants.js +++ b/open-sse/config/kiroConstants.js @@ -15,6 +15,9 @@ * fiction. The suffix is stripped before the request leaves this process. */ +import { extractThinking } from "../translator/concerns/thinkingUnified.js"; +import { effortToBudget } from "../translator/concerns/thinking.js"; + export const KIRO_AGENTIC_SUFFIX = "-agentic"; export const KIRO_THINKING_SUFFIX = "-thinking"; @@ -89,16 +92,48 @@ REMEMBER: When in doubt, write LESS per operation. Multiple small operations > o `.trim(); /** - * Detect whether an inbound request is asking for reasoning / thinking output. + * Resolve the Kiro thinking budget requested by a client. * - * Sources of intent (any one is enough): - * - HTTP header `Anthropic-Beta: ...interleaved-thinking...` - * - JSON `thinking.type === "enabled"` (Claude Messages API) - * - JSON `reasoning_effort` in {low, medium, high, auto} (OpenAI o1/o3) - * - JSON `reasoning.effort` in {low, medium, high, auto} (OpenAI Responses) - * - System prompt contains `enabled` or - * `interleaved` (AMP / Cursor) - * - Model name contains `thinking` or `-reason` + * Reuses the shared thinkingUnified parser (extractThinking) so every client + * shape (Claude output_config.effort / thinking.budget_tokens, OpenAI + * reasoning_effort / reasoning.effort, Gemini, Qwen) maps consistently. Explicit + * `none`/`off`/disabled wins and returns null (no prefix injected). + * buildThinkingSystemPrefix performs Kiro's final 1..32000 clamp. + * + * @param {object} body OpenAI/Claude-shaped request body + * @param {object} [headers] Original inbound HTTP headers (case-insensitive) + * @param {string} [model] Model id the caller asked for + * @returns {number|null} budget to inject, or null when thinking is disabled + */ +export function resolveKiroThinkingBudget(body, headers, model) { + const cfg = extractThinking(body); + if (cfg) { + if (cfg.mode === "none") 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; + } + + if (headers) { + const beta = pickHeader(headers, "anthropic-beta"); + if (typeof beta === "string" && beta.toLowerCase().includes("interleaved-thinking")) { + return KIRO_THINKING_BUDGET_DEFAULT; + } + } + + if (containsThinkingModeTag(body)) return KIRO_THINKING_BUDGET_DEFAULT; + + if (typeof model === "string" && model) { + const m = model.toLowerCase(); + if (m.includes("thinking") || m.includes("-reason")) return KIRO_THINKING_BUDGET_DEFAULT; + } + + return null; +} + +/** + * Detect whether an inbound request is asking for reasoning / thinking output. + * Thin wrapper over resolveKiroThinkingBudget (single source of truth). * * @param {object} body OpenAI-shaped request body (post-translation) * @param {object} [headers] Original inbound HTTP headers (case-insensitive) @@ -106,44 +141,7 @@ REMEMBER: When in doubt, write LESS per operation. Multiple small operations > o * @returns {boolean} */ export function isThinkingEnabled(body, headers, model) { - if (headers) { - const beta = pickHeader(headers, "anthropic-beta"); - if (typeof beta === "string" && beta.toLowerCase().includes("interleaved-thinking")) { - return true; - } - } - - if (body && typeof body === "object") { - const thinking = body.thinking; - if (thinking && typeof thinking === "object" && thinking.type === "enabled") { - const budget = Number(thinking.budget_tokens); - if (!Number.isFinite(budget) || budget > 0) { - return true; - } - } - - const effort = body.reasoning_effort - ?? (body.reasoning && typeof body.reasoning === "object" ? body.reasoning.effort : null); - if (typeof effort === "string") { - const v = effort.toLowerCase(); - if (v && v !== "none" && (v === "low" || v === "medium" || v === "high" || v === "auto")) { - return true; - } - } - - if (containsThinkingModeTag(body)) { - return true; - } - } - - if (typeof model === "string" && model) { - const m = model.toLowerCase(); - if (m.includes("thinking") || m.includes("-reason")) { - return true; - } - } - - return false; + return resolveKiroThinkingBudget(body, headers, model) !== null; } /** diff --git a/open-sse/translator/request/claude-to-kiro.js b/open-sse/translator/request/claude-to-kiro.js index aeb41365..8a38e4a9 100644 --- a/open-sse/translator/request/claude-to-kiro.js +++ b/open-sse/translator/request/claude-to-kiro.js @@ -27,7 +27,7 @@ import { FORMATS } from "../formats.js"; import { v4 as uuidv4 } from "uuid"; import { resolveKiroModel, - isThinkingEnabled, + resolveKiroThinkingBudget, buildThinkingSystemPrefix, KIRO_AGENTIC_SYSTEM_PROMPT, resolveDefaultProfileArn, @@ -374,13 +374,8 @@ export function claudeToKiroRequest(model, body, stream, credentials) { const temperature = body.temperature; const topP = body.top_p; - const { - upstream: upstreamModel, - agentic, - thinking: modelImpliesThinking, - } = resolveKiroModel(model); - const thinkingEnabled = - modelImpliesThinking || isThinkingEnabled(body, null, model); + const { upstream: upstreamModel, agentic } = resolveKiroModel(model); + const thinkingBudget = resolveKiroThinkingBudget(body, credentials?.rawHeaders, model); // Guard 1: no client tools → flatten all tool interactions to text. if (!clientProvidedTools) { @@ -420,7 +415,7 @@ export function claudeToKiroRequest(model, body, stream, credentials) { // Prefix order: thinking_mode tag, timestamp marker, then agentic prompt. const timestamp = new Date().toISOString(); const prefixParts = []; - if (thinkingEnabled) prefixParts.push(buildThinkingSystemPrefix()); + if (thinkingBudget !== null) prefixParts.push(buildThinkingSystemPrefix(thinkingBudget)); prefixParts.push(`[Context: Current time is ${timestamp}]`); if (agentic) prefixParts.push(KIRO_AGENTIC_SYSTEM_PROMPT); finalContent = `${prefixParts.join("\n\n")}\n\n${finalContent}`; diff --git a/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js index b15fbeae..e3e7f475 100644 --- a/open-sse/translator/request/openai-to-kiro.js +++ b/open-sse/translator/request/openai-to-kiro.js @@ -8,7 +8,7 @@ import { v4 as uuidv4 } from "uuid"; import { resolveSessionId } from "../../utils/sessionManager.js"; import { resolveKiroModel, - isThinkingEnabled, + resolveKiroThinkingBudget, buildThinkingSystemPrefix, KIRO_AGENTIC_SYSTEM_PROMPT, resolveDefaultProfileArn @@ -519,8 +519,8 @@ export function openaiToKiroRequest(model, body, stream, credentials) { const temperature = body.temperature; const topP = body.top_p; - const { upstream: upstreamModel, agentic, thinking: modelImpliesThinking } = resolveKiroModel(model); - const thinkingEnabled = modelImpliesThinking || isThinkingEnabled(body, null, model); + const { upstream: upstreamModel, agentic } = resolveKiroModel(model); + const thinkingBudget = resolveKiroThinkingBudget(body, credentials?.rawHeaders, model); const { history, currentMessage } = convertMessages(messages, tools, upstreamModel); @@ -543,8 +543,8 @@ export function openaiToKiroRequest(model, body, stream, credentials) { // Order: thinking_mode tag first (so Kiro sees it before any user text), // then context/timestamp marker, then optional agentic chunked-write prompt. const prefixParts = []; - if (thinkingEnabled) { - prefixParts.push(buildThinkingSystemPrefix()); + if (thinkingBudget !== null) { + prefixParts.push(buildThinkingSystemPrefix(thinkingBudget)); } prefixParts.push(`[Context: Current time is ${timestamp}]`); if (agentic) { diff --git a/tests/translator/claude-kiro-direct.test.js b/tests/translator/claude-kiro-direct.test.js index be98eafc..7e01951f 100644 --- a/tests/translator/claude-kiro-direct.test.js +++ b/tests/translator/claude-kiro-direct.test.js @@ -64,6 +64,17 @@ describe("Claude → Kiro (direct route)", () => { "enabled" ); }); + + it("maps output_config.effort high to Kiro max_thinking_length 24576", () => { + const out = C2K({ + output_config: { effort: "high" }, + messages: [{ role: "user", content: "think with adaptive effort" }], + }); + + expect(out.conversationState.currentMessage.userInputMessage.content).toContain( + "24576" + ); + }); }); describe("Kiro → Claude (direct route, OpenAI-shaped chunks from executor)", () => { diff --git a/tests/unit/openai-to-kiro.test.js b/tests/unit/openai-to-kiro.test.js index 4b31b249..ddda87ab 100644 --- a/tests/unit/openai-to-kiro.test.js +++ b/tests/unit/openai-to-kiro.test.js @@ -9,6 +9,9 @@ import { describe, it, expect } from "vitest"; import { openaiToKiroRequest } from "../../open-sse/translator/request/openai-to-kiro.js"; +const contentOf = (result) => + result.conversationState.currentMessage.userInputMessage.content; + describe("openaiToKiroRequest", () => { describe("basic message conversion", () => { it("should convert a simple text message", () => { @@ -280,4 +283,83 @@ describe("openaiToKiroRequest", () => { expect(allJson).toContain("[Tool result: important orphaned output]"); }); }); + + describe("thinking budget", () => { + it("maps reasoning_effort low to max_thinking_length 1024", () => { + const body = { + reasoning_effort: "low", + messages: [{ role: "user", content: "Think lightly" }] + }; + + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); + + expect(contentOf(result)).toContain("1024"); + }); + + it("maps reasoning_effort high to max_thinking_length 24576", () => { + const body = { + reasoning_effort: "high", + messages: [{ role: "user", content: "Think deeply" }] + }; + + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); + + expect(contentOf(result)).toContain("24576"); + }); + + it("clamps reasoning_effort max to Kiro max_thinking_length 32000", () => { + const body = { + reasoning_effort: "max", + messages: [{ role: "user", content: "Think as much as possible" }] + }; + + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); + + expect(contentOf(result)).toContain("32000"); + }); + + it("clamps OpenAI Responses reasoning.effort xhigh to max_thinking_length 32000", () => { + const body = { + reasoning: { effort: "xhigh" }, + messages: [{ role: "user", content: "Think extra deeply" }] + }; + + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); + + expect(contentOf(result)).toContain("32000"); + }); + + it("uses Claude thinking.budget_tokens as max_thinking_length", () => { + const body = { + thinking: { type: "enabled", budget_tokens: 4096 }, + messages: [{ role: "user", content: "Use a fixed budget" }] + }; + + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); + + expect(contentOf(result)).toContain("4096"); + }); + + it("uses the default budget for synthetic -thinking models with no explicit config", () => { + const body = { + messages: [{ role: "user", content: "Think by model suffix" }] + }; + + const result = openaiToKiroRequest("claude-sonnet-4.6-thinking", body, true, {}); + + expect(contentOf(result)).toContain("16000"); + }); + + it("does not inject thinking prefix for reasoning_effort none", () => { + const body = { + reasoning_effort: "none", + messages: [{ role: "user", content: "Do not think" }] + }; + + const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {}); + + expect(contentOf(result)).not.toContain("enabled"); + expect(contentOf(result)).not.toContain(""); + }); + }); });