@magpie-community/opencode-workbuddy-auth 0.1.43 → 0.1.45
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 +3 -1
- package/index.mjs +115 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -244,7 +244,9 @@ client names, `none` too, goes as it came.
|
|
|
244
244
|
|
|
245
245
|
A chat goes gzipped (`Content-Encoding: gzip`), as WorkBuddy's own client
|
|
246
246
|
sends its model requests (`RequestBodyGzip` in both builds' product.json).
|
|
247
|
-
Claude's Messages route goes as it came
|
|
247
|
+
Claude's Messages route goes as it came, and so does anything that isn't a
|
|
248
|
+
chat completion: magpie's drawings (`/v2/images/generations`,
|
|
249
|
+
`/v2/images/edits`) go as plain JSON, which is all the images API reads.
|
|
248
250
|
|
|
249
251
|
A model's price is its `credits` ("x0.29") and the config's
|
|
250
252
|
`modelPromotions`. A discount for whole days (Step-5-Preview's 独家限免) is
|
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
|
|
@@ -1300,9 +1336,13 @@ function fitted(body) {
|
|
|
1300
1336
|
// copilot.tencent.com against 258 MB down. Measured 2026-10-28 on
|
|
1301
1337
|
// www.workbuddy.ai: a gzipped chat answers as the plain one does. A
|
|
1302
1338
|
// Messages request (Claude on the China build) goes as it came: that route
|
|
1303
|
-
// wasn't tried gzipped.
|
|
1304
|
-
|
|
1305
|
-
|
|
1339
|
+
// wasn't tried gzipped. Only a chat completion goes gzipped (isCompletion):
|
|
1340
|
+
// WorkBuddy's images API reads its body as plain JSON, and magpie's drawing
|
|
1341
|
+
// (/v2/images/generations, /v2/images/edits) is sent through this same
|
|
1342
|
+
// fetch; gzipped it answered 400 11101 "invalid character '\x1f'" (iwinux on
|
|
1343
|
+
// Discord, 0.1.43). null when the body isn't sent gzipped.
|
|
1344
|
+
function gzipped(completion, b) {
|
|
1345
|
+
if (!completion) return null
|
|
1306
1346
|
if (typeof b === "string") return gzipSync(Buffer.from(b))
|
|
1307
1347
|
if (b instanceof ArrayBuffer || ArrayBuffer.isView(b)) return gzipSync(b)
|
|
1308
1348
|
return null
|
|
@@ -2138,6 +2178,19 @@ function effortsOf(d, site, id) {
|
|
|
2138
2178
|
return EFFORT_ORDER.filter((e) => set.has(e))
|
|
2139
2179
|
}
|
|
2140
2180
|
|
|
2181
|
+
// isCompletion is whether a request is a chat completion: one to
|
|
2182
|
+
// .../chat/completions, or, with no URL to read, a body holding a chat's
|
|
2183
|
+
// messages. Nothing else the loader's fetch sends (magpie's drawings to
|
|
2184
|
+
// /v2/images/*) is a chat: it goes neither gzipped nor given an effort.
|
|
2185
|
+
function isCompletion(url, chat) {
|
|
2186
|
+
let path = ""
|
|
2187
|
+
try {
|
|
2188
|
+
path = new URL(String(url ?? "")).pathname
|
|
2189
|
+
} catch {}
|
|
2190
|
+
if (path) return /\/chat\/completions$/.test(path)
|
|
2191
|
+
return Array.isArray(chat?.messages)
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2141
2194
|
// defaultEffortOf is the level WorkBuddy's own client asks a model for when
|
|
2142
2195
|
// the user picked none: its CLI's configureThinkingSettings (WorkBuddy
|
|
2143
2196
|
// 5.9.3, CLI 2.165.0) takes the session's effort, else the settings', else
|
|
@@ -2237,13 +2290,19 @@ const AFTER_TOOLS_REFUSED = /must end with a user message|["']code["']\s*:\s*["'
|
|
|
2237
2290
|
// ending with the assistant's message is a prefill, refused as it is.
|
|
2238
2291
|
function replyAfterTools(body) {
|
|
2239
2292
|
const b = chatOf(body)
|
|
2293
|
+
return replyAfterToolsOn(b) ? JSON.stringify(b) : null
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// replyAfterToolsOn makes replyAfterTools's change on a chat's object; false
|
|
2297
|
+
// for a chat it doesn't take
|
|
2298
|
+
function replyAfterToolsOn(b) {
|
|
2240
2299
|
const msgs = b?.messages
|
|
2241
|
-
if (!Array.isArray(msgs)) return
|
|
2300
|
+
if (!Array.isArray(msgs)) return false
|
|
2242
2301
|
let i = msgs.length - 1
|
|
2243
2302
|
while (i >= 0 && msgs[i]?.role === "user") i--
|
|
2244
|
-
if (i === msgs.length - 1 || i < 0 || msgs[i]?.role !== "tool") return
|
|
2303
|
+
if (i === msgs.length - 1 || i < 0 || msgs[i]?.role !== "tool") return false
|
|
2245
2304
|
msgs.splice(i + 1, 0, { role: "assistant", content: "" })
|
|
2246
|
-
return
|
|
2305
|
+
return true
|
|
2247
2306
|
}
|
|
2248
2307
|
|
|
2249
2308
|
// FREE_ONLY is the switch that keeps an account off credits (#430): its
|
|
@@ -2411,18 +2470,6 @@ function makePlugin(site) {
|
|
|
2411
2470
|
sign(site, a, headers)
|
|
2412
2471
|
let body = init?.body
|
|
2413
2472
|
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
2473
|
// An Anthropic request goes as the agent sent it: WorkBuddy
|
|
2427
2474
|
// serves these models on Messages itself, and the rewrites
|
|
2428
2475
|
// below (a default system message, the chat endpoint's
|
|
@@ -2433,22 +2480,37 @@ function makePlugin(site) {
|
|
|
2433
2480
|
// Codex's and DeepSeek Harness's openings all answer on
|
|
2434
2481
|
// /v1/messages, where a chat is refused 11128.
|
|
2435
2482
|
const native = isMessages(req ? req.url : String(input), body)
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
let changed
|
|
2440
|
-
body =
|
|
2441
|
-
if (
|
|
2483
|
+
// read once (prepared): nothing below parses the body again on
|
|
2484
|
+
// its way out, and nothing keeps the parsed chat once it is
|
|
2485
|
+
// written (plugins#461)
|
|
2486
|
+
let chat, changed, names
|
|
2487
|
+
;({ body, chat, changed, names } = prepared(body, native))
|
|
2488
|
+
if (native) names = {}
|
|
2489
|
+
// the switch is checked before anything is sent: a model not
|
|
2490
|
+
// free, or one whose price can't be read, never reaches
|
|
2491
|
+
// WorkBuddy. The model is read off the chat parsed above; a body
|
|
2492
|
+
// prepared leaves unread (bytes on Messages) is read for it.
|
|
2493
|
+
if (freeOnly()) {
|
|
2494
|
+
const model = (chat ?? chatOf(typeof body === "string" ? body : bodyString(body)))?.model
|
|
2495
|
+
let list
|
|
2496
|
+
try {
|
|
2497
|
+
list = await listed(a)
|
|
2498
|
+
} catch (e) {
|
|
2499
|
+
return notFree(site, model, e?.message ?? String(e))
|
|
2500
|
+
}
|
|
2501
|
+
if (!list.some((m) => m.id === model && m.free)) return notFree(site, model)
|
|
2502
|
+
}
|
|
2442
2503
|
// a chat that names no effort is asked at the model's default,
|
|
2443
2504
|
// as WorkBuddy's client asks it (withDefaultEffort); with no
|
|
2444
2505
|
// list to read (effortList) the chat goes as it came
|
|
2445
|
-
|
|
2506
|
+
const completion = !native && isCompletion(req ? req.url : input, chat)
|
|
2507
|
+
if (completion && chat && !namesEffort(chat) && withDefaultEffort(chat, await effortList(a))) changed = true
|
|
2446
2508
|
chatHeaders(site, headers)
|
|
2447
2509
|
attendChat(headers, chat, session)
|
|
2448
2510
|
const post = (b) => {
|
|
2449
2511
|
// a chat goes gzipped, as WorkBuddy's client sends it
|
|
2450
2512
|
// (gzipped); its size on the wire is what unanswered weighs
|
|
2451
|
-
const z = gzipped(
|
|
2513
|
+
const z = gzipped(completion, b)
|
|
2452
2514
|
if (z) headers.set("Content-Encoding", "gzip")
|
|
2453
2515
|
else headers.delete("Content-Encoding")
|
|
2454
2516
|
return fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: z ?? b }).catch((e) => unanswered(site, a, z ?? b, e, init?.signal))
|
|
@@ -2502,10 +2564,16 @@ function makePlugin(site) {
|
|
|
2502
2564
|
// words at once (see systemAsUser)
|
|
2503
2565
|
const sysKey = native ? "" : systemKeyOf(chat)
|
|
2504
2566
|
if (sysKey && refusedSystems.has(sysKey) && systemAsUserOn(chat)) changed = true
|
|
2567
|
+
// shaped is a body as such a model takes it. The first try's
|
|
2568
|
+
// is made on the chat already parsed, after the move above,
|
|
2569
|
+
// as shaped would make it of the body written; a resend with
|
|
2570
|
+
// the system prompt moved (below) is shaped again, and a body
|
|
2571
|
+
// that has the reply already is left as it is.
|
|
2572
|
+
const shaped = (b) => (!native && endsAfterTools.has(model) ? replyAfterTools(b) ?? b : b)
|
|
2573
|
+
if (!native && endsAfterTools.has(model) && replyAfterToolsOn(chat)) changed = true
|
|
2505
2574
|
if (changed) body = JSON.stringify(chat)
|
|
2506
2575
|
chat = null
|
|
2507
|
-
|
|
2508
|
-
let res = await send(shaped(body))
|
|
2576
|
+
let res = await send(body)
|
|
2509
2577
|
// a signature WorkBuddy's own signing didn't make — a session
|
|
2510
2578
|
// moved here from another provider keeps its thinking blocks,
|
|
2511
2579
|
// and a truncated or altered one reads the same way — is 11159
|
|
@@ -2733,4 +2801,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
2733
2801
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
2734
2802
|
|
|
2735
2803
|
// 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 }
|
|
2804
|
+
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.45",
|
|
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",
|