From 6235e3960bf86d714ccf692c857475d2724f1525 Mon Sep 17 00:00:00 2001 From: fivetaku Date: Sat, 26 Sep 2026 21:46:46 +0900 Subject: [PATCH 1/3] fix(kiro): send the Builder ID service profile on the usage probe Builder ID accounts never receive an account-scoped profile ARN. The runtime path already falls back to KIRO_BUILDER_ID_SERVICE_PROFILE_ARN (resolveKiroRequestProfile), but kiroUsageContextForAccount only forwarded a stored ARN, so GetUsageLimits answered 400 "Invalid profileArn" and every Builder ID account reported no quota. The fixed ARN is excluded from region inference, matching the kiro-constants contract. Co-Authored-By: Claude --- src/providers/kiro-usage.ts | 15 +++++- .../providers/kiro/kiro-account-quota.test.ts | 46 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/providers/kiro-usage.ts b/src/providers/kiro-usage.ts index 46fa8279604..e1e78652ec8 100644 --- a/src/providers/kiro-usage.ts +++ b/src/providers/kiro-usage.ts @@ -12,6 +12,7 @@ * the pool needs those two answers to decide how long to cool a 429'd account. */ import { getValidAccessSnapshotForAccount } from "../oauth"; +import { KIRO_BUILDER_ID_SERVICE_PROFILE_ARN } from "../adapters/kiro-constants"; import type { ProviderQuota, ProviderQuotaWindow } from "./quota-types"; import { ACCOUNT_QUOTA_TTL_MS, @@ -82,7 +83,10 @@ function safeRegion(value: string | undefined): string | undefined { * the SSO session that minted the token. */ function usageRegion(ctx: KiroUsageContext): string { - return safeRegion(ctx.profileArn?.split(":")[3]) + // The Builder ID service profile is Amazon's fixed us-east-1 ARN, not the account's own, so it + // must not pin the region the same way kiro-constants.ts forbids for the runtime path. + const arnRegion = ctx.profileArn === KIRO_BUILDER_ID_SERVICE_PROFILE_ARN ? undefined : ctx.profileArn?.split(":")[3]; + return safeRegion(arnRegion) ?? safeRegion(ctx.apiRegion) ?? safeRegion(ctx.ssoRegion) ?? "us-east-1"; @@ -210,7 +214,14 @@ export async function kiroUsageContextForAccount(accountId: string): Promise { expect(getKiroAccountExhaustion(key)?.exhausted).toBe(false); }); }); + +describe("Kiro Builder ID usage probe", () => { + const BUILDER_ID_ARN = "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX"; + + function captureProbe(): Array<{ host: string; arn: string | null; bodyArn: unknown }> { + const seen: Array<{ host: string; arn: string | null; bodyArn: unknown }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const request = new Request(input instanceof Request ? input : String(input), init); + const url = new URL(request.url); + const body = await request.json() as Record; + seen.push({ host: url.host, arn: url.searchParams.get("profileArn"), bodyArn: body.profileArn }); + return new Response(usagePayload(25.21, 5000), { status: 200, headers: { "content-type": "application/json" } }); + }) as typeof fetch; + return seen; + } + + test("a Builder ID account without a stored ARN sends the service profile and reports usage", async () => { + // Builder ID credentials carry the OIDC client pair and never an account-scoped ARN. + await saveCredential("kiro", { + access: "token-bid", refresh: "refresh-bid", expires: Date.now() + 60 * 60_000, + accountId: "kiro-bid", email: "bid@example.com", + kiro: { apiRegion: "us-west-2", ssoRegion: "us-west-2", clientId: "client-id", clientSecret: "client-secret" }, + }); + const seen = captureProbe(); + const rows = await fetchProviderAccountQuotas("kiro"); + expect(seen).toHaveLength(1); + expect(seen[0]!.arn).toBe(BUILDER_ID_ARN); + expect(seen[0]!.bodyArn).toBe(BUILDER_ID_ARN); + // The fixed ARN is Amazon's us-east-1 profile; it must not pin the account's own region. + expect(seen[0]!.host).toBe("management.us-west-2.kiro.dev"); + expect(rows[0]!.quota?.monthlyPercent).toBeCloseTo(0.5042, 4); + }); + + test("a non-Builder-ID account without a stored ARN still sends none", async () => { + await saveCredential("kiro", { + access: "token-desk", refresh: "refresh-desk", expires: Date.now() + 60 * 60_000, + accountId: "kiro-desk", email: "desk@example.com", + kiro: { apiRegion: "us-east-1", ssoRegion: "us-east-1" }, + }); + const seen = captureProbe(); + await fetchProviderAccountQuotas("kiro"); + expect(seen).toHaveLength(1); + expect(seen[0]!.arn).toBeNull(); + expect(seen[0]!.bodyArn).toBeUndefined(); + }); +}); From d590d8ab516d1da37dd8470191620ed777baa1bc Mon Sep 17 00:00:00 2001 From: fivetaku Date: Sat, 26 Sep 2026 22:10:03 +0900 Subject: [PATCH 2/3] fix(kiro): reuse resolveKiroRequestProfile for the usage probe ARN Review follow-up: take the ARN and the builderIdFallback flag from the shared resolver instead of re-deriving the aws_sso_oidc rule, so the usage probe cannot drift from the runtime path. Region inference skips the ARN on builderIdFallback rather than on string equality. The Builder ID test now answers with the CREDIT-only breakdown observed live. Co-Authored-By: Claude --- src/providers/kiro-usage.ts | 26 +++++++++++-------- .../providers/kiro/kiro-account-quota.test.ts | 13 +++++++++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/providers/kiro-usage.ts b/src/providers/kiro-usage.ts index e1e78652ec8..e5005d83fd3 100644 --- a/src/providers/kiro-usage.ts +++ b/src/providers/kiro-usage.ts @@ -12,7 +12,7 @@ * the pool needs those two answers to decide how long to cool a 429'd account. */ import { getValidAccessSnapshotForAccount } from "../oauth"; -import { KIRO_BUILDER_ID_SERVICE_PROFILE_ARN } from "../adapters/kiro-constants"; +import { resolveKiroRequestProfile } from "../oauth/kiro"; import type { ProviderQuota, ProviderQuotaWindow } from "./quota-types"; import { ACCOUNT_QUOTA_TTL_MS, @@ -47,6 +47,8 @@ export interface KiroUsageContext { accountId: string; access: string; profileArn?: string; + /** The ARN is the Builder ID service profile, not the account's; it must not pick the region. */ + builderIdFallback?: boolean; apiRegion?: string; ssoRegion?: string; } @@ -84,8 +86,8 @@ function safeRegion(value: string | undefined): string | undefined { */ function usageRegion(ctx: KiroUsageContext): string { // The Builder ID service profile is Amazon's fixed us-east-1 ARN, not the account's own, so it - // must not pin the region the same way kiro-constants.ts forbids for the runtime path. - const arnRegion = ctx.profileArn === KIRO_BUILDER_ID_SERVICE_PROFILE_ARN ? undefined : ctx.profileArn?.split(":")[3]; + // must not pin the region, as kiro-constants.ts requires for the runtime path. + const arnRegion = ctx.builderIdFallback ? undefined : ctx.profileArn?.split(":")[3]; return safeRegion(arnRegion) ?? safeRegion(ctx.apiRegion) ?? safeRegion(ctx.ssoRegion) @@ -211,17 +213,19 @@ export async function fetchKiroUsageSnapshot(ctx: KiroUsageContext): Promise { const snapshot = await getValidAccessSnapshotForAccount("kiro", accountId); + // Builder ID accounts never get an account-scoped ARN, and GetUsageLimits rejects a missing one + // with 400 "Invalid profileArn". Ask the same resolver the runtime path uses, so the usage probe + // sends exactly the ARN a generation request would. An account object is always passed, so the + // accountless env/local-import fallbacks never apply to a pooled account. + const profile = resolveKiroRequestProfile({ + profileArn: snapshot.kiro?.profileArn, + authType: snapshot.kiro?.authType, + }); return { accountId, access: snapshot.accessToken, - // Builder ID accounts never get an account-scoped ARN, and GetUsageLimits rejects a missing one - // with 400 "Invalid profileArn". Send the same request-scoped service profile the runtime path - // sends (resolveKiroRequestProfile), so pooled Builder ID accounts report usage at all. - ...(snapshot.kiro?.profileArn - ? { profileArn: snapshot.kiro.profileArn } - : snapshot.kiro?.authType === "aws_sso_oidc" - ? { profileArn: KIRO_BUILDER_ID_SERVICE_PROFILE_ARN } - : {}), + ...(profile.profileArn ? { profileArn: profile.profileArn } : {}), + ...(profile.builderIdFallback ? { builderIdFallback: true } : {}), ...(snapshot.kiro?.apiRegion ? { apiRegion: snapshot.kiro.apiRegion } : {}), ...(snapshot.kiro?.ssoRegion ? { ssoRegion: snapshot.kiro.ssoRegion } : {}), }; diff --git a/tests/providers/kiro/kiro-account-quota.test.ts b/tests/providers/kiro/kiro-account-quota.test.ts index 1a235d8133d..ca4314e0c1c 100644 --- a/tests/providers/kiro/kiro-account-quota.test.ts +++ b/tests/providers/kiro/kiro-account-quota.test.ts @@ -189,7 +189,18 @@ describe("Kiro Builder ID usage probe", () => { const url = new URL(request.url); const body = await request.json() as Record; seen.push({ host: url.host, arn: url.searchParams.get("profileArn"), bodyArn: body.profileArn }); - return new Response(usagePayload(25.21, 5000), { status: 200, headers: { "content-type": "application/json" } }); + // Builder ID accounts answer with a CREDIT-only breakdown (observed live), so this also pins + // the parser's fallback past AGENTIC_REQUEST. + return new Response(JSON.stringify({ + usageBreakdownList: [{ + resourceType: "CREDIT", + currentUsageWithPrecision: 25.21, + usageLimitWithPrecision: 5000, + unit: "CREDITS", + }], + overageConfiguration: { overageStatus: "DISABLED" }, + nextDateReset: Math.floor(Date.now() / 1000) + 3 * 24 * 3600, + }), { status: 200, headers: { "content-type": "application/json" } }); }) as typeof fetch; return seen; } From 7c77efb6934ebc4912059df5077d965d336b6055 Mon Sep 17 00:00:00 2001 From: fivetaku Date: Sat, 26 Sep 2026 22:11:26 +0900 Subject: [PATCH 3/3] docs(kiro): note the Builder ID service profile on the usage probe Co-Authored-By: Claude --- docs-site/src/content/docs/reference/adapters.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index f00d96edc08..6b6c73740e6 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -344,7 +344,9 @@ MiMo model Command Code serves. - Reports per-account usage. `AmazonCodeWhispererService.GetUsageLimits` on `https://management.{region}.kiro.dev/` returns the plan allowance, which becomes the monthly quota window for that account; a free-trial balance is reported as its own window. - The region comes from the account's profile ARN, then its stored API/SSO region. An + The region comes from the account's profile ARN, then its stored API/SSO region. An AWS + Builder ID account has no profile ARN of its own, so the probe sends the same Builder ID + service profile that generation requests use; that fixed ARN never selects the region. An unreadable or unrecognised response is reported as unknown rather than as zero usage, and an account whose overage is enabled is not treated as exhausted merely for passing its limit. The operation is undocumented by AWS, so treat the numbers as best-effort.