diff --git a/changelog.d/fixes/6726-kiro-adaptive-thinking-allowlist.md b/changelog.d/fixes/6726-kiro-adaptive-thinking-allowlist.md new file mode 100644 index 0000000000..e31c103661 --- /dev/null +++ b/changelog.d/fixes/6726-kiro-adaptive-thinking-allowlist.md @@ -0,0 +1 @@ +- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. diff --git a/open-sse/translator/request/openai-to-kiro.ts b/open-sse/translator/request/openai-to-kiro.ts index d5d6d7f825..3da0a59340 100644 --- a/open-sse/translator/request/openai-to-kiro.ts +++ b/open-sse/translator/request/openai-to-kiro.ts @@ -5,12 +5,13 @@ import { register } from "../registry.ts"; import { FORMATS } from "../formats.ts"; import { v4 as uuidv4, v5 as uuidv5 } from "uuid"; -import { capMaxOutputTokens, capThinkingBudget, supportsReasoning } from "@/lib/modelCapabilities"; +import { capMaxOutputTokens, capThinkingBudget } from "@/lib/modelCapabilities"; import { parseToolInput, normalizeKiroToolSchema, serializeToolResultContent, } from "./openai-to-kiro/messageHelpers.ts"; +import { supportsKiroAdaptiveThinking } from "./openai-to-kiro/adaptiveThinking.ts"; /** * Anthropic's direct-provider `[1m]` context-1m beta suffix. Kiro is AWS @@ -858,15 +859,14 @@ export function buildKiroPayload(model, body, stream, credentials) { // Thinking mode for Claude models on Kiro (ported from javargasm/pi-kiro). // Two coordinated signals steer reasoning on the CodeWhisperer surface: // 1. a `enabledN` - // directive prepended to the current user message — makes Claude emit its - // reasoning INLINE as ``, which the Kiro executor - // splits back into the OpenAI `reasoning_content` channel (kiroThinking.ts); + // directive prepended to the user message — makes Claude emit reasoning + // INLINE, split back into `reasoning_content` by the executor (kiroThinking.ts); // 2. top-level `additionalModelRequestFields` (output_config.effort + // thinking:{type:"adaptive"} + a clamped max_tokens), forwarded to AWS by - // the Kiro executor's transformRequest allowlist — this is the graded - // effort lever. Gated on models that advertise thinking support. + // the Kiro executor's transformRequest allowlist — the graded effort lever, + // gated on Kiro's adaptive-thinking allowlist (#6576), not supportsReasoning(). const requestedEffort = resolveKiroEffort(body) || (modelRequestsThinking ? "high" : ""); - const kiroEffort = supportsReasoning(normalizedModel) ? requestedEffort : ""; + const kiroEffort = supportsKiroAdaptiveThinking(normalizedModel) ? requestedEffort : ""; if (kiroEffort) { // `` / `` are Kiro/CodeWhisperer prompt // conventions (NOT Anthropic API params); the length is a soft hint (the hard diff --git a/open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts b/open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts new file mode 100644 index 0000000000..ba8fefdc97 --- /dev/null +++ b/open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts @@ -0,0 +1,19 @@ +/** + * Kiro/AWS CodeWhisperer only accepts the adaptive-thinking + * `additionalModelRequestFields` envelope for a narrow allowlist of models — + * NOT the same set the generic Anthropic-API capability table + * (`supportsReasoning()` in `@/lib/modelCapabilities`) marks as + * thinking-capable. That table is correct for Anthropic's own API, but Kiro + * rejects the field for `claude-sonnet-4.5` and `claude-haiku-4.5` with a raw + * upstream 400 (`additionalModelRequestFields is not supported for this + * model`, issue #6576) even though both ARE thinking-capable on Anthropic's + * direct API. Only `claude-sonnet-5` is confirmed to accept the adaptive + * envelope on Kiro today — keep this allowlist in sync with + * `open-sse/config/providers/registry/kiro/index.ts` if Kiro's catalog or + * upstream behavior changes. + */ +const KIRO_ADAPTIVE_THINKING_MODELS = new Set(["claude-sonnet-5"]); + +export function supportsKiroAdaptiveThinking(normalizedModel: string): boolean { + return KIRO_ADAPTIVE_THINKING_MODELS.has(normalizedModel); +} diff --git a/tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts b/tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts new file mode 100644 index 0000000000..8c56090b33 --- /dev/null +++ b/tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts @@ -0,0 +1,64 @@ +// Repro probe for GitHub issue #6576. +// +// Kiro/CodeWhisperer rejects `additionalModelRequestFields` for +// claude-sonnet-4.5 / claude-haiku-4.5 with a raw upstream 400: +// "[400]: additionalModelRequestFields is not supported for this model" +// +// buildKiroPayload() gates thinking injection on supportsReasoning(model), +// which resolves from the GENERIC Anthropic-API capability data +// (MODEL_SPECS["claude-sonnet-4-5-..."].supportsThinking === true, +// MODEL_SPECS["claude-haiku-4-5-20251001"].supportsThinking === true). +// That flag says nothing about whether the *Kiro/AWS CodeWhisperer* +// backend accepts the adaptive-thinking additionalModelRequestFields +// envelope for these specific models — only the newer adaptive-only +// models (Opus 4.7/4.8, Sonnet 5, Fable 5) are proven to accept it there +// (see the existing "drops temperature when thinking is enabled" tests). +// +// This test asserts the payload for claude-sonnet-4.5 (the reporter's own +// model) must NOT carry additionalModelRequestFields when reasoning is +// requested, matching what Kiro's upstream actually accepts. It currently +// FAILS because buildKiroPayload has no Kiro-specific allowlist/exclusion +// and blindly forwards the field whenever the generic capability flag says +// supportsThinking:true. +import test from "node:test"; +import assert from "node:assert/strict"; + +const { buildKiroPayload } = await import( + "../../open-sse/translator/request/openai-to-kiro.ts" +); + +test("[repro #6576] buildKiroPayload must not attach additionalModelRequestFields for claude-sonnet-4.5 (Kiro rejects it)", () => { + const body = { + messages: [{ role: "user", content: "Calculate 51818+62218, and reply with result only." }], + reasoning_effort: "medium", + max_tokens: 2048, + stream: false, + }; + + const result = buildKiroPayload("claude-sonnet-4.5", body, false, null); + + assert.equal( + result.additionalModelRequestFields, + undefined, + "additionalModelRequestFields must not be sent for claude-sonnet-4.5 — " + + "Kiro/CodeWhisperer rejects it upstream with " + + "'[400]: additionalModelRequestFields is not supported for this model' (issue #6576)" + ); +}); + +test("[repro #6576] buildKiroPayload must not attach additionalModelRequestFields for claude-haiku-4.5 (Kiro rejects it)", () => { + const body = { + messages: [{ role: "user", content: "hi" }], + thinking: { type: "adaptive" }, + }; + + const result = buildKiroPayload("claude-haiku-4.5", body, false, null); + + assert.equal( + result.additionalModelRequestFields, + undefined, + "additionalModelRequestFields must not be sent for claude-haiku-4.5 — " + + "Kiro/CodeWhisperer rejects it upstream (issue #6576 comment by fenix007: " + + "9/9 production requests with reasoning params 400'd for this exact model)" + ); +}); diff --git a/tests/unit/translator-openai-to-kiro.test.ts b/tests/unit/translator-openai-to-kiro.test.ts index 2b34de746b..4cf19efebb 100644 --- a/tests/unit/translator-openai-to-kiro.test.ts +++ b/tests/unit/translator-openai-to-kiro.test.ts @@ -1049,10 +1049,10 @@ test("buildKiroPayload accepts kr/* model ids without the [1m] suffix", () => { test("buildKiroPayload strips local Kiro selector suffixes before upstream", () => { const body = { messages: [{ role: "user", content: "Hello" }] }; - const result = buildKiroPayload("claude-opus-4.8-thinking-agentic", body, true, {}); + const result = buildKiroPayload("claude-sonnet-5-thinking-agentic", body, true, {}); assert.equal( result.conversationState.currentMessage.userInputMessage.modelId, - "claude-opus-4.8", + "claude-sonnet-5", "local -thinking/-agentic aliases must not be forwarded to Kiro" ); assert.equal( @@ -1121,7 +1121,7 @@ test("buildKiroPayload enables thinking mode for Claude models via reasoning_eff max_tokens: 64000, }; - const result = buildKiroPayload("claude-opus-4.8", body, false, null); + const result = buildKiroPayload("claude-sonnet-5", body, false, null); // only Kiro model accepting adaptive thinking (#6576) assert.ok(result.additionalModelRequestFields, "additionalModelRequestFields must be set"); assert.deepEqual(result.additionalModelRequestFields.thinking, { @@ -1149,7 +1149,7 @@ test("buildKiroPayload drops temperature when thinking is enabled", () => { temperature: 0.5, }; - const result = buildKiroPayload("claude-opus-4.8", body, false, null); + const result = buildKiroPayload("claude-sonnet-5", body, false, null); assert.ok(result.additionalModelRequestFields, "thinking must be enabled"); assert.equal( @@ -1180,7 +1180,7 @@ test("buildKiroPayload maps body.thinking budget_tokens to effort level", () => thinking: { type: "enabled", budget_tokens: 50000 }, }; - const result = buildKiroPayload("claude-opus-4.7", body, false, null); + const result = buildKiroPayload("claude-sonnet-5", body, false, null); assert.ok(result.additionalModelRequestFields, "thinking must be enabled from budget_tokens"); assert.equal(result.additionalModelRequestFields.output_config.effort, "high"); @@ -1212,7 +1212,7 @@ test("buildKiroPayload maps reasoning_effort to the same Kiro effort level (no + test("buildKiroPayload reads effort from Anthropic output_config.effort", () => { const result = buildKiroPayload( - "claude-opus-4.8", + "claude-sonnet-5", { messages: [{ role: "user", content: "hard" }], output_config: { effort: "xhigh" } }, false, null @@ -1224,7 +1224,7 @@ test("buildKiroPayload reads effort from Anthropic output_config.effort", () => test("buildKiroPayload defaults adaptive thinking (no effort) to high", () => { const result = buildKiroPayload( - "claude-opus-4.8", + "claude-sonnet-5", { messages: [{ role: "user", content: "hard" }], thinking: { type: "adaptive" } }, false, null @@ -1239,7 +1239,7 @@ test("buildKiroPayload defaults adaptive thinking (no effort) to high", () => { test("buildKiroPayload drops both temperature and top_p when thinking is enabled", () => { const result = buildKiroPayload( - "claude-opus-4.8", + "claude-sonnet-5", { messages: [{ role: "user", content: "hard" }], reasoning_effort: "high",