fix(kiro): add runtime.us-east-1.kiro.dev as first-attempt endpoint (#11517)

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.
This commit is contained in:
rifqiawl
2026-08-26 19:21:44 +07:00
committed by GitHub
parent e616130496
commit 9f1f5ecf1f
2 changed files with 197 additions and 7 deletions

View File

@@ -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 };

View File

@@ -0,0 +1,152 @@
import test from "node:test";
import assert from "node:assert/strict";
import { KiroExecutor } from "../../open-sse/executors/kiro.ts";
// #<issue>: 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;
}
});