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) {