From 35b950be81314158999b63566cf3bbad502dcfd7 Mon Sep 17 00:00:00 2001 From: mrnim94 <50592567+mrnim94@users.noreply.github.com> Date: Wed, 9 Sep 2026 10:45:44 +0700 Subject: [PATCH] fix(kiro): route requests through current runtime surfaces and fix 400 REQUEST_BODY_INVALID (#3776) --- open-sse/executors/kiro.js | 40 +++++++++++++------ open-sse/translator/request/claude-to-kiro.js | 3 -- open-sse/translator/request/openai-to-kiro.js | 3 -- tests/translator/claude-kiro-direct.test.js | 7 ++-- .../kiro-api-key-endpoint-routing.test.js | 14 +++---- tests/unit/kiro-minimal-wire-payload.test.js | 19 +++++++++ tests/unit/openai-to-kiro.test.js | 2 +- 7 files changed, 57 insertions(+), 31 deletions(-) create mode 100644 tests/unit/kiro-minimal-wire-payload.test.js diff --git a/open-sse/executors/kiro.js b/open-sse/executors/kiro.js index 77616618..e12aced6 100644 --- a/open-sse/executors/kiro.js +++ b/open-sse/executors/kiro.js @@ -259,6 +259,19 @@ export class KiroExecutor extends BaseExecutor { } } + // CLIRO parity for the Amazon surfaces: the Kiro runtime accepts the + // SSO bearer header + agent-mode marker. Without these the deprecated + // path gateway answers REQUEST_BODY_INVALID for modern payloads. + if (credentials?.accessToken) { + headers["x-amz-sso-bearer"] = credentials.accessToken; + } + headers["x-amzn-kiro-agent-mode"] = "spec"; + headers["x-amzn-codewhisperer-machine-id"] = "kiro-desktop"; + const profileArn = credentials?.providerSpecificData?.profileArn; + if (profileArn) { + headers["x-amzn-codewhisperer-profile-arn"] = profileArn; + } + return headers; } @@ -285,9 +298,13 @@ export class KiroExecutor extends BaseExecutor { // 403 "bearer token invalid", so they must hit the CodeWhisperer // *.amazonaws.com surface, and in the region the token was minted in // (the baseUrls are hardcoded us-east-1). - const isCodeWhispererSurface = - authMethod === "api_key" || authMethod === "external_idp" || authMethod === "idc"; - if (!isCodeWhispererSurface) return baseUrls; + // Kiro deprecated the legacy path-style GenerateAssistantResponse on + // runtime.*.kiro.dev (IDE 1.0.228+ moved to POST / + x-amz-target). The + // path gateway now answers valid modern payloads with 400 + // REQUEST_BODY_INVALID, and 400 is terminal in BaseExecutor, so kiro.dev + // must never be the first surface for any auth method. Amazon surfaces + // reject foreign tokens with 401/403, which DO fall through, so trying + // q/codewhisperer first is safe for every auth method (CLIRO parity). const region = (credentials?.providerSpecificData?.region || "us-east-1").trim(); const regionalize = (u) => @@ -297,20 +314,17 @@ export class KiroExecutor extends BaseExecutor { const amazon = baseUrls.filter((u) => u.includes("amazonaws.com")).map(regionalize); const others = baseUrls.filter((u) => !u.includes("amazonaws.com")); - if (authMethod === "api_key") { - const q = amazon.filter((u) => u.includes("://q.")); - const remaining = amazon.filter((u) => !u.includes("://q.")); - return q.length > 0 - ? [...q, ...remaining, ...others] - : [...amazon, ...others]; - } - - return amazon.length > 0 ? [...amazon, ...others] : baseUrls; + const q = amazon.filter((u) => u.includes("://q.")); + const remaining = amazon.filter((u) => !u.includes("://q.")); + return q.length > 0 + ? [...q, ...remaining, ...others] + : [...amazon, ...others]; } buildUrl(model, stream, urlIndex = 0, credentials = null) { const baseUrls = this.getOrderedBaseUrls(credentials); - return baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl; + const url = baseUrls[urlIndex] || baseUrls[0] || this.config.baseUrl; + return url; } // Retry only endpoint/auth-surface failures. Payload-invalid HTTP 400 must be diff --git a/open-sse/translator/request/claude-to-kiro.js b/open-sse/translator/request/claude-to-kiro.js index 3cfad109..9a7cf377 100644 --- a/open-sse/translator/request/claude-to-kiro.js +++ b/open-sse/translator/request/claude-to-kiro.js @@ -316,14 +316,11 @@ export function claudeToKiroRequest(model, body, stream, credentials) { conversationState: { chatTriggerType: "MANUAL", conversationId, - agentContinuationId: continuationId, - agentTaskType: "vibe", currentMessage: { userInputMessage, }, history: canonical.history, }, - agentMode: "vibe", }; if (profileArn) payload.profileArn = profileArn; diff --git a/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js index 1d9bedec..990b607f 100644 --- a/open-sse/translator/request/openai-to-kiro.js +++ b/open-sse/translator/request/openai-to-kiro.js @@ -397,8 +397,6 @@ export function openaiToKiroRequest(model, body, stream, credentials) { conversationState: { chatTriggerType: "MANUAL", conversationId, - agentContinuationId: continuationId, - agentTaskType: "vibe", currentMessage: { userInputMessage: { content: replayCurrent.content || "", @@ -414,7 +412,6 @@ export function openaiToKiroRequest(model, body, stream, credentials) { }, history: canonical.history }, - agentMode: "vibe", }; if (profileArn) { diff --git a/tests/translator/claude-kiro-direct.test.js b/tests/translator/claude-kiro-direct.test.js index 3fca7be1..ab122b0c 100644 --- a/tests/translator/claude-kiro-direct.test.js +++ b/tests/translator/claude-kiro-direct.test.js @@ -26,9 +26,8 @@ describe("Claude → Kiro (direct route)", () => { expect(first.conversationState.conversationId).toBe("hermes-session-123-claude-replay"); expect(second.conversationState.conversationId).toBe("hermes-session-123-claude-replay"); - expect(first.conversationState.agentContinuationId).toBeTruthy(); - expect(second.conversationState.agentContinuationId).toBe(first.conversationState.agentContinuationId); - expect(first.conversationState.agentTaskType).toBe("vibe"); + expect(first.conversationState).not.toHaveProperty("agentContinuationId"); + expect(second.conversationState).not.toHaveProperty("agentTaskType"); expect(second.conversationState.history[0].userInputMessage.content).toBe( first.conversationState.currentMessage.userInputMessage.content ); @@ -84,7 +83,7 @@ describe("Claude → Kiro (direct route)", () => { expect(out.systemPrompt).toContain( "enabled" ); - expect(out.agentMode).toBe("vibe"); + expect(out).not.toHaveProperty("agentMode"); }); it("does not send additionalModelRequestFields for Kiro models without effort support", () => { diff --git a/tests/unit/kiro-api-key-endpoint-routing.test.js b/tests/unit/kiro-api-key-endpoint-routing.test.js index a0750adc..22cf97fc 100644 --- a/tests/unit/kiro-api-key-endpoint-routing.test.js +++ b/tests/unit/kiro-api-key-endpoint-routing.test.js @@ -20,26 +20,26 @@ describe("Kiro auth-aware endpoint routing", () => { ]); }); - it("keeps Builder ID OAuth on the Kiro runtime surface", () => { + it("routes Builder ID OAuth through Amazon Q first (runtime path deprecated)", () => { expect(executor.getOrderedBaseUrls(credentials("builder-id"))).toEqual([ - RUNTIME, - CODEWHISPERER, Q, + CODEWHISPERER, + RUNTIME, ]); }); - it("keeps external IdP on CodeWhisperer before Amazon Q", () => { + it("routes external IdP through Amazon Q first", () => { expect(executor.getOrderedBaseUrls(credentials("external_idp"))).toEqual([ - CODEWHISPERER, Q, + CODEWHISPERER, RUNTIME, ]); }); - it("regionalizes AWS endpoints for IDC without changing Kiro runtime", () => { + it("regionalizes AWS endpoints for IDC with Q first", () => { expect(executor.getOrderedBaseUrls(credentials("idc", "eu-west-1"))).toEqual([ - "https://codewhisperer.eu-west-1.amazonaws.com/generateAssistantResponse", "https://q.eu-west-1.amazonaws.com/generateAssistantResponse", + "https://codewhisperer.eu-west-1.amazonaws.com/generateAssistantResponse", RUNTIME, ]); }); diff --git a/tests/unit/kiro-minimal-wire-payload.test.js b/tests/unit/kiro-minimal-wire-payload.test.js new file mode 100644 index 00000000..97075d4b --- /dev/null +++ b/tests/unit/kiro-minimal-wire-payload.test.js @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { openaiToKiroRequest } from "../../open-sse/translator/request/openai-to-kiro.js"; +import { claudeToKiroRequest } from "../../open-sse/translator/request/claude-to-kiro.js"; + +for (const [name, translate, body] of [ + ["OpenAI", openaiToKiroRequest, { messages: [{ role: "user", content: "hello" }] }], + ["Claude", claudeToKiroRequest, { messages: [{ role: "user", content: "hello" }] }], +]) { + describe(`${name} Kiro minimal wire payload`, () => { + it("omits unsupported agent fields", () => { + const payload = translate("kiro/claude-sonnet-4.5", body, true, {}); + expect(payload).not.toHaveProperty("agentMode"); + expect(payload.conversationState).not.toHaveProperty("agentContinuationId"); + expect(payload.conversationState).not.toHaveProperty("agentTaskType"); + expect(payload.conversationState.chatTriggerType).toBe("MANUAL"); + expect(payload.conversationState.currentMessage.userInputMessage.origin).toBe("AI_EDITOR"); + }); + }); +} diff --git a/tests/unit/openai-to-kiro.test.js b/tests/unit/openai-to-kiro.test.js index 17773bdb..965bd2d1 100644 --- a/tests/unit/openai-to-kiro.test.js +++ b/tests/unit/openai-to-kiro.test.js @@ -606,7 +606,7 @@ describe("openaiToKiroRequest", () => { ); expect(second.conversationState.conversationId).toBe("hermes-session-openai-replay"); - expect(second.conversationState.agentContinuationId).toBe(first.conversationState.agentContinuationId); + expect(second.conversationState).not.toHaveProperty("agentContinuationId"); expect(second.conversationState.history[0].userInputMessage.content).toBe( first.conversationState.currentMessage.userInputMessage.content );