mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
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:
committed by
decolua
co-authored by
Cursor
parent
3f9382dee4
commit
2ff11246ae
@@ -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)", () => {
|
||||
|
||||
@@ -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>");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user