@magpie-community/opencode-kiro-auth 0.1.3 → 0.1.5
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.
- package/index.mjs +75 -32
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -188,11 +188,14 @@ async function saveBack(c) {
|
|
|
188
188
|
|
|
189
189
|
// ---- tokens --------------------------------------------------------------------
|
|
190
190
|
|
|
191
|
+
// EXPIRED is the built-in's words for a sign-in Kiro refused, or one with
|
|
192
|
+
// nothing to refresh it.
|
|
191
193
|
const EXPIRED = "Kiro's sign-in has expired; sign in again"
|
|
194
|
+
const EXPIRED_CLI = EXPIRED + " with `kiro-cli login` or the Kiro IDE"
|
|
192
195
|
|
|
193
196
|
// gone is an error saying the sign-in itself is gone — refused, or never
|
|
194
|
-
// there
|
|
195
|
-
//
|
|
197
|
+
// there. The built-in marked no Kiro account lapsed, not even then, nor
|
|
198
|
+
// cleared one: every answer and usage read says the account is kept.
|
|
196
199
|
const gone = (msg) => Object.assign(new Error(msg), { gone: true })
|
|
197
200
|
const fresh = (c) => c.method === "apikey" || !c.expires || c.expires - Date.now() > 2 * 60 * 1000
|
|
198
201
|
|
|
@@ -206,8 +209,8 @@ async function post(url, contentType, body, headers = {}) {
|
|
|
206
209
|
}
|
|
207
210
|
const text = await res.text()
|
|
208
211
|
if (!res.ok) {
|
|
209
|
-
if ([400, 401, 403].includes(res.status)) throw gone(
|
|
210
|
-
throw new Error(`refreshing Kiro's sign-in: ${res.status}
|
|
212
|
+
if ([400, 401, 403].includes(res.status)) throw gone(EXPIRED_CLI)
|
|
213
|
+
throw new Error(`refreshing Kiro's sign-in: ${res.status} ${STATUS_TEXT[res.status] ?? ""}`.trimEnd()) // Go's res.Status
|
|
211
214
|
}
|
|
212
215
|
return JSON.parse(text)
|
|
213
216
|
}
|
|
@@ -223,7 +226,7 @@ async function refresh(c) {
|
|
|
223
226
|
if (n && n.dbKey === c.dbKey && n.access !== c.access && fresh(n)) return n
|
|
224
227
|
}
|
|
225
228
|
}
|
|
226
|
-
if (!c.refresh) throw gone(
|
|
229
|
+
if (!c.refresh) throw gone(EXPIRED_CLI)
|
|
227
230
|
let out
|
|
228
231
|
switch (c.method) {
|
|
229
232
|
case "social": {
|
|
@@ -246,9 +249,9 @@ async function refresh(c) {
|
|
|
246
249
|
break
|
|
247
250
|
}
|
|
248
251
|
default:
|
|
249
|
-
throw gone(
|
|
252
|
+
throw gone("Kiro's sign-in has expired")
|
|
250
253
|
}
|
|
251
|
-
if (!out.access) throw gone(
|
|
254
|
+
if (!out.access) throw gone(EXPIRED_CLI)
|
|
252
255
|
const n = { ...c, access: out.access, expires: Date.now() + (out.expiresIn > 0 ? out.expiresIn : 3600) * 1000 }
|
|
253
256
|
if (out.refresh) n.refresh = out.refresh
|
|
254
257
|
await saveBack(n)
|
|
@@ -315,7 +318,7 @@ async function profileOf(c) {
|
|
|
315
318
|
signal: AbortSignal.timeout(20_000),
|
|
316
319
|
})
|
|
317
320
|
if (!res.ok) {
|
|
318
|
-
const msg = `Kiro didn't take the API key: ${res.status}
|
|
321
|
+
const msg = `Kiro didn't take the API key: ${res.status} ${STATUS_TEXT[res.status] ?? ""}`.trimEnd()
|
|
319
322
|
throw res.status === 401 || res.status === 403 ? gone(msg) : new Error(msg)
|
|
320
323
|
}
|
|
321
324
|
const arn = (await res.json().catch(() => null))?.profile?.arn
|
|
@@ -431,7 +434,7 @@ function account(client) {
|
|
|
431
434
|
if (stale && c && read.access !== c.access && fresh(read)) stale = false // the owner refreshed it already
|
|
432
435
|
c = read
|
|
433
436
|
if (!fresh(c) || stale) {
|
|
434
|
-
if (c.method === "apikey") throw gone("Kiro turned down the API key")
|
|
437
|
+
if (c.method === "apikey") throw gone("Kiro turned down the API key saved on the provider")
|
|
435
438
|
c = await refresh(c)
|
|
436
439
|
await save(auth, c)
|
|
437
440
|
}
|
|
@@ -712,7 +715,7 @@ function failure(status, body) {
|
|
|
712
715
|
const e = JSON.parse(body)
|
|
713
716
|
if (e?.message) msg = e.message + (e.reason ? ` (${e.reason})` : "")
|
|
714
717
|
} catch {}
|
|
715
|
-
msg ||=
|
|
718
|
+
msg ||= STATUS_TEXT[status] ?? ""
|
|
716
719
|
if (msg.includes("CONTENT_LENGTH_EXCEEDS_THRESHOLD") || msg.toLowerCase().includes("input is too long"))
|
|
717
720
|
return { status: 400, message: "input is too long for the model's context: " + msg }
|
|
718
721
|
if (msg.includes("INSUFFICIENT_MODEL_CAPACITY")) return { status: 503, message: msg }
|
|
@@ -723,12 +726,23 @@ function failure(status, body) {
|
|
|
723
726
|
const ERROR_TYPES = { 400: "invalid_request_error", 401: "authentication_error", 402: "billing_error", 403: "permission_error",
|
|
724
727
|
404: "not_found_error", 413: "request_too_large", 429: "rate_limit_error", 503: "overloaded_error", 529: "overloaded_error" }
|
|
725
728
|
const errorType = (status) => ERROR_TYPES[status] ?? "api_error"
|
|
729
|
+
// Each failure says X-Magpie-Sign-In: kept, as the built-in never marked
|
|
730
|
+
// an account lapsed (see gone).
|
|
726
731
|
const errorResponse = (status, message) =>
|
|
727
732
|
new Response(JSON.stringify({ type: "error", error: { type: errorType(status), message } }), {
|
|
728
733
|
status,
|
|
729
|
-
headers: { "Content-Type": "application/json" },
|
|
734
|
+
headers: { "Content-Type": "application/json", "X-Magpie-Sign-In": "kept" },
|
|
730
735
|
})
|
|
731
736
|
|
|
737
|
+
// kept is res saying the account is kept: a success too, which the
|
|
738
|
+
// built-in never took for a sign-in come back.
|
|
739
|
+
function kept(res) {
|
|
740
|
+
if (res.headers.has("X-Magpie-Sign-In")) return res
|
|
741
|
+
const headers = new Headers(res.headers)
|
|
742
|
+
headers.set("X-Magpie-Sign-In", "kept")
|
|
743
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
744
|
+
}
|
|
745
|
+
|
|
732
746
|
// ---- the reply: an AWS event stream → Messages ---------------------------------------
|
|
733
747
|
|
|
734
748
|
// frames reads an AWS event stream: each message its length, its headers'
|
|
@@ -952,6 +966,19 @@ async function* events(body, model, budget) {
|
|
|
952
966
|
yield { kind: "stop", reason, usage }
|
|
953
967
|
}
|
|
954
968
|
|
|
969
|
+
// finished is the message's blocks with each call's arguments read.
|
|
970
|
+
function finished(content) {
|
|
971
|
+
for (const b of content)
|
|
972
|
+
if (b.type === "tool_use") {
|
|
973
|
+
try {
|
|
974
|
+
b.input = b.input ? JSON.parse(b.input) : {}
|
|
975
|
+
} catch {
|
|
976
|
+
b.input = {}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return content
|
|
980
|
+
}
|
|
981
|
+
|
|
955
982
|
const anthropicUsage = (u) => ({
|
|
956
983
|
input_tokens: u.input,
|
|
957
984
|
output_tokens: u.output,
|
|
@@ -959,6 +986,16 @@ const anthropicUsage = (u) => ({
|
|
|
959
986
|
cache_creation_input_tokens: u.cacheWrite,
|
|
960
987
|
})
|
|
961
988
|
|
|
989
|
+
// quotaWords are magpie's (internal/gateway/fallback.go): how a vendor says
|
|
990
|
+
// "out of quota" or "slow down".
|
|
991
|
+
const QUOTA_WORDS = /quota|insufficient|balance|credit|billing|exceeded|rate.?limit|usage.?limit|limit.?reached|hit your .*limit|limit.{0,24}resets|too many requests|overloaded|余额|额度|欠费|限流|频率|套餐|用量|上限/i
|
|
992
|
+
|
|
993
|
+
// failedBefore is the reply to an error before any of the answer, as
|
|
994
|
+
// magpie's built-in relay gives it: a status, not a stream, so another
|
|
995
|
+
// account can take over — a 429 when it reads as a quota or rate limit,
|
|
996
|
+
// else a 502.
|
|
997
|
+
const failedBefore = (text) => errorResponse(QUOTA_WORDS.test(text) ? 429 : 502, text)
|
|
998
|
+
|
|
962
999
|
// reply answers the Messages request from Kiro's events: a stream of
|
|
963
1000
|
// Messages' events, or one message.
|
|
964
1001
|
async function reply(it, model, stream) {
|
|
@@ -982,23 +1019,24 @@ async function reply(it, model, stream) {
|
|
|
982
1019
|
} else if (e.kind === "toolStart") add({ type: "tool_use", id: e.id, name: e.name, input: "" })
|
|
983
1020
|
else if (e.kind === "toolArgs") {
|
|
984
1021
|
if (last?.type === "tool_use") last.input += e.text
|
|
985
|
-
} else if (e.kind === "error")
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
b.input = {}
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
return Response.json({ id, type: "message", role: "assistant", model, content, stop_reason: e.reason, stop_sequence: null,
|
|
1022
|
+
} else if (e.kind === "error") {
|
|
1023
|
+
// with none of the answer said, a status; else what was said
|
|
1024
|
+
if (!content.length) return failedBefore(e.text)
|
|
1025
|
+
return Response.json({ id, type: "message", role: "assistant", model, content: finished(content), stop_reason: "end_turn",
|
|
1026
|
+
stop_sequence: null, usage: anthropicUsage({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }) })
|
|
1027
|
+
} else if (e.kind === "stop") {
|
|
1028
|
+
return Response.json({ id, type: "message", role: "assistant", model, content: finished(content), stop_reason: e.reason, stop_sequence: null,
|
|
996
1029
|
usage: anthropicUsage(e.usage) })
|
|
997
1030
|
}
|
|
998
1031
|
}
|
|
999
1032
|
return errorResponse(502, "the reply ended before it was complete")
|
|
1000
1033
|
}
|
|
1001
1034
|
|
|
1035
|
+
// an error before any of the answer is a status, as the built-in's
|
|
1036
|
+
const head = await it.next()
|
|
1037
|
+
if (head.value?.kind === "error") return failedBefore(head.value.text)
|
|
1038
|
+
let pending = head
|
|
1039
|
+
|
|
1002
1040
|
const enc = new TextEncoder()
|
|
1003
1041
|
const sse = (type, data) => enc.encode(`event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`)
|
|
1004
1042
|
let index = -1
|
|
@@ -1022,7 +1060,8 @@ async function reply(it, model, stream) {
|
|
|
1022
1060
|
open = kind
|
|
1023
1061
|
ctl.enqueue(sse("content_block_start", { index, content_block: block }))
|
|
1024
1062
|
}
|
|
1025
|
-
const r = await it.next()
|
|
1063
|
+
const r = pending ?? (await it.next())
|
|
1064
|
+
pending = null
|
|
1026
1065
|
if (r.done) return ctl.close()
|
|
1027
1066
|
const e = r.value
|
|
1028
1067
|
switch (e.kind) {
|
|
@@ -1062,7 +1101,9 @@ async function reply(it, model, stream) {
|
|
|
1062
1101
|
}
|
|
1063
1102
|
|
|
1064
1103
|
// generate answers a Messages request through Kiro's API: once more,
|
|
1065
|
-
// refreshed, when Kiro turns the token down (403).
|
|
1104
|
+
// refreshed, when Kiro turns the token down (403). As the built-in: no
|
|
1105
|
+
// credentials is a 401, a failed refresh after the 403 a 502; none marks
|
|
1106
|
+
// the account.
|
|
1066
1107
|
async function generate(creds, auth, req, signal) {
|
|
1067
1108
|
const model = String(req.model ?? "auto")
|
|
1068
1109
|
const budget = thinking(req, model)
|
|
@@ -1070,7 +1111,7 @@ async function generate(creds, auth, req, signal) {
|
|
|
1070
1111
|
try {
|
|
1071
1112
|
a = await creds(auth)
|
|
1072
1113
|
} catch (e) {
|
|
1073
|
-
return errorResponse(
|
|
1114
|
+
return errorResponse(401, e.message)
|
|
1074
1115
|
}
|
|
1075
1116
|
let res
|
|
1076
1117
|
try {
|
|
@@ -1084,7 +1125,7 @@ async function generate(creds, auth, req, signal) {
|
|
|
1084
1125
|
try {
|
|
1085
1126
|
a = await creds(auth, true)
|
|
1086
1127
|
} catch (e) {
|
|
1087
|
-
return errorResponse(
|
|
1128
|
+
return errorResponse(502, e.message)
|
|
1088
1129
|
}
|
|
1089
1130
|
try {
|
|
1090
1131
|
res = await sendKiro(a, buildKiro(req, model, a.profile, budget), signal)
|
|
@@ -1094,10 +1135,11 @@ async function generate(creds, auth, req, signal) {
|
|
|
1094
1135
|
}
|
|
1095
1136
|
}
|
|
1096
1137
|
if (!res.ok) {
|
|
1138
|
+
// Kiro's own 401 is passed on, as the built-in did, the sign-in kept
|
|
1097
1139
|
const f = failure(res.status, (await res.text()).slice(0, 1 << 20))
|
|
1098
1140
|
return errorResponse(f.status, f.message)
|
|
1099
1141
|
}
|
|
1100
|
-
return reply(events(res.body, model, budget), model, req.stream === true)
|
|
1142
|
+
return kept(await reply(events(res.body, model, budget), model, req.stream === true))
|
|
1101
1143
|
}
|
|
1102
1144
|
|
|
1103
1145
|
// ---- signing in ----------------------------------------------------------------------
|
|
@@ -1304,14 +1346,15 @@ export async function KiroAuthPlugin({ client } = {}) {
|
|
|
1304
1346
|
},
|
|
1305
1347
|
}
|
|
1306
1348
|
},
|
|
1307
|
-
// the account's credits, as magpie's built-in showed them
|
|
1349
|
+
// the account's credits, as magpie's built-in showed them; a read,
|
|
1350
|
+
// clean or failed, marked the built-in's account nothing
|
|
1308
1351
|
async usage(getAuth) {
|
|
1309
1352
|
const auth = await getAuth()
|
|
1310
|
-
if (!(auth?.type === "api" && auth.key) && auth?.type !== "oauth") return { error: "not signed in" }
|
|
1353
|
+
if (!(auth?.type === "api" && auth.key) && auth?.type !== "oauth") return { error: "not signed in", signIn: "kept" }
|
|
1311
1354
|
try {
|
|
1312
|
-
return usageOf(await usageLimits(await creds(auth), true))
|
|
1355
|
+
return { ...usageOf(await usageLimits(await creds(auth), true)), signIn: "kept" }
|
|
1313
1356
|
} catch (e) {
|
|
1314
|
-
return { error: e.message }
|
|
1357
|
+
return { error: e.message, signIn: "kept" }
|
|
1315
1358
|
}
|
|
1316
1359
|
},
|
|
1317
1360
|
methods: [
|
|
@@ -1348,4 +1391,4 @@ export async function KiroAuthPlugin({ client } = {}) {
|
|
|
1348
1391
|
}
|
|
1349
1392
|
|
|
1350
1393
|
// for tests
|
|
1351
|
-
export const _internal = { usageOf, buildKiro, events, reply, failure, toolID, thinking, readCLI, readIDE, regionOf, planName, frames }
|
|
1394
|
+
export const _internal = { generate, refresh, usageOf, buildKiro, events, reply, failure, toolID, thinking, readCLI, readIDE, regionOf, planName, frames }
|