@magpie-community/opencode-workbuddy-auth 0.1.43 → 0.1.44
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 +92 -42
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -459,8 +459,28 @@ const SALT = randomBytes(16).toString("hex")
|
|
|
459
459
|
|
|
460
460
|
// idFor is the 32 hex digits WorkBuddy's own ids are made of, made of a
|
|
461
461
|
// name of the thing the id is for (a turn, a conversation) and what names it.
|
|
462
|
+
// key is a string, or the pieces it is made of: the digest is the same
|
|
463
|
+
// either way, and a key made of a 45 MiB message is never joined into one
|
|
464
|
+
// string to be hashed (plugins#461). Each piece goes to the hash whole:
|
|
465
|
+
// Bun reads a one-byte string's bytes in place, where a piece cut into
|
|
466
|
+
// slices was measured costing a copy of each slice.
|
|
462
467
|
function idFor(kind, key) {
|
|
463
|
-
|
|
468
|
+
const h = createHash("sha256").update(`${SALT}|${kind}|`)
|
|
469
|
+
// a surrogate pair's first half ending one piece waits for the next, as
|
|
470
|
+
// the joined key would hold the pair together
|
|
471
|
+
let held = ""
|
|
472
|
+
for (let piece of typeof key === "string" ? [key] : key) {
|
|
473
|
+
if (held) piece = held + piece
|
|
474
|
+
held = ""
|
|
475
|
+
const c = piece.charCodeAt(piece.length - 1)
|
|
476
|
+
if (c >= 0xd800 && c <= 0xdbff) {
|
|
477
|
+
held = piece.slice(-1)
|
|
478
|
+
piece = piece.slice(0, -1)
|
|
479
|
+
}
|
|
480
|
+
h.update(piece)
|
|
481
|
+
}
|
|
482
|
+
if (held) h.update(held)
|
|
483
|
+
return h.digest("hex").slice(0, 32)
|
|
464
484
|
}
|
|
465
485
|
|
|
466
486
|
// signatureOf is a message content's signature: its text, and a digest of
|
|
@@ -469,14 +489,20 @@ function idFor(kind, key) {
|
|
|
469
489
|
// clients of Anthropic's shape — is left out: it is not what the user said,
|
|
470
490
|
// and it changes with every step of the send.
|
|
471
491
|
function signatureOf(content) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
492
|
+
return signatureParts(content).join("")
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// signatureParts is signatureOf in the pieces it is joined from, none empty
|
|
496
|
+
function signatureParts(content) {
|
|
497
|
+
if (typeof content === "string") return content ? [content] : []
|
|
498
|
+
if (!Array.isArray(content)) return []
|
|
499
|
+
const out = []
|
|
475
500
|
for (const part of content) {
|
|
476
501
|
const type = part?.type ?? ""
|
|
477
|
-
if (type === "" || type === "text")
|
|
478
|
-
|
|
479
|
-
|
|
502
|
+
if (type === "" || type === "text") {
|
|
503
|
+
if (typeof part?.text === "string" && part.text) out.push(part.text)
|
|
504
|
+
} else if (type !== "tool_result" && type !== "tool-result") {
|
|
505
|
+
out.push(`[${type}:${createHash("sha256").update(JSON.stringify(part)).digest("hex").slice(0, 8)}]`)
|
|
480
506
|
}
|
|
481
507
|
}
|
|
482
508
|
return out
|
|
@@ -522,13 +548,18 @@ function hasBlock(m, type) {
|
|
|
522
548
|
// has none at all: the caller then makes a fresh id for that request, as
|
|
523
549
|
// before these were derived at all.
|
|
524
550
|
function turnKey(chat) {
|
|
551
|
+
return turnParts(chat).join("")
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// turnParts is turnKey in pieces (idFor takes them): [] for ""
|
|
555
|
+
function turnParts(chat) {
|
|
525
556
|
const msgs = Array.isArray(chat?.messages) ? chat.messages : []
|
|
526
557
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
527
558
|
if (msgs[i]?.role !== "user" || answeredBefore(msgs, i)) continue
|
|
528
|
-
const sig =
|
|
529
|
-
return sig ? `u${i}
|
|
559
|
+
const sig = signatureParts(msgs[i].content)
|
|
560
|
+
return sig.length ? [`u${i}:`, ...sig] : []
|
|
530
561
|
}
|
|
531
|
-
return
|
|
562
|
+
return []
|
|
532
563
|
}
|
|
533
564
|
|
|
534
565
|
// conversationKey names the conversation a chat belongs to: the session
|
|
@@ -538,14 +569,19 @@ function turnKey(chat) {
|
|
|
538
569
|
// conversation id is sent then: a made-up one would be a conversation of the
|
|
539
570
|
// request's own, which is what fragments the usage detail in the first place.
|
|
540
571
|
function conversationKey(chat, session) {
|
|
541
|
-
|
|
572
|
+
return conversationParts(chat, session).join("")
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// conversationParts is conversationKey in pieces (idFor takes them): [] for ""
|
|
576
|
+
function conversationParts(chat, session) {
|
|
577
|
+
if (session) return [`session:${session}`]
|
|
542
578
|
const msgs = Array.isArray(chat?.messages) ? chat.messages : []
|
|
543
579
|
for (const m of msgs) {
|
|
544
580
|
if (m?.role !== "user") continue
|
|
545
|
-
const sig =
|
|
546
|
-
if (sig) return
|
|
581
|
+
const sig = signatureParts(m.content)
|
|
582
|
+
if (sig.length) return ["first:", ...sig]
|
|
547
583
|
}
|
|
548
|
-
return
|
|
584
|
+
return []
|
|
549
585
|
}
|
|
550
586
|
|
|
551
587
|
// isMessages says whether a request is an Anthropic Messages one, which
|
|
@@ -684,17 +720,17 @@ function attend(headers, body, session) {
|
|
|
684
720
|
}
|
|
685
721
|
|
|
686
722
|
function attendChat(headers, chat, session) {
|
|
687
|
-
const conv =
|
|
723
|
+
const conv = conversationParts(chat, session)
|
|
688
724
|
const msg = hex()
|
|
689
725
|
headers.set("X-Conversation-Message-ID", msg)
|
|
690
726
|
headers.set("X-Request-ID", msg)
|
|
691
727
|
if (!headers.has("X-Conversation-Request-ID")) {
|
|
692
|
-
const turn =
|
|
728
|
+
const turn = turnParts(chat)
|
|
693
729
|
// a turn is a conversation's own: two conversations whose user message
|
|
694
730
|
// reads the same are still two sends, not one
|
|
695
|
-
headers.set("X-Conversation-Request-ID", turn ? idFor("turn",
|
|
731
|
+
headers.set("X-Conversation-Request-ID", turn.length ? idFor("turn", [...conv, "|", ...turn]) : hex())
|
|
696
732
|
}
|
|
697
|
-
if (conv && !headers.has("X-Conversation-ID")) headers.set("X-Conversation-ID", idFor("conversation", conv))
|
|
733
|
+
if (conv.length && !headers.has("X-Conversation-ID")) headers.set("X-Conversation-ID", idFor("conversation", conv))
|
|
698
734
|
}
|
|
699
735
|
|
|
700
736
|
// FLAGGED are the words WorkBuddy refuses a chat for, "Illegal API
|
|
@@ -2237,13 +2273,19 @@ const AFTER_TOOLS_REFUSED = /must end with a user message|["']code["']\s*:\s*["'
|
|
|
2237
2273
|
// ending with the assistant's message is a prefill, refused as it is.
|
|
2238
2274
|
function replyAfterTools(body) {
|
|
2239
2275
|
const b = chatOf(body)
|
|
2276
|
+
return replyAfterToolsOn(b) ? JSON.stringify(b) : null
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// replyAfterToolsOn makes replyAfterTools's change on a chat's object; false
|
|
2280
|
+
// for a chat it doesn't take
|
|
2281
|
+
function replyAfterToolsOn(b) {
|
|
2240
2282
|
const msgs = b?.messages
|
|
2241
|
-
if (!Array.isArray(msgs)) return
|
|
2283
|
+
if (!Array.isArray(msgs)) return false
|
|
2242
2284
|
let i = msgs.length - 1
|
|
2243
2285
|
while (i >= 0 && msgs[i]?.role === "user") i--
|
|
2244
|
-
if (i === msgs.length - 1 || i < 0 || msgs[i]?.role !== "tool") return
|
|
2286
|
+
if (i === msgs.length - 1 || i < 0 || msgs[i]?.role !== "tool") return false
|
|
2245
2287
|
msgs.splice(i + 1, 0, { role: "assistant", content: "" })
|
|
2246
|
-
return
|
|
2288
|
+
return true
|
|
2247
2289
|
}
|
|
2248
2290
|
|
|
2249
2291
|
// FREE_ONLY is the switch that keeps an account off credits (#430): its
|
|
@@ -2411,18 +2453,6 @@ function makePlugin(site) {
|
|
|
2411
2453
|
sign(site, a, headers)
|
|
2412
2454
|
let body = init?.body
|
|
2413
2455
|
if (body === undefined && req) body = await req.clone().text()
|
|
2414
|
-
// the switch is checked before anything is sent: a model not
|
|
2415
|
-
// free, or one whose price can't be read, never reaches WorkBuddy
|
|
2416
|
-
if (freeOnly()) {
|
|
2417
|
-
const model = chatOf(typeof body === "string" ? body : bodyString(body))?.model
|
|
2418
|
-
let list
|
|
2419
|
-
try {
|
|
2420
|
-
list = await listed(a)
|
|
2421
|
-
} catch (e) {
|
|
2422
|
-
return notFree(site, model, e?.message ?? String(e))
|
|
2423
|
-
}
|
|
2424
|
-
if (!list.some((m) => m.id === model && m.free)) return notFree(site, model)
|
|
2425
|
-
}
|
|
2426
2456
|
// An Anthropic request goes as the agent sent it: WorkBuddy
|
|
2427
2457
|
// serves these models on Messages itself, and the rewrites
|
|
2428
2458
|
// below (a default system message, the chat endpoint's
|
|
@@ -2433,12 +2463,26 @@ function makePlugin(site) {
|
|
|
2433
2463
|
// Codex's and DeepSeek Harness's openings all answer on
|
|
2434
2464
|
// /v1/messages, where a chat is refused 11128.
|
|
2435
2465
|
const native = isMessages(req ? req.url : String(input), body)
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
let changed
|
|
2440
|
-
body =
|
|
2441
|
-
if (
|
|
2466
|
+
// read once (prepared): nothing below parses the body again on
|
|
2467
|
+
// its way out, and nothing keeps the parsed chat once it is
|
|
2468
|
+
// written (plugins#461)
|
|
2469
|
+
let chat, changed, names
|
|
2470
|
+
;({ body, chat, changed, names } = prepared(body, native))
|
|
2471
|
+
if (native) names = {}
|
|
2472
|
+
// the switch is checked before anything is sent: a model not
|
|
2473
|
+
// free, or one whose price can't be read, never reaches
|
|
2474
|
+
// WorkBuddy. The model is read off the chat parsed above; a body
|
|
2475
|
+
// prepared leaves unread (bytes on Messages) is read for it.
|
|
2476
|
+
if (freeOnly()) {
|
|
2477
|
+
const model = (chat ?? chatOf(typeof body === "string" ? body : bodyString(body)))?.model
|
|
2478
|
+
let list
|
|
2479
|
+
try {
|
|
2480
|
+
list = await listed(a)
|
|
2481
|
+
} catch (e) {
|
|
2482
|
+
return notFree(site, model, e?.message ?? String(e))
|
|
2483
|
+
}
|
|
2484
|
+
if (!list.some((m) => m.id === model && m.free)) return notFree(site, model)
|
|
2485
|
+
}
|
|
2442
2486
|
// a chat that names no effort is asked at the model's default,
|
|
2443
2487
|
// as WorkBuddy's client asks it (withDefaultEffort); with no
|
|
2444
2488
|
// list to read (effortList) the chat goes as it came
|
|
@@ -2502,10 +2546,16 @@ function makePlugin(site) {
|
|
|
2502
2546
|
// words at once (see systemAsUser)
|
|
2503
2547
|
const sysKey = native ? "" : systemKeyOf(chat)
|
|
2504
2548
|
if (sysKey && refusedSystems.has(sysKey) && systemAsUserOn(chat)) changed = true
|
|
2549
|
+
// shaped is a body as such a model takes it. The first try's
|
|
2550
|
+
// is made on the chat already parsed, after the move above,
|
|
2551
|
+
// as shaped would make it of the body written; a resend with
|
|
2552
|
+
// the system prompt moved (below) is shaped again, and a body
|
|
2553
|
+
// that has the reply already is left as it is.
|
|
2554
|
+
const shaped = (b) => (!native && endsAfterTools.has(model) ? replyAfterTools(b) ?? b : b)
|
|
2555
|
+
if (!native && endsAfterTools.has(model) && replyAfterToolsOn(chat)) changed = true
|
|
2505
2556
|
if (changed) body = JSON.stringify(chat)
|
|
2506
2557
|
chat = null
|
|
2507
|
-
|
|
2508
|
-
let res = await send(shaped(body))
|
|
2558
|
+
let res = await send(body)
|
|
2509
2559
|
// a signature WorkBuddy's own signing didn't make — a session
|
|
2510
2560
|
// moved here from another provider keeps its thinking blocks,
|
|
2511
2561
|
// and a truncated or altered one reads the same way — is 11159
|
|
@@ -2733,4 +2783,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
2733
2783
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
2734
2784
|
|
|
2735
2785
|
// for tests
|
|
2736
|
-
export const _internal = { withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
|
2786
|
+
export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-workbuddy-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "OpenCode provider plugins for WorkBuddy's plans: WorkBuddy (CodeBuddy, copilot.tencent.com) and WorkBuddy AI (www.workbuddy.ai)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|