@magpie-community/opencode-kiro-auth 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/index.mjs +86 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,8 +46,12 @@ Power) and makes its requests, in OpenCode and in magpie. Provider id:
46
46
  kiro-cli refreshes its own token first (`kiro-cli debug refresh-auth-token`).
47
47
  A token the plugin refreshes itself is written back, so kiro-cli and the
48
48
  IDE go on with it.
49
- - **Kiro API key (`ksk_…`)**: sent with `tokentype: API_KEY`. Its profile is
50
- found with `GetProfile`.
49
+ - **Kiro API key (`ksk_…`)**: sent with `tokentype: API_KEY`, as kiro-cli
50
+ sends it. `GetProfile` is asked in us-east-1, then eu-central-1, for the
51
+ region the key's profile is in; a key it finds none for is used in
52
+ us-east-1, as kiro-cli uses it. Its models are listed with the control
53
+ plane's `ListAvailableModels` and its credits with `GetUsageLimits`, and
54
+ no profile is named in its calls, as kiro-cli names none.
51
55
 
52
56
  A browser sign-in is kept where OpenCode keeps sign-ins (`auth.json`; in
53
57
  magpie, `plugin-auth.json`) as an `oauth` entry. The entry holds the
package/index.mjs CHANGED
@@ -333,25 +333,48 @@ async function managementCall(region, a, method, query, body) {
333
333
  return JSON.parse(text)
334
334
  }
335
335
 
336
- // profileOf finds the profile a sign-in that doesn't name one uses: an API
337
- // key's own, Builder ID's, or the first Kiro lists in either of its regions.
338
- async function profileOf(c) {
339
- if (c.method === "apikey") {
340
- const res = await fetch(management("us-east-1") + "/", {
341
- method: "POST",
342
- headers: { ...authHeaders({ token: c.access, tokenType: "API_KEY" }), "Content-Type": "application/x-amz-json-1.0",
343
- "X-Amz-Target": "AmazonCodeWhispererService.GetProfile" },
344
- body: "{}",
345
- signal: AbortSignal.timeout(20_000),
346
- })
347
- if (!res.ok) {
348
- const msg = `Kiro didn't take the API key: ${res.status} ${STATUS_TEXT[res.status] ?? ""}`.trimEnd()
349
- throw res.status === 401 || res.status === 403 ? gone(msg) : new Error(msg)
350
- }
351
- const arn = (await res.json().catch(() => null))?.profile?.arn
352
- if (!arn) throw new Error("Kiro didn't say which profile the API key is for")
353
- return arn
336
+ // controlCall is one of Kiro's management operations as kiro-cli sends it
337
+ // for an API key: AWS JSON 1.0, the operation named in X-Amz-Target, POSTed
338
+ // to the endpoint's root (with ?origin=KIRO_CLI where kiro-cli adds it).
339
+ async function controlCall(region, a, target, body, query) {
340
+ const res = await fetch(management(region) + "/" + (query ? "?" + new URLSearchParams(query) : ""), {
341
+ method: "POST",
342
+ headers: { ...authHeaders(a), "Content-Type": "application/x-amz-json-1.0", "X-Amz-Target": target,
343
+ "x-amzn-codewhisperer-optout": "false" },
344
+ body: JSON.stringify(body),
345
+ signal: AbortSignal.timeout(20_000),
346
+ })
347
+ const text = await res.text()
348
+ if (!res.ok) throw new KiroError(res.status, text)
349
+ return JSON.parse(text)
350
+ }
351
+
352
+ // KEY_REGIONS are where kiro-cli asks GetProfile for an API key's profile,
353
+ // in its order (the GovCloud and ISO ones after them aren't Kiro's
354
+ // subscriptions).
355
+ const KEY_REGIONS = ["us-east-1", "eu-central-1"]
356
+
357
+ // keyProfile is an API key's profile, as kiro-cli finds it: GetProfile in
358
+ // each region until one answers with a profile. kiro-cli goes on without
359
+ // one when every region turns it down (a key from an account with no
360
+ // CodeWhisperer profile answers 400), and so does this: "" then, and the
361
+ // key's calls are made in us-east-1 without a profile, as kiro-cli makes
362
+ // them (Hollowhere, on magpie's Discord: a key kiro-cli 2.33.1 served
363
+ // failed every request here with "Kiro didn't take the API key: 400").
364
+ async function keyProfile(c) {
365
+ for (const region of KEY_REGIONS) {
366
+ try {
367
+ const arn = (await controlCall(region, { token: c.access, tokenType: "API_KEY" }, "AmazonCodeWhispererService.GetProfile", {}))
368
+ ?.profile?.arn
369
+ if (arn) return arn
370
+ } catch {}
354
371
  }
372
+ return ""
373
+ }
374
+
375
+ // profileOf finds the profile a sign-in that doesn't name one uses:
376
+ // Builder ID's, or the first Kiro lists in either of its regions.
377
+ async function profileOf(c) {
355
378
  if (c.builderID) return BUILDER_ID_PROFILE
356
379
  let last
357
380
  for (const region of ["us-east-1", "eu-central-1"]) {
@@ -370,6 +393,8 @@ async function profileOf(c) {
370
393
 
371
394
  // usageLimits is Get-Usage-Limits: the plan, the account's email, credits.
372
395
  function usageLimits(a, email) {
396
+ // an API key's, as kiro-cli asks for it: no profile, no email asked
397
+ if (a.tokenType === "API_KEY") return controlCall(a.region, a, "AmazonCodeWhispererService.GetUsageLimits", { origin: "KIRO_CLI" }, { origin: "KIRO_CLI" })
373
398
  return managementCall(a.region, a, "Get-Usage-Limits",
374
399
  { origin: "KIRO_CLI", profileArn: a.profile, resourceType: "CREDIT", isEmailRequired: String(email) })
375
400
  }
@@ -497,7 +522,12 @@ function account(client) {
497
522
  saved = true
498
523
  }
499
524
  }
500
- if (!c.profile) {
525
+ if (c.method === "apikey") {
526
+ if (!c.asked) {
527
+ c.profile = await keyProfile(c)
528
+ c.asked = true
529
+ }
530
+ } else if (!c.profile) {
501
531
  c.profile = await profileOf(c)
502
532
  if (!auth?.profileArn && !saved) {
503
533
  await save(auth, c)
@@ -510,7 +540,10 @@ function account(client) {
510
540
  await save(auth, c)
511
541
  }
512
542
  held = { id, c }
513
- return { token: c.access, tokenType: tokenType(c), profile: c.profile, region: regionOf(c.profile, c.region) }
543
+ // kiro-cli names no profile in an API key's calls, even one GetProfile
544
+ // found; the profile only says the key's region
545
+ const profile = c.method === "apikey" ? "" : c.profile
546
+ return { token: c.access, tokenType: tokenType(c), profile, region: regionOf(c.profile, c.region) }
514
547
  })
515
548
  // renew is auth.refresh's: a sign-in kept here (not kiro-cli's or the
516
549
  // IDE's, which their owners keep) renewed under the same lock as the
@@ -584,9 +617,25 @@ function runtimeModel(m) {
584
617
  // picks a model for each request.
585
618
  const MODELS = [{ id: "auto", name: "Auto" }]
586
619
 
620
+ // keyModels is an API key's list, asked as kiro-cli asks it: Kiro's
621
+ // control plane's ListAvailableModels, with no profile, each page.
622
+ async function keyModels(a) {
623
+ const out = { models: [] }
624
+ let next
625
+ for (let i = 0; i < 20; i++) {
626
+ const page = await controlCall(a.region, a, "KiroControlPlaneBearerService.ListAvailableModels",
627
+ { origin: "KIRO_CLI", ...(next ? { nextToken: next } : {}) }, { origin: "KIRO_CLI" })
628
+ out.models.push(...(page?.models ?? []))
629
+ out.defaultModel ??= page?.defaultModel
630
+ next = page?.nextToken
631
+ if (!next) break
632
+ }
633
+ return out
634
+ }
635
+
587
636
  // listModels is List-Available-Models, the default first.
588
637
  async function listModels(a) {
589
- const out = await managementCall(a.region, a, "List-Available-Models", { origin: "KIRO_CLI", profileArn: a.profile })
638
+ const out = a.tokenType === "API_KEY" ? await keyModels(a) : await managementCall(a.region, a, "List-Available-Models", { origin: "KIRO_CLI", profileArn: a.profile })
590
639
  const list = []
591
640
  for (const m of out?.models ?? []) {
592
641
  if (!m?.modelId) continue
@@ -1048,7 +1097,12 @@ async function* events(body, model, budget) {
1048
1097
  let tool = ""
1049
1098
  let tools = 0
1050
1099
  let stop = ""
1051
- const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
1100
+ // reported: Kiro sent a tokenUsage. It doesn't today: a reply has a
1101
+ // contextUsageEvent and a meteringEvent (credits) and nothing more, and
1102
+ // kiro-cli 2.32's own trace log shows its MetadataEvent with every token
1103
+ // count None, while a repeated prompt is billed about half the credits,
1104
+ // so Kiro does cache and doesn't say how much
1105
+ const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reported: false }
1052
1106
  let pct = 0
1053
1107
  let said = 0
1054
1108
  let failed = ""
@@ -1098,6 +1152,7 @@ async function* events(body, model, budget) {
1098
1152
  if (m.stopReason) stop = m.stopReason
1099
1153
  const tu = m.tokenUsage
1100
1154
  if (tu && typeof tu === "object") {
1155
+ usage.reported = true
1101
1156
  usage.input += tu.uncachedInputTokens || tu.inputTokens || 0
1102
1157
  usage.output += tu.outputTokens || 0
1103
1158
  usage.cacheRead += tu.cacheReadInputTokens || 0
@@ -1143,12 +1198,15 @@ function finished(content) {
1143
1198
  return content
1144
1199
  }
1145
1200
 
1146
- const anthropicUsage = (u) => ({
1147
- input_tokens: u.input,
1148
- output_tokens: u.output,
1149
- cache_read_input_tokens: u.cacheRead,
1150
- cache_creation_input_tokens: u.cacheWrite,
1151
- })
1201
+ // anthropicUsage is the usage in Messages' words. With no tokenUsage from
1202
+ // Kiro the input is reckoned from how full the context is, and what of it
1203
+ // Kiro read from its cache is unknown, not 0: the cache fields are left
1204
+ // out and magpie is told so (magpie_cache_unknown), so it shows no hit
1205
+ // rate rather than 0%.
1206
+ const anthropicUsage = (u) =>
1207
+ u.reported
1208
+ ? { input_tokens: u.input, output_tokens: u.output, cache_read_input_tokens: u.cacheRead, cache_creation_input_tokens: u.cacheWrite }
1209
+ : { input_tokens: u.input, output_tokens: u.output, magpie_cache_unknown: true }
1152
1210
 
1153
1211
  // quotaWords are magpie's (internal/gateway/fallback.go): how a vendor says
1154
1212
  // "out of quota" or "slow down".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-kiro-auth",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Kiro subscriptions (Free, Pro, Pro+, Power) in OpenCode and magpie",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",