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 <cursoragent@cursor.com>
This commit is contained in:
EdisonPVE
2026-06-18 15:14:42 +07:00
committed by decolua
co-authored by Cursor
parent 3f9382dee4
commit 2ff11246ae
5 changed files with 147 additions and 61 deletions
+45 -47
View File
@@ -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 `<thinking_mode>enabled</thinking_mode>` or
* `<thinking_mode>interleaved</thinking_mode>` (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;
}
/**
@@ -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}`;
@@ -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) {
@@ -64,6 +64,17 @@ describe("Claude → Kiro (direct route)", () => {
"<thinking_mode>enabled</thinking_mode>"
);
});
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(
"<max_thinking_length>24576</max_thinking_length>"
);
});
});
describe("Kiro → Claude (direct route, OpenAI-shaped chunks from executor)", () => {
+82
View File
@@ -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("<max_thinking_length>1024</max_thinking_length>");
});
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("<max_thinking_length>24576</max_thinking_length>");
});
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("<max_thinking_length>32000</max_thinking_length>");
});
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("<max_thinking_length>32000</max_thinking_length>");
});
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("<max_thinking_length>4096</max_thinking_length>");
});
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("<max_thinking_length>16000</max_thinking_length>");
});
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("<thinking_mode>enabled</thinking_mode>");
expect(contentOf(result)).not.toContain("<max_thinking_length>");
});
});
});