mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
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:
@@ -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 };
|
||||
|
||||
152
tests/unit/kiro-runtime-gateway-fallback.test.ts
Normal file
152
tests/unit/kiro-runtime-gateway-fallback.test.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user