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("");
+ });
+ });
});