Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 48 additions & 11 deletions open-sse/config/kiroConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
* - `-agentic` model suffix detection + chunked-write system prompt
* - reasoning / thinking trigger detection (Anthropic-Beta header,
* Claude `thinking`, OpenAI `reasoning_effort`, AMP/Cursor magic tag)
* - the `<thinking_mode>enabled</thinking_mode>` system-prompt injection
* that turns Kiro reasoning on
* - schema-specific native effort fields for supported GPT and Claude models
* - legacy `<thinking_mode>` system-prompt injection for other models
*
* Kiro upstream does not advertise `-agentic` model IDs; they are a 9router
* fiction. The suffix is stripped before the request leaves this process.
Expand Down Expand Up @@ -109,6 +109,7 @@ export function resolveKiroThinkingBudget(body, headers, model) {
const cfg = extractThinking(body);
if (cfg) {
if (cfg.mode === "none") return null;
if (cfg.mode === "level" && cfg.level === "disabled") 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;
Expand Down Expand Up @@ -144,35 +145,71 @@ export function extractKiroEffortLevel(body) {
return null;
}

export function buildKiroAdditionalModelRequestFields(body) {
const effort = extractKiroEffortLevel(body);
function extractKiroGptEffortLevel(body) {
const effort =
body?.output_config?.effort ??
body?.reasoning_effort ??
(typeof body?.reasoning === "object" ? body.reasoning?.effort : null);
if (typeof effort !== "string") return null;
const normalized = effort.toLowerCase();
if (normalized === "max") return "xhigh";
// Kiro CLI does not advertise an explicit GPT "none" wire value; omit it.
if (["low", "medium", "high", "xhigh"].includes(normalized)) {
return normalized;
}
return null;
}

export function buildKiroAdditionalModelRequestFields(body, effortPath = "output_config") {
const effort = effortPath === "reasoning"
? extractKiroGptEffortLevel(body)
: extractKiroEffortLevel(body);
if (!effort) return undefined;
if (effortPath === "reasoning") {
// Mirrors Kiro CLI/KAS buildEffortRequestFields("reasoning") for GPT.
return { reasoning: { effort } };
}
// Mirrors Kiro CLI/KAS buildEffortRequestFields("output_config").
return {
thinking: { type: "adaptive", display: "summarized" },
output_config: { effort },
};
}

export function supportsKiroAdditionalModelRequestFields(model) {
if (typeof model !== "string") return false;
export function resolveKiroEffortPath(model) {
if (typeof model !== "string") return null;
const normalized = model.toLowerCase().replace(/-/g, ".");
if (!normalized.includes("claude")) return false;
if (/(?:^|[/.])gpt[/.]5[/.]6(?:[/.]|$)/.test(normalized)) {
return "reasoning";
}
if (!normalized.includes("claude")) return null;
const match = normalized.match(/(?:^|[/.])claude(?:[/.][a-z]+)*[/.](\d+)(?:[/.](\d+))?(?:[/.]|$)/);
if (!match) return false;
if (!match) return null;
const [, majorText, minorText] = match;
const major = Number(majorText);
const minor = minorText === undefined ? null : Number(minorText);
const dateSuffixMinor = minor !== null && minor >= 1000;
// Kiro rejected additionalModelRequestFields on legacy 4.5 models in live smoke.
// Default future Claude/Kiro models to supported so new model releases do not
// need a code allowlist update.
return !(major < 4 || (major === 4 && (minor === null || minor <= 5 || dateSuffixMinor)));
return major < 4 || (major === 4 && (minor === null || minor <= 5 || dateSuffixMinor))
? null
: "output_config";
}

export function supportsKiroAdditionalModelRequestFields(model) {
return resolveKiroEffortPath(model) !== null;
}

export function usesKiroNativeGptEffort(body, model) {
return resolveKiroEffortPath(model) === "reasoning"
&& extractKiroGptEffortLevel(body) !== null;
}

export function buildKiroAdditionalModelRequestFieldsForModel(body, model) {
if (!supportsKiroAdditionalModelRequestFields(model)) return undefined;
return buildKiroAdditionalModelRequestFields(body);
const effortPath = resolveKiroEffortPath(model);
if (!effortPath) return undefined;
return buildKiroAdditionalModelRequestFields(body, effortPath);
}

/**
Expand Down
8 changes: 6 additions & 2 deletions open-sse/translator/request/claude-to-kiro.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
KIRO_AGENTIC_SYSTEM_PROMPT,
resolveDefaultProfileArn,
buildKiroAdditionalModelRequestFieldsForModel,
usesKiroNativeGptEffort,
} from "../../config/kiroConstants.js";
import { DEFAULT_IMAGE_MIME } from "../schema/index.js";
import { ROLE, CLAUDE_BLOCK } from "../schema/index.js";
Expand Down Expand Up @@ -390,6 +391,8 @@ export function claudeToKiroRequest(model, body, stream, credentials) {

const { upstream: upstreamModel, agentic } = resolveKiroModel(model);
const thinkingBudget = resolveKiroThinkingBudget(body, credentials?.rawHeaders, model);
const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel);
const usesNativeGptEffort = usesKiroNativeGptEffort(body, upstreamModel);

// Guard 1: no client tools → flatten all tool interactions to text.
if (!clientProvidedTools) {
Expand Down Expand Up @@ -421,7 +424,9 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
// enforce top-level systemPrompt for direct calls.
const timestamp = new Date().toISOString();
const systemPromptParts = [];
if (thinkingBudget !== null) systemPromptParts.push(buildThinkingSystemPrefix(thinkingBudget));
if (thinkingBudget !== null && !usesNativeGptEffort) {
systemPromptParts.push(buildThinkingSystemPrefix(thinkingBudget));
}
if (agentic) systemPromptParts.push(KIRO_AGENTIC_SYSTEM_PROMPT);
const systemInstruction = extractClaudeSystemText(body.system);
if (systemInstruction) systemPromptParts.push(systemInstruction);
Expand Down Expand Up @@ -481,7 +486,6 @@ export function claudeToKiroRequest(model, body, stream, credentials) {

if (profileArn) payload.profileArn = profileArn;
if (systemPrompt) payload.systemPrompt = systemPrompt;
const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel);
if (additionalModelRequestFields) {
payload.additionalModelRequestFields = additionalModelRequestFields;
}
Expand Down
3 changes: 3 additions & 0 deletions open-sse/translator/request/openai-responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
delete result.include;
delete result.prompt_cache_key;
delete result.store;
if (typeof result.reasoning?.effort === "string") {
result.reasoning_effort = result.reasoning.effort;
}
delete result.reasoning;
delete result.client_metadata;

Expand Down
16 changes: 8 additions & 8 deletions open-sse/translator/request/openai-to-kiro.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
buildThinkingSystemPrefix,
KIRO_AGENTIC_SYSTEM_PROMPT,
resolveDefaultProfileArn,
buildKiroAdditionalModelRequestFieldsForModel
buildKiroAdditionalModelRequestFieldsForModel,
usesKiroNativeGptEffort
} from "../../config/kiroConstants.js";
import { parseDataUri } from "../concerns/image.js";
import { DEFAULT_IMAGE_MIME } from "../schema/index.js";
Expand Down Expand Up @@ -511,12 +512,10 @@ function convertMessages(messages, tools, model) {
* Kiro's 2-3 minute server timeout. The suffix is stripped before being
* sent upstream.
*
* 2. Thinking / reasoning. Kiro does not accept `thinking.type` or
* `reasoning_effort` natively. The only way to enable reasoning is to
* inject `<thinking_mode>enabled</thinking_mode>` into the user content
* sent upstream. Detection covers Anthropic-Beta header, Claude API
* 2. Thinking / reasoning. Detection covers Anthropic-Beta header, Claude API
* `thinking`, OpenAI `reasoning_effort`, AMP/Cursor magic tags, and model
* name hints.
* name hints. Supported models receive Kiro's schema-specific effort fields;
* legacy prompt tags remain only for models that need them.
*/
export function openaiToKiroRequest(model, body, stream, credentials) {
const messages = body.messages || [];
Expand All @@ -527,6 +526,8 @@ export function openaiToKiroRequest(model, body, stream, credentials) {

const { upstream: upstreamModel, agentic } = resolveKiroModel(model);
const thinkingBudget = resolveKiroThinkingBudget(body, credentials?.rawHeaders, model);
const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel);
const usesNativeGptEffort = usesKiroNativeGptEffort(body, upstreamModel);

const { history, currentMessage } = convertMessages(messages, tools, upstreamModel);

Expand Down Expand Up @@ -554,7 +555,7 @@ export function openaiToKiroRequest(model, body, stream, credentials) {
// too because the CodeWhisperer surface does not always enforce top-level
// systemPrompt for direct calls.
const systemPromptParts = [];
if (thinkingBudget !== null) {
if (thinkingBudget !== null && !usesNativeGptEffort) {
systemPromptParts.push(buildThinkingSystemPrefix(thinkingBudget));
}
if (agentic) {
Expand Down Expand Up @@ -612,7 +613,6 @@ export function openaiToKiroRequest(model, body, stream, credentials) {
payload.profileArn = profileArn;
}
if (systemPrompt) payload.systemPrompt = systemPrompt;
const additionalModelRequestFields = buildKiroAdditionalModelRequestFieldsForModel(body, upstreamModel);
if (additionalModelRequestFields) {
payload.additionalModelRequestFields = additionalModelRequestFields;
}
Expand Down
26 changes: 26 additions & 0 deletions tests/translator/bugs-kiro.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,34 @@ import { translateRequest } from "../../open-sse/translator/index.js";
import { FORMATS } from "../../open-sse/translator/formats.js";

const O2K = (body) => translateRequest(FORMATS.OPENAI, FORMATS.KIRO, "m", body, true, null, "kiro");
const R2K = (model, body) => translateRequest(
FORMATS.OPENAI_RESPONSES,
FORMATS.KIRO,
model,
body,
true,
null,
"kiro"
);

describe("OpenAI → Kiro", () => {
it.each([
["high", "gpt-5.6-sol"],
["medium", "gpt-5.6-terra"],
["low", "gpt-5.6-luna"],
])("preserves Responses reasoning.effort %s through the full Kiro route", (effort, model) => {
const out = R2K(model, {
input: "Use the requested effort",
reasoning: { effort },
});

expect(out.additionalModelRequestFields).toEqual({
reasoning: { effort },
});
expect(out.systemPrompt || "").not.toContain("<thinking_mode>");
expect(out.systemPrompt || "").not.toContain("<max_thinking_length>");
});

// openai-to-kiro.js — safeJSONParse guards bad tool-call JSON (fixed in PR #1582)
it("malformed tool arguments do not throw the whole request", () => {
expect(() =>
Expand Down
53 changes: 53 additions & 0 deletions tests/translator/claude-kiro-direct.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,59 @@ describe("Claude → Kiro (direct route)", () => {
expect(out.systemPrompt).toContain("<max_thinking_length>24576</max_thinking_length>");
});

it("maps Claude-format effort to GPT-5.6 reasoning fields without legacy prompt tags", () => {
const out = C2K({
output_config: { effort: "low" },
messages: [{ role: "user", content: "think lightly" }],
}, null, "gpt-5.6-sol");

expect(out.additionalModelRequestFields).toEqual({
reasoning: { effort: "low" },
});
expect(out.systemPrompt || "").not.toContain("<thinking_mode>");
expect(out.systemPrompt || "").not.toContain("<max_thinking_length>");
});

it.each(["auto", "minimal", "ultra"])(
"keeps the legacy thinking fallback for unsupported GPT-5.6 effort %s",
(effort) => {
const out = C2K({
output_config: { effort },
messages: [{ role: "user", content: "Use legacy thinking" }],
}, null, "gpt-5.6-sol");

expect(out.additionalModelRequestFields).toBeUndefined();
expect(out.systemPrompt).toContain("<thinking_mode>enabled</thinking_mode>");
expect(out.systemPrompt).toContain("<max_thinking_length>");
}
);

it.each(["none", "off", "disabled"])(
"keeps GPT-5.6 reasoning intentionally disabled for effort %s",
(effort) => {
const out = C2K({
output_config: { effort },
messages: [{ role: "user", content: "Do not reason" }],
}, null, "gpt-5.6-sol");

expect(out.additionalModelRequestFields).toBeUndefined();
expect(out.systemPrompt || "").not.toContain("<thinking_mode>");
expect(out.systemPrompt || "").not.toContain("<max_thinking_length>");
}
);

it("keeps explicit Claude effort ahead of an injected OpenAI effort", () => {
const out = C2K({
output_config: { effort: "low" },
reasoning_effort: "high",
messages: [{ role: "user", content: "honor the client effort" }],
}, null, "gpt-5.6-sol");

expect(out.additionalModelRequestFields).toEqual({
reasoning: { effort: "low" },
});
});

it("sends Claude system as top-level systemPrompt and keeps a user-content fallback", () => {
const out = C2K({
system: "system-only instruction",
Expand Down
96 changes: 96 additions & 0 deletions tests/unit/openai-to-kiro.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,102 @@ describe("openaiToKiroRequest", () => {
});
});

it.each([
["high", "gpt-5.6-sol"],
["medium", "kiro/gpt-5.6-terra"],
["low", "gpt-5.6-luna"],
])("maps GPT-5.6 reasoning.effort %s without legacy prompt tags", (effort, model) => {
const body = {
reasoning: { effort },
messages: [{ role: "user", content: "Use the requested effort" }]
};

const result = openaiToKiroRequest(model, body, true, {});

expect(result.additionalModelRequestFields).toEqual({
reasoning: { effort },
});
expect(systemPromptOf(result)).not.toContain("<thinking_mode>");
expect(systemPromptOf(result)).not.toContain("<max_thinking_length>");
expect(contentOf(result)).not.toContain("<thinking_mode>");
expect(contentOf(result)).not.toContain("<max_thinking_length>");
});

it.each([
["xhigh", "gpt-5.6-terra", "xhigh"],
["max", "gpt-5.6-sol", "xhigh"],
])("preserves GPT-5.6 effort %s as supported wire effort %s", (effort, model, wireEffort) => {
const body = {
reasoning: { effort },
messages: [{ role: "user", content: "Use extended effort" }]
};

const result = openaiToKiroRequest(model, body, true, {});

expect(result.additionalModelRequestFields).toEqual({
reasoning: { effort: wireEffort },
});
expect(systemPromptOf(result)).not.toContain("<thinking_mode>");
expect(systemPromptOf(result)).not.toContain("<max_thinking_length>");
});

it("omits GPT-5.6 effort fields and legacy prompt tags when effort is absent", () => {
const body = {
messages: [{ role: "user", content: "No explicit reasoning effort" }]
};

const result = openaiToKiroRequest("gpt-5.6-sol", body, true, {});

expect(result.additionalModelRequestFields).toBeUndefined();
expect(systemPromptOf(result)).not.toContain("<thinking_mode>");
expect(systemPromptOf(result)).not.toContain("<max_thinking_length>");
});

it.each(["auto", "minimal", "ultra"])(
"keeps the legacy thinking fallback for unsupported GPT-5.6 effort %s",
(effort) => {
const body = {
reasoning: { effort },
messages: [{ role: "user", content: "Use legacy thinking" }]
};

const result = openaiToKiroRequest("gpt-5.6-luna", body, true, {});

expect(result.additionalModelRequestFields).toBeUndefined();
expect(systemPromptOf(result)).toContain("<thinking_mode>enabled</thinking_mode>");
expect(systemPromptOf(result)).toContain("<max_thinking_length>");
}
);

it.each(["none", "off", "disabled"])(
"keeps GPT-5.6 reasoning intentionally disabled for effort %s",
(effort) => {
const body = {
reasoning: { effort },
messages: [{ role: "user", content: "Do not reason" }]
};

const result = openaiToKiroRequest("gpt-5.6-luna", body, true, {});

expect(result.additionalModelRequestFields).toBeUndefined();
expect(systemPromptOf(result)).not.toContain("<thinking_mode>");
expect(systemPromptOf(result)).not.toContain("<max_thinking_length>");
}
);

it("keeps the thinking-alias fallback when GPT effort is blank", () => {
const body = {
reasoning: { effort: "" },
messages: [{ role: "user", content: "Use the thinking alias" }]
};

const result = openaiToKiroRequest("gpt-5.6-sol-thinking", body, true, {});

expect(result.additionalModelRequestFields).toBeUndefined();
expect(systemPromptOf(result)).toContain("<thinking_mode>enabled</thinking_mode>");
expect(systemPromptOf(result)).toContain("<max_thinking_length>");
});

it("does not send additionalModelRequestFields for legacy Kiro model ids", () => {
const body = {
reasoning_effort: "high",
Expand Down