@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.
Files changed (2) hide show
  1. package/index.mjs +92 -42
  2. 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
- return createHash("sha256").update(`${SALT}|${kind}|${key}`).digest("hex").slice(0, 32)
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
- if (typeof content === "string") return content
473
- if (!Array.isArray(content)) return ""
474
- let out = ""
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") out += typeof part?.text === "string" ? part.text : ""
478
- else if (type !== "tool_result" && type !== "tool-result") {
479
- out += `[${type}:${createHash("sha256").update(JSON.stringify(part)).digest("hex").slice(0, 8)}]`
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 = signatureOf(msgs[i].content)
529
- return sig ? `u${i}:${sig}` : ""
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
- if (session) return `session:${session}`
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 = signatureOf(m.content)
546
- if (sig) return `first:${sig}`
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 = conversationKey(chat, session)
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 = turnKey(chat)
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", `${conv}|${turn}`) : hex())
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 null
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 null
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 JSON.stringify(b)
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
- let names = {}
2437
- const p = prepared(body, native)
2438
- let chat = p.chat
2439
- let changed = p.changed
2440
- body = p.body
2441
- if (!native) names = p.names
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
- const shaped = (b) => (!native && endsAfterTools.has(model) ? replyAfterTools(b) ?? b : b)
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.43",
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",