@magpie-community/opencode-kiro-auth 0.1.16 → 0.1.18
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/README.md +5 -0
- package/index.mjs +123 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,11 @@ request:
|
|
|
96
96
|
- A tool's images (a screenshot, Codex's view_image) go with the
|
|
97
97
|
message that answers the call, as Kiro's tool results hold text alone.
|
|
98
98
|
- Only the latest images are sent.
|
|
99
|
+
- An image Kiro would turn away for size (over 5 MiB as Base64, which is
|
|
100
|
+
3.75 MiB of file) is made smaller on the request being sent: its long
|
|
101
|
+
edge brought to 2000 pixels, then WebP or JPEG, smaller each try. One
|
|
102
|
+
that can't be read or brought under is replaced by a short note, so the
|
|
103
|
+
conversation goes on (#428). Smaller images are sent as they are.
|
|
99
104
|
- Tools the history used but the request doesn't offer are declared.
|
|
100
105
|
- It turns the AWS event stream Kiro answers with back into Messages'
|
|
101
106
|
events (streamed) or one message. The reply includes text, thinking,
|
package/index.mjs
CHANGED
|
@@ -414,7 +414,10 @@ function goG(x) {
|
|
|
414
414
|
|
|
415
415
|
// usageOf is the allowance Get-Usage-Limits tells, as magpie's built-in
|
|
416
416
|
// said it: the account's email, the credits of each allowance, the month's and a free trial's
|
|
417
|
-
// while it runs.
|
|
417
|
+
// while it runs. Each window carries its count (amount, limit, unit), so
|
|
418
|
+
// magpie can say what is left of it when the user shows what is left
|
|
419
|
+
// (#408): display alone is the vendor's text, which magpie can't turn
|
|
420
|
+
// round.
|
|
418
421
|
function usageOf(l) {
|
|
419
422
|
const at = (secs) => (secs > 0 ? new Date(Math.trunc(secs) * 1000).toISOString() : undefined)
|
|
420
423
|
const count = (used, limit) => used.toFixed(2).replace(/0+$/, "").replace(/\.$/, "") + " / " + goG(limit)
|
|
@@ -424,13 +427,14 @@ function usageOf(l) {
|
|
|
424
427
|
const t = u?.freeTrialInfo
|
|
425
428
|
if (t && String(t.freeTrialStatus ?? "").toUpperCase() === "ACTIVE" && num(t.usageLimitWithPrecision) > 0) {
|
|
426
429
|
const used = num(t.currentUsageWithPrecision), limit = num(t.usageLimitWithPrecision)
|
|
427
|
-
windows.push({ name: "Free trial", used: (100 * used) / limit, resetsAt: at(num(t.freeTrialExpiry)), display: count(used, limit)
|
|
430
|
+
windows.push({ name: "Free trial", used: (100 * used) / limit, resetsAt: at(num(t.freeTrialExpiry)), display: count(used, limit),
|
|
431
|
+
amount: used, limit, unit: "credits" })
|
|
428
432
|
}
|
|
429
433
|
const used = num(u?.currentUsageWithPrecision), limit = num(u?.usageLimitWithPrecision)
|
|
430
434
|
if (limit <= 0) continue
|
|
431
435
|
const reset = num(u?.nextDateReset) || num(l?.nextDateReset)
|
|
432
436
|
windows.push({ name: u?.displayNamePlural || u?.displayName || "Credits", used: (100 * used) / limit, resetsAt: at(reset),
|
|
433
|
-
display: count(used, limit), span: 30 * 24 * 3600 })
|
|
437
|
+
display: count(used, limit), amount: used, limit, unit: "credits", span: 30 * 24 * 3600 })
|
|
434
438
|
}
|
|
435
439
|
const email = l?.userInfo?.email
|
|
436
440
|
return { plan: planName(l?.subscriptionInfo?.subscriptionTitle), ...(typeof email === "string" && email ? { user: email } : {}), windows }
|
|
@@ -706,6 +710,106 @@ function addImage(images, p) {
|
|
|
706
710
|
if (p.source?.type === "base64" && p.source.data) images.push({ format: imageFormat(p.source.media_type), source: { bytes: p.source.data } })
|
|
707
711
|
}
|
|
708
712
|
|
|
713
|
+
// ---- images over Kiro's size limit ---------------------------------------------------
|
|
714
|
+
//
|
|
715
|
+
// Kiro turns away an image whose Base64 is over 5 MiB, counted on the
|
|
716
|
+
// encoded string, not the file (magpie-community/plugins#428):
|
|
717
|
+
// messages.130.content.2.image.source.base64: image exceeds 5 MB maximum:
|
|
718
|
+
// 5561904 bytes > 5242880 bytes (IMAGE_SIZE_EXCEEDED)
|
|
719
|
+
// 5561904 is that image's Base64 length (4,171,428 bytes decoded), so the
|
|
720
|
+
// largest file that fits is 3,932,160 bytes. kiro-cli's own gate is 10 MB of
|
|
721
|
+
// raw file (MAX_IMAGE_SIZE, aws/amazon-q-developer-cli chat/consts.rs), which
|
|
722
|
+
// lets such an image in and then every later turn fails on it
|
|
723
|
+
// (kirodotdev/Kiro#11497). The image stays in history, so each turn would
|
|
724
|
+
// fail the same way: an image over the limit is made smaller on the request
|
|
725
|
+
// being sent, and one that can't be is replaced by a note.
|
|
726
|
+
const IMAGE_LIMIT = 5 * 1024 * 1024 // Base64 characters
|
|
727
|
+
const IMAGE_EDGE = 2000 // a shrunk image's long edge: Kiro's web app's limit (kirodotdev/Kiro#8884), above what Claude looks at
|
|
728
|
+
const IMAGE_MIN_EDGE = 256
|
|
729
|
+
const IMAGE_MAX_PIXELS = 50_000_000 // a decoded image is ~4 bytes a pixel: a 200MB ceiling, and a header claiming more is refused unread
|
|
730
|
+
const IMAGE_MAX_INPUT = 64 * 1024 * 1024 // Base64 characters looked at all
|
|
731
|
+
const IMAGE_DEADLINE = 30_000 // ms one image may take
|
|
732
|
+
const fitted = new Map() // sha256 of the Base64 → Promise of { format, bytes } or { note }, newest last
|
|
733
|
+
const FITTED_KEEP = 4
|
|
734
|
+
|
|
735
|
+
const mib = (n) => (n / (1024 * 1024)).toFixed(1) + " MB"
|
|
736
|
+
|
|
737
|
+
// shrink tries, in order, the image's own kind at Claude's size, then a
|
|
738
|
+
// lossy kind (WebP keeps transparency, JPEG stays JPEG) smaller each time,
|
|
739
|
+
// until the Base64 fits.
|
|
740
|
+
async function shrink(b64) {
|
|
741
|
+
const I = globalThis.Bun?.Image
|
|
742
|
+
if (typeof I !== "function") return { note: "this plugin host can't resize images" }
|
|
743
|
+
const started = Date.now()
|
|
744
|
+
const raw = Buffer.from(b64, "base64")
|
|
745
|
+
let meta
|
|
746
|
+
try {
|
|
747
|
+
meta = await new I(raw, { maxPixels: IMAGE_MAX_PIXELS }).metadata()
|
|
748
|
+
} catch (e) {
|
|
749
|
+
return { note: e?.code === "ERR_IMAGE_TOO_MANY_PIXELS" ? "it has too many pixels to resize" : "it couldn't be read as an image" }
|
|
750
|
+
}
|
|
751
|
+
const long = Math.max(meta.width, meta.height)
|
|
752
|
+
const jpeg = meta.format === "jpeg"
|
|
753
|
+
const own = jpeg ? { format: "jpeg", opts: { quality: 85 } } : meta.format === "webp" ? { format: "webp", opts: { quality: 85 } } : { format: "png", opts: { compressionLevel: 9 } }
|
|
754
|
+
const lossy = (quality) => (jpeg ? { format: "jpeg", opts: { quality } } : { format: "webp", opts: { quality } })
|
|
755
|
+
const tries = [{ edge: Math.min(long, IMAGE_EDGE), ...own }, { edge: Math.min(long, IMAGE_EDGE), ...lossy(80) }]
|
|
756
|
+
for (let edge = Math.min(long, IMAGE_EDGE) * 0.7; edge >= IMAGE_MIN_EDGE; edge *= 0.7) tries.push({ edge: Math.round(edge), ...lossy(75) })
|
|
757
|
+
for (const t of tries) {
|
|
758
|
+
if (Date.now() - started > IMAGE_DEADLINE) return { note: "resizing it took too long" }
|
|
759
|
+
let out
|
|
760
|
+
try {
|
|
761
|
+
let img = new I(raw, { maxPixels: IMAGE_MAX_PIXELS })
|
|
762
|
+
if (t.edge < long) img = img.resize(t.edge, t.edge, { fit: "inside", withoutEnlargement: true })
|
|
763
|
+
out = await img[t.format](t.opts).toBase64()
|
|
764
|
+
} catch (e) {
|
|
765
|
+
if (e?.code === "ERR_IMAGE_FORMAT_UNSUPPORTED") continue
|
|
766
|
+
return { note: "it couldn't be resized" }
|
|
767
|
+
}
|
|
768
|
+
if (out.length <= IMAGE_LIMIT) return { format: t.format, bytes: out }
|
|
769
|
+
}
|
|
770
|
+
return { note: "it couldn't be made small enough" }
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function fitOne(b64) {
|
|
774
|
+
const key = createHash("sha256").update(b64).digest("hex")
|
|
775
|
+
let p = fitted.get(key)
|
|
776
|
+
if (p) fitted.delete(key) // kept as the newest below
|
|
777
|
+
else p = b64.length > IMAGE_MAX_INPUT ? Promise.resolve({ note: "it is too large to resize" }) : shrink(b64).catch(() => ({ note: "it couldn't be resized" }))
|
|
778
|
+
fitted.set(key, p)
|
|
779
|
+
while (fitted.size > FITTED_KEEP) fitted.delete(fitted.keys().next().value)
|
|
780
|
+
return p
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// fitImages brings every image left in a built request under Kiro's limit,
|
|
784
|
+
// in place: the request is the plugin's own copy, never the caller's. An
|
|
785
|
+
// image already under it is sent as it is.
|
|
786
|
+
async function fitImages(out) {
|
|
787
|
+
const s = out.conversationState
|
|
788
|
+
const users = [...(s.history ?? []).map((e) => e.userInputMessage), s.currentMessage.userInputMessage].filter(Boolean)
|
|
789
|
+
for (const u of users) {
|
|
790
|
+
if (!u.images?.length) continue
|
|
791
|
+
const kept = []
|
|
792
|
+
const notes = []
|
|
793
|
+
for (const img of u.images) {
|
|
794
|
+
const b64 = img.source?.bytes
|
|
795
|
+
if (typeof b64 !== "string" || b64.length <= IMAGE_LIMIT) {
|
|
796
|
+
kept.push(img)
|
|
797
|
+
continue
|
|
798
|
+
}
|
|
799
|
+
const r = await fitOne(b64)
|
|
800
|
+
if (r.bytes) kept.push({ ...img, format: r.format, source: { ...img.source, bytes: r.bytes } })
|
|
801
|
+
else
|
|
802
|
+
notes.push(
|
|
803
|
+
`[An image (${img.format}, ${mib(b64.length)} as Base64) was left out of this message: Kiro takes at most ${mib(IMAGE_LIMIT)} per image, and ${r.note}.]`,
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
if (kept.length) u.images = kept
|
|
807
|
+
else delete u.images
|
|
808
|
+
if (notes.length) u.content = joinNonEmpty(u.content, notes.join("\n"))
|
|
809
|
+
}
|
|
810
|
+
return out
|
|
811
|
+
}
|
|
812
|
+
|
|
709
813
|
const textOf = (c) =>
|
|
710
814
|
typeof c === "string" ? c : Array.isArray(c) ? c.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("\n\n") : ""
|
|
711
815
|
const joinNonEmpty = (a, b) => (a && b ? a + "\n\n" + b : a + b)
|
|
@@ -740,8 +844,19 @@ function formatText(req) {
|
|
|
740
844
|
return "Respond with a single JSON value and nothing else, matching this JSON schema:\n" + JSON.stringify(f.schema) + "."
|
|
741
845
|
}
|
|
742
846
|
|
|
743
|
-
// buildKiro is the body of a generateAssistantResponse call
|
|
847
|
+
// buildKiro is the body of a generateAssistantResponse call, as it is
|
|
848
|
+
// before its images are fitted (kiroBody).
|
|
744
849
|
function buildKiro(req, model, profile, budget) {
|
|
850
|
+
return JSON.stringify(kiroRequest(req, model, profile, budget))
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// kiroBody is the body sent: buildKiro's, with every image it keeps under
|
|
854
|
+
// Kiro's size limit.
|
|
855
|
+
async function kiroBody(req, model, profile, budget) {
|
|
856
|
+
return JSON.stringify(await fitImages(kiroRequest(req, model, profile, budget)))
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function kiroRequest(req, model, profile, budget) {
|
|
745
860
|
const entries = []
|
|
746
861
|
// the messages the caller marked for its prompt cache (cache_control),
|
|
747
862
|
// which go to Kiro as a cachePoint on the message they end up in (#110)
|
|
@@ -897,7 +1012,7 @@ function buildKiro(req, model, profile, budget) {
|
|
|
897
1012
|
if (entries.length > 1) state.history = entries.slice(0, -1)
|
|
898
1013
|
const out = { conversationState: state, agentMode: "vibe" }
|
|
899
1014
|
if (profile) out.profileArn = profile
|
|
900
|
-
return
|
|
1015
|
+
return out
|
|
901
1016
|
}
|
|
902
1017
|
|
|
903
1018
|
function sendKiro(a, body, signal) {
|
|
@@ -1338,7 +1453,7 @@ async function generate(creds, auth, req, signal) {
|
|
|
1338
1453
|
}
|
|
1339
1454
|
let res
|
|
1340
1455
|
try {
|
|
1341
|
-
res = await sendKiro(a,
|
|
1456
|
+
res = await sendKiro(a, await kiroBody(req, model, a.profile, budget), signal)
|
|
1342
1457
|
} catch (e) {
|
|
1343
1458
|
if (e?.name === "AbortError") throw e
|
|
1344
1459
|
return errorResponse(502, e.message)
|
|
@@ -1351,7 +1466,7 @@ async function generate(creds, auth, req, signal) {
|
|
|
1351
1466
|
return errorResponse(502, e.message)
|
|
1352
1467
|
}
|
|
1353
1468
|
try {
|
|
1354
|
-
res = await sendKiro(a,
|
|
1469
|
+
res = await sendKiro(a, await kiroBody(req, model, a.profile, budget), signal)
|
|
1355
1470
|
} catch (e) {
|
|
1356
1471
|
if (e?.name === "AbortError") throw e
|
|
1357
1472
|
return errorResponse(502, e.message)
|
|
@@ -1630,4 +1745,4 @@ export async function KiroAuthPlugin({ client } = {}) {
|
|
|
1630
1745
|
}
|
|
1631
1746
|
|
|
1632
1747
|
// for tests
|
|
1633
|
-
export const _internal = { generate, refresh, usageOf, buildKiro, events, reply, failure, toolID, toolName, toolNames, thinking, configModel, runtimeModel, readCLI, readIDE, regionOf, planName, frames }
|
|
1748
|
+
export const _internal = { generate, refresh, usageOf, buildKiro, kiroBody, IMAGE_LIMIT, events, reply, failure, toolID, toolName, toolNames, thinking, configModel, runtimeModel, readCLI, readIDE, regionOf, planName, frames }
|