From 9f1f5ecf1f2e53e4aab1faaf40b14bbfcc49dbb9 Mon Sep 17 00:00:00 2001 From: rifqiawl Date: Wed, 26 Aug 2026 19:21:44 +0700 Subject: [PATCH] fix(kiro): add runtime.us-east-1.kiro.dev as first-attempt endpoint (#11517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged via /merge-batch (lote 2026-08-26 batch 2, v3.8.51). Boarded no worktree combinado junto com outras ~20 PRs; validação única: typecheck/complexity/cognitive-complexity/changelog-integrity verdes, file-size rebaseado onde necessário (crescimento legítimo), lint com os mesmos 228 achados pré-existentes confirmados via sonda contra o tip puro (não introduzidos por este lote), e 292 testes focados (unit) + 18 (vitest) passando. Obrigado pela contribuição. --- open-sse/executors/kiro.ts | 52 +++++- .../kiro-runtime-gateway-fallback.test.ts | 152 ++++++++++++++++++ 2 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 tests/unit/kiro-runtime-gateway-fallback.test.ts diff --git a/open-sse/executors/kiro.ts b/open-sse/executors/kiro.ts index 815d3a6348..b2759154e4 100644 --- a/open-sse/executors/kiro.ts +++ b/open-sse/executors/kiro.ts @@ -243,6 +243,14 @@ export function resolveKiroRegion( // kiroRuntimeHost from this executor keep working. export { kiroRuntimeHost }; +/** + * Status codes for which trying the next candidate endpoint may succeed where the + * current one failed (auth/profile mismatch, not a payload problem). Mirrors + * 9router's KIRO_ENDPOINT_FALLBACK_STATUSES — a 400 (malformed body) is deliberately + * excluded since resending the same body to another host cannot fix it. + */ +const KIRO_ENDPOINT_FALLBACK_STATUSES = new Set([401, 403, 404]); + /** * KiroExecutor - Executor for Kiro AI (AWS CodeWhisperer) * Uses AWS CodeWhisperer streaming API with AWS EventStream binary format @@ -334,17 +342,47 @@ export class KiroExecutor extends BaseExecutor { // Center accounts (e.g. eu-central-1) are rejected by the default us-east-1 host; only the // regional endpoint accepts the region-bound token + profileArn. const region = resolveKiroRegion(credentials); - const url = `${kiroRuntimeHost(region)}/generateAssistantResponse`; + const regionalUrl = `${kiroRuntimeHost(region)}/generateAssistantResponse`; + + // The Kiro IDE's own branded gateway (runtime.*.kiro.dev) only exists for + // us-east-1 and only accepts Kiro OIDC/social tokens — it rejects + // TokenType=API_KEY and external-IdP/IdC SSO tokens outright (403 "bearer + // token invalid"), so those auth methods go straight to the region-resolved + // CodeWhisperer/Amazon Q surface (mirrors 9router's getOrderedBaseUrls in + // open-sse/executors/kiro.js). For everything else, try the branded gateway + // first — it is the surface the native Kiro IDE itself talks to — and fall + // back to the raw AWS host on an auth/profile-shaped failure. + const authMethod = + typeof credentials.providerSpecificData?.authMethod === "string" + ? credentials.providerSpecificData.authMethod + : undefined; + const isCodeWhispererOnly = + authMethod === "api_key" || authMethod === "idc" || isExternalIdpAuthMethod(authMethod); + const candidateUrls = + region === "us-east-1" && !isCodeWhispererOnly + ? ["https://runtime.us-east-1.kiro.dev/generateAssistantResponse", regionalUrl] + : [regionalUrl]; + const headers = this.buildHeaders(credentials, stream); mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders); const transformedBody = await this.transformRequest(model, body, stream, credentials); + const requestBody = JSON.stringify(transformedBody); - const response = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify(transformedBody), - signal, - }); + let response!: Response; + let url = candidateUrls[0]; + for (let i = 0; i < candidateUrls.length; i++) { + url = candidateUrls[i]; + response = await fetch(url, { + method: "POST", + headers, + body: requestBody, + signal, + }); + const hasFallback = i + 1 < candidateUrls.length; + if (response.ok || !hasFallback || !KIRO_ENDPOINT_FALLBACK_STATUSES.has(response.status)) { + break; + } + } if (!response.ok) { return { response, url, headers, transformedBody }; diff --git a/tests/unit/kiro-runtime-gateway-fallback.test.ts b/tests/unit/kiro-runtime-gateway-fallback.test.ts new file mode 100644 index 0000000000..b16a77c48f --- /dev/null +++ b/tests/unit/kiro-runtime-gateway-fallback.test.ts @@ -0,0 +1,152 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { KiroExecutor } from "../../open-sse/executors/kiro.ts"; + +// #: the executor only ever called the region-resolved CodeWhisperer/ +// Amazon Q surface directly. The native Kiro IDE talks to a branded gateway +// (runtime.us-east-1.kiro.dev) first — this covers the new candidate-url +// ordering, fallback-on-auth-failure behavior, and the auth-method gate that +// keeps API-key/IdC/external-IdP connections off the gateway entirely (it +// rejects those token types outright). + +test("KiroExecutor.execute tries runtime.us-east-1.kiro.dev before the regional CodeWhisperer host for OAuth accounts", async () => { + const executor = new KiroExecutor(); + const originalFetch = globalThis.fetch; + const calledUrls: string[] = []; + + globalThis.fetch = (async (url: string) => { + calledUrls.push(String(url)); + return new Response("ok", { + status: 200, + headers: { "Content-Type": "application/vnd.amazon.eventstream" }, + }); + }) as typeof fetch; + + try { + executor.transformEventStreamToSSE = (() => + new Response("data: [DONE]\n\n", { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + })) as typeof executor.transformEventStreamToSSE; + + await executor.execute({ + model: "claude-sonnet-4.5", + body: { conversationState: {} }, + stream: true, + credentials: { accessToken: "kiro-token", providerSpecificData: { authMethod: "social" } }, + }); + + assert.equal(calledUrls.length, 1); + assert.match( + calledUrls[0], + /^https:\/\/runtime\.us-east-1\.kiro\.dev\/generateAssistantResponse/ + ); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("KiroExecutor.execute falls back to the regional CodeWhisperer host when the gateway rejects the token", async () => { + const executor = new KiroExecutor(); + const originalFetch = globalThis.fetch; + const calledUrls: string[] = []; + + globalThis.fetch = (async (url: string) => { + calledUrls.push(String(url)); + if (calledUrls.length === 1) { + return new Response("bearer token invalid", { status: 403 }); + } + return new Response("ok", { + status: 200, + headers: { "Content-Type": "application/vnd.amazon.eventstream" }, + }); + }) as typeof fetch; + + try { + executor.transformEventStreamToSSE = (() => + new Response("data: [DONE]\n\n", { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + })) as typeof executor.transformEventStreamToSSE; + + const result = await executor.execute({ + model: "claude-sonnet-4.5", + body: { conversationState: {} }, + stream: true, + credentials: { accessToken: "kiro-token", providerSpecificData: { authMethod: "social" } }, + }); + + assert.equal(calledUrls.length, 2); + assert.match(calledUrls[0], /^https:\/\/runtime\.us-east-1\.kiro\.dev/); + assert.match(calledUrls[1], /^https:\/\/codewhisperer\.us-east-1\.amazonaws\.com/); + assert.equal((result.response as Response).status, 200); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("KiroExecutor.execute never tries the gateway for api_key/idc/external_idp auth methods", async () => { + const executor = new KiroExecutor(); + const originalFetch = globalThis.fetch; + + for (const authMethod of ["api_key", "idc", "external_idp"]) { + const calledUrls: string[] = []; + globalThis.fetch = (async (url: string) => { + calledUrls.push(String(url)); + return new Response("ok", { + status: 200, + headers: { "Content-Type": "application/vnd.amazon.eventstream" }, + }); + }) as typeof fetch; + + try { + executor.transformEventStreamToSSE = (() => + new Response("data: [DONE]\n\n", { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + })) as typeof executor.transformEventStreamToSSE; + + await executor.execute({ + model: "claude-sonnet-4.5", + body: { conversationState: {} }, + stream: true, + credentials: { accessToken: "kiro-token", providerSpecificData: { authMethod } }, + }); + + assert.equal(calledUrls.length, 1, `expected exactly one call for authMethod=${authMethod}`); + assert.doesNotMatch( + calledUrls[0], + /kiro\.dev/, + `${authMethod} must never hit the branded gateway` + ); + } finally { + globalThis.fetch = originalFetch; + } + } +}); + +test("KiroExecutor.execute does not retry a malformed-body 400 across endpoints", async () => { + const executor = new KiroExecutor(); + const originalFetch = globalThis.fetch; + const calledUrls: string[] = []; + + globalThis.fetch = (async (url: string) => { + calledUrls.push(String(url)); + return new Response("REQUEST_BODY_INVALID", { status: 400 }); + }) as typeof fetch; + + try { + const result = await executor.execute({ + model: "claude-sonnet-4.5", + body: { conversationState: {} }, + stream: true, + credentials: { accessToken: "kiro-token", providerSpecificData: { authMethod: "social" } }, + }); + + assert.equal(calledUrls.length, 1); + assert.equal((result.response as Response).status, 400); + } finally { + globalThis.fetch = originalFetch; + } +});