feat(kiro): live per-account model discovery via ListAvailableModels (#3836)

Kiro's catalog is per-account / per-tier (and admin-curated for IAM Identity Center
orgs), which the static registry can't reflect. The models route now discovers the
live list from the CodeWhisperer ListAvailableModels API with the stored OAuth token
(Builder ID / social and IdC accounts; profileArn only as a retry to avoid 403,
region-matched with us-east-1 fallback), falling back to the static registry catalog
when the token is missing/expired or the upstream is unavailable so import never breaks.

Integrated into release/v3.8.25.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
NOXX - Commiter 2026-06-14 16:30:32 +03:00 committed by GitHub
parent ed0638c0f1
commit 948cf1f92c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 407 additions and 2 deletions

View file

@ -72,7 +72,7 @@
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1069,
"src/app/api/oauth/[provider]/[action]/route.ts": 903,
"src/app/api/providers/[id]/models/route.ts": 2426,
"src/app/api/providers/[id]/models/route.ts": 2487,
"src/app/api/providers/[id]/test/route.ts": 842,
"src/app/api/usage/analytics/route.ts": 941,
"src/app/api/v1/models/catalog.ts": 1435,
@ -127,5 +127,6 @@
"_rebaseline_2026_06_14_r3_3835_kiro_pricing": "PR #3835 own growth: pricing.ts 1470→1508 (+38 = missing Kiro pricing rows, claude-sonnet-4.6 etc., pure data). Also carries inherited release/v3.8.25 drift not yet frozen by prior r2 merges: RequestLoggerV2.tsx 1276→1282 (#3820 resizable log table) and combo.ts 5198→5203 (#3811 round-robin replay-response fix). This PR does not touch RequestLoggerV2/combo.ts source; updating the frozen values restores Fast Quality Gates on the current release branch.",
"_rebaseline_2026_06_14_r3_3849_transient_hide": "PR #3849 own growth: providerPageHelpers.ts 939→955 (+16 = expanded JSDoc on the auto-hide policy + transient-failure guard in evaluateTestAllEntry). Cohesive helper logic; not extractable.",
"_rebaseline_2026_06_14_r3_3838_opencode_quota": "PR #3838 own growth: usage.ts 3394→3408 (+14 = clearer OpenCode Go missing-quota-API messages with OMNIROUTE_OPENCODE_GO_QUOTA_URL override hint + upstream issue links). Message text only; no logic extractable.",
"_rebaseline_2026_06_14_r3_3839_veo_video": "PR #3839 own growth: schemas.ts 2522→2523 (+1 = Veo video model (predictLongRunning) validation for Gemini/Vertex dynamic discovery)."
"_rebaseline_2026_06_14_r3_3839_veo_video": "PR #3839 own growth: schemas.ts 2522→2523 (+1 = Veo video model (predictLongRunning) validation for Gemini/Vertex dynamic discovery).",
"_rebaseline_2026_06_14_r3_3836_kiro_discovery": "PR #3836 own growth: models/route.ts 2426→2487 (+61 = kiro live per-account discovery branch wiring fetchKiroAvailableModels into the existing cache/auto-fetch/fallback discovery flow). Structural shrink of this route tracked in #3789."
}

View file

@ -0,0 +1,196 @@
/**
* Kiro (AWS CodeWhisperer / Amazon Q) live model discovery.
*
* Kiro's model catalog is per-account / per-tier the free tier, Pro, Pro+ and
* Power plans expose different model sets, and AWS IAM Identity Center (enterprise)
* orgs further restrict it to an admin-curated "approved models" list. The Kiro
* IDE / CLI populates its model picker by calling the CodeWhisperer
* `ListAvailableModels` operation:
*
* GET https://q.{region}.amazonaws.com/ListAvailableModels?origin=AI_EDITOR
* Authorization: Bearer <accessToken>
* { models: [ { modelId, modelName?, tokenLimits?: { maxInputTokens } }, ... ] }
*
* This works for both "simple" Builder ID / social logins and AWS IAM Identity
* Center accounts:
* - `origin=AI_EDITOR` alone is the universal call (Builder ID / IdC).
* - `profileArn` is only sent for desktop-style accounts that have one, and only
* as a retry, because sending it for Builder ID can yield 403.
* - The endpoint is region-matched (IdC tokens are region-bound, e.g.
* eu-central-1) with a us-east-1 fallback (the legacy CodeWhisperer home region).
*
* A safe fallback to the static registry catalog is preserved so model import
* never breaks when the account is offline / unauthenticated / token-expired.
*/
type RawRecord = Record<string, unknown>;
function asRecord(value: unknown): RawRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as RawRecord) : {};
}
function toNonEmptyString(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
export type KiroModel = {
id: string;
name: string;
owned_by: string;
};
export type KiroModelsResult = {
models: KiroModel[];
/** "api" = live discovery; "fallback" = static catalog (offline/unauthed/error). */
source: "api" | "fallback";
};
/**
* Parse a CodeWhisperer `ListAvailableModels` response into managed model rows.
* Only ids present in the live response are returned, which gives the exact
* per-account / per-tier entitlement filtering.
*/
export function parseKiroModels(data: unknown): KiroModel[] {
const payload = asRecord(data);
const items = Array.isArray(payload.models)
? (payload.models as unknown[])
: Array.isArray(payload.availableModels)
? (payload.availableModels as unknown[])
: [];
const seen = new Set<string>();
const models: KiroModel[] = [];
for (const value of items) {
const item = asRecord(value);
const id = toNonEmptyString(item.modelId) || toNonEmptyString(item.id);
if (!id || seen.has(id)) continue;
seen.add(id);
const name =
toNonEmptyString(item.modelName) || toNonEmptyString(item.name) || id;
models.push({ id, name, owned_by: "kiro" });
}
return models;
}
/**
* Derive the AWS region for a Kiro connection. Mirrors getKiroUsage: prefer the
* stored region, then the region embedded in the profileArn, else us-east-1.
*/
export function resolveKiroRegion(providerSpecificData: unknown): string {
const psd = asRecord(providerSpecificData);
const explicit = toNonEmptyString(psd.region);
if (explicit) return explicit.toLowerCase();
const profileArn = toNonEmptyString(psd.profileArn);
const fromArn = profileArn
? profileArn.toLowerCase().match(/^arn:aws:codewhisperer:([a-z0-9-]+):/)?.[1]
: undefined;
return fromArn || "us-east-1";
}
/**
* Build the ordered list of `ListAvailableModels` base URLs to try: the
* region-matched Amazon Q host first, then the us-east-1 home region as a
* fallback (CodeWhisperer's canonical region).
*/
export function buildKiroModelsEndpoints(region: string): string[] {
const normalized = (toNonEmptyString(region) || "us-east-1").toLowerCase();
const urls: string[] = [`https://q.${normalized}.amazonaws.com/ListAvailableModels`];
if (normalized !== "us-east-1") {
urls.push("https://q.us-east-1.amazonaws.com/ListAvailableModels");
}
return urls;
}
export type FetchKiroModelsOptions = {
/** Stored Kiro access token (Bearer). */
accessToken: string | null | undefined;
/** Connection providerSpecificData (region, profileArn). */
providerSpecificData?: unknown;
/** Injectable fetch (defaults to global fetch). */
fetchImpl?: typeof fetch;
/** Static catalog to fall back to when live discovery is unavailable. */
fallbackModels?: Array<{ id: string; name?: string }>;
};
function toFallbackResult(
fallbackModels: Array<{ id: string; name?: string }> | undefined
): KiroModelsResult {
const models = (fallbackModels || [])
.map((model) => {
const id = toNonEmptyString(model.id);
if (!id) return null;
return { id, name: toNonEmptyString(model.name) || id, owned_by: "kiro" };
})
.filter((model): model is KiroModel => Boolean(model));
return { models, source: "fallback" };
}
async function tryFetchModels(
fetchImpl: typeof fetch,
url: string,
accessToken: string
): Promise<KiroModel[] | null> {
try {
const response = await fetchImpl(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
});
if (!response.ok) return null;
const data = await response.json();
const models = parseKiroModels(data);
return models.length > 0 ? models : null;
} catch {
return null;
}
}
/**
* Discover the Kiro model catalog live via `ListAvailableModels`, falling back
* to the static catalog when no token is available or every attempt fails.
*
* Attempt order (stops at the first success):
* 1. `origin=AI_EDITOR` on each region-matched endpoint universal path that
* works for Builder ID / social ("simple") and IAM Identity Center accounts.
* 2. `origin=AI_EDITOR&profileArn=...` on the primary endpoint, only when a
* profileArn is present (desktop-style accounts that require it).
*/
export async function fetchKiroAvailableModels(
options: FetchKiroModelsOptions
): Promise<KiroModelsResult> {
const { accessToken, providerSpecificData, fetchImpl = fetch, fallbackModels } = options;
const token = toNonEmptyString(accessToken);
if (!token) {
return toFallbackResult(fallbackModels);
}
const region = resolveKiroRegion(providerSpecificData);
const endpoints = buildKiroModelsEndpoints(region);
const profileArn = toNonEmptyString(asRecord(providerSpecificData).profileArn);
// Pass 1: origin-only (works for Builder ID / social / IdC).
for (const base of endpoints) {
const models = await tryFetchModels(fetchImpl, `${base}?origin=AI_EDITOR`, token);
if (models) return { models, source: "api" };
}
// Pass 2: retry with profileArn (desktop accounts that require it) on the
// region-matched endpoint only. Skipped for Builder ID / IdC where sending a
// profileArn can 403.
if (profileArn) {
const url = `${endpoints[0]}?origin=AI_EDITOR&profileArn=${encodeURIComponent(profileArn)}`;
const models = await tryFetchModels(fetchImpl, url, token);
if (models) return { models, source: "api" };
}
return toFallbackResult(fallbackModels);
}

View file

@ -24,6 +24,7 @@ import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { getStaticQoderModels } from "@omniroute/open-sse/services/qoderCli.ts";
import { fetchGitHubCopilotModels } from "@omniroute/open-sse/services/githubCopilotModels.ts";
import { fetchKiroAvailableModels } from "@omniroute/open-sse/services/kiroModels.ts";
import { getAntigravityHeaders } from "@omniroute/open-sse/services/antigravityHeaders.ts";
import { ensureAntigravityProjectAssigned } from "@omniroute/open-sse/services/antigravityProjectBootstrap.ts";
import {
@ -2042,6 +2043,66 @@ export async function GET(
});
}
if (provider === "kiro") {
// Kiro's catalog is per-account / per-tier (free vs Pro vs Power) and, for
// IAM Identity Center orgs, an admin-curated approved list. The static
// registry catalog can't reflect that. Discover the live list from the
// CodeWhisperer ListAvailableModels API with the stored OAuth token
// (works for Builder ID / social AND IAM Identity Center accounts); fall
// back to the static registry catalog when the token is missing/expired or
// the upstream is unavailable so import never breaks.
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
if (!accessToken) {
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: "OAuth token unavailable — using cached catalog",
localWarning: "OAuth token unavailable — using local catalog",
});
if (fallback) return fallback;
return buildResponse({
provider,
connectionId,
models: toLocalCatalogModels(),
source: "local_catalog",
warning: "OAuth token unavailable — using local catalog",
});
}
const discovery = await fetchKiroAvailableModels({
accessToken,
providerSpecificData: connection.providerSpecificData,
fetchImpl: (url, init) =>
safeOutboundFetch(url as string, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
guard: getProviderOutboundGuard(),
proxyConfig: proxy,
...(init as Record<string, unknown>),
}),
fallbackModels: toLocalCatalogModels(),
});
if (discovery.source === "api" && discovery.models.length > 0) {
return buildApiDiscoveryResponse(discovery.models);
}
const fallback = buildDiscoveryFallbackResponse({
cacheWarning: "Kiro models API unavailable — using cached catalog",
localWarning: "Kiro models API unavailable — using local catalog",
});
if (fallback) return fallback;
return buildResponse({
provider,
connectionId,
models: discovery.models,
source: "local_catalog",
warning: "Kiro models API unavailable — using local catalog",
});
}
if (provider === "vertex" || provider === "vertex-partner") {
const cachedResponse = maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;

View file

@ -0,0 +1,147 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
parseKiroModels,
resolveKiroRegion,
buildKiroModelsEndpoints,
fetchKiroAvailableModels,
} from "../../open-sse/services/kiroModels.ts";
const FALLBACK = [{ id: "auto-kiro", name: "Auto" }, { id: "claude-sonnet-4.6" }];
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
test("parseKiroModels reads CodeWhisperer ListAvailableModels shape", () => {
const models = parseKiroModels({
models: [
{ modelId: "auto", modelName: "Auto" },
{ modelId: "claude-sonnet-4.6", modelName: "Claude Sonnet 4.6" },
{ modelId: "claude-sonnet-4.6" }, // duplicate id is ignored
{ modelName: "no id" }, // missing id is skipped
],
});
assert.deepEqual(
models.map((m) => m.id),
["auto", "claude-sonnet-4.6"]
);
assert.equal(models[1].name, "Claude Sonnet 4.6");
assert.equal(models[0].owned_by, "kiro");
});
test("resolveKiroRegion prefers stored region, then profileArn, else us-east-1", () => {
assert.equal(resolveKiroRegion({ region: "eu-central-1" }), "eu-central-1");
assert.equal(
resolveKiroRegion({ profileArn: "arn:aws:codewhisperer:eu-central-1:123:profile/X" }),
"eu-central-1"
);
assert.equal(resolveKiroRegion({}), "us-east-1");
assert.equal(resolveKiroRegion(null), "us-east-1");
});
test("buildKiroModelsEndpoints is region-matched with a us-east-1 fallback", () => {
assert.deepEqual(buildKiroModelsEndpoints("us-east-1"), [
"https://q.us-east-1.amazonaws.com/ListAvailableModels",
]);
assert.deepEqual(buildKiroModelsEndpoints("eu-central-1"), [
"https://q.eu-central-1.amazonaws.com/ListAvailableModels",
"https://q.us-east-1.amazonaws.com/ListAvailableModels",
]);
});
test("fetchKiroAvailableModels: simple (Builder ID) account, us-east-1, origin-only", async () => {
const calls: string[] = [];
const fetchImpl = (async (url: string) => {
calls.push(url);
return jsonResponse({ models: [{ modelId: "claude-sonnet-4.6" }, { modelId: "auto" }] });
}) as unknown as typeof fetch;
const result = await fetchKiroAvailableModels({
accessToken: "tok",
providerSpecificData: {}, // no region, no profileArn → us-east-1, origin-only
fetchImpl,
fallbackModels: FALLBACK,
});
assert.equal(result.source, "api");
assert.deepEqual(result.models.map((m) => m.id).sort(), ["auto", "claude-sonnet-4.6"]);
assert.deepEqual(calls, [
"https://q.us-east-1.amazonaws.com/ListAvailableModels?origin=AI_EDITOR",
]);
});
test("fetchKiroAvailableModels: IAM Identity Center account, region-matched endpoint", async () => {
const calls: string[] = [];
const fetchImpl = (async (url: string) => {
calls.push(url);
// First (region-matched) endpoint succeeds.
return jsonResponse({ models: [{ modelId: "claude-opus-4.8", modelName: "Opus 4.8" }] });
}) as unknown as typeof fetch;
const result = await fetchKiroAvailableModels({
accessToken: "tok",
providerSpecificData: { region: "eu-central-1" },
fetchImpl,
fallbackModels: FALLBACK,
});
assert.equal(result.source, "api");
assert.deepEqual(result.models.map((m) => m.id), ["claude-opus-4.8"]);
assert.equal(calls[0], "https://q.eu-central-1.amazonaws.com/ListAvailableModels?origin=AI_EDITOR");
});
test("fetchKiroAvailableModels: retries with profileArn when origin-only fails", async () => {
const calls: string[] = [];
const fetchImpl = (async (url: string) => {
calls.push(url);
if (url.includes("profileArn=")) {
return jsonResponse({ models: [{ modelId: "claude-sonnet-4.6" }] });
}
return jsonResponse({ message: "forbidden" }, 403);
}) as unknown as typeof fetch;
const result = await fetchKiroAvailableModels({
accessToken: "tok",
providerSpecificData: {
region: "us-east-1",
profileArn: "arn:aws:codewhisperer:us-east-1:123:profile/ABC",
},
fetchImpl,
fallbackModels: FALLBACK,
});
assert.equal(result.source, "api");
assert.deepEqual(result.models.map((m) => m.id), ["claude-sonnet-4.6"]);
// origin-only attempted first, then profileArn retry.
assert.equal(calls.length, 2);
assert.ok(calls[0].endsWith("?origin=AI_EDITOR"));
assert.ok(calls[1].includes("profileArn=arn%3Aaws%3Acodewhisperer"));
});
test("fetchKiroAvailableModels: falls back to static catalog when no token", async () => {
const result = await fetchKiroAvailableModels({
accessToken: "",
providerSpecificData: {},
fallbackModels: FALLBACK,
});
assert.equal(result.source, "fallback");
assert.deepEqual(result.models.map((m) => m.id), ["auto-kiro", "claude-sonnet-4.6"]);
});
test("fetchKiroAvailableModels: falls back when every upstream attempt fails", async () => {
const fetchImpl = (async () => jsonResponse({ message: "expired" }, 403)) as unknown as typeof fetch;
const result = await fetchKiroAvailableModels({
accessToken: "stale",
providerSpecificData: { region: "us-east-1" },
fetchImpl,
fallbackModels: FALLBACK,
});
assert.equal(result.source, "fallback");
assert.deepEqual(result.models.map((m) => m.id), ["auto-kiro", "claude-sonnet-4.6"]);
});