@magpie-community/opencode-workbuddy-auth 0.1.41 → 0.1.42

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 +130 -69
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -608,8 +608,13 @@ function isMessages(url, body) {
608
608
  // unsigned thinking is body itself, byte for byte.
609
609
  function withoutUnsigned(body) {
610
610
  const b = chatOf(body)
611
+ return unsignedOn(b) ? JSON.stringify(b) : body
612
+ }
613
+
614
+ // unsignedOn makes withoutUnsigned's change to a body's object; true when it made one
615
+ function unsignedOn(b) {
611
616
  const msgs = b?.messages
612
- if (!Array.isArray(msgs)) return body
617
+ if (!Array.isArray(msgs)) return false
613
618
  let changed = false
614
619
  for (const m of msgs) {
615
620
  if (m?.role !== "assistant" || !Array.isArray(m.content)) continue
@@ -618,7 +623,7 @@ function withoutUnsigned(body) {
618
623
  changed = true
619
624
  m.content = kept.length ? kept : [{ type: "text", text: "" }]
620
625
  }
621
- return changed ? JSON.stringify(b) : body
626
+ return changed
622
627
  }
623
628
 
624
629
  // withoutSignedThinking is a Messages body without any thinking block at
@@ -674,7 +679,10 @@ function sessionOf(headers) {
674
679
  // already there — the caller's own, or another plugin's — is kept, and the
675
680
  // message and request ids are the same one, as WorkBuddy's client has them.
676
681
  function attend(headers, body, session) {
677
- const chat = chatOf(body)
682
+ attendChat(headers, chatOf(body), session)
683
+ }
684
+
685
+ function attendChat(headers, chat, session) {
678
686
  const conv = conversationKey(chat, session)
679
687
  const msg = hex()
680
688
  headers.set("X-Conversation-Message-ID", msg)
@@ -792,29 +800,34 @@ function withSystem(body) {
792
800
  if (typeof body !== "string") return body
793
801
  try {
794
802
  const b = JSON.parse(body)
795
- if (!Array.isArray(b?.messages) || !b.messages.length) return body
796
- let changed = unflag(b.messages)
797
- if (b.max_completion_tokens !== undefined) {
798
- if (b.max_tokens === undefined) b.max_tokens = b.max_completion_tokens
799
- delete b.max_completion_tokens
800
- changed = true
801
- }
802
- for (const m of b.messages) {
803
- if (m?.role !== "developer") continue
804
- m.role = "system"
805
- changed = true
806
- }
807
- if (asReasoning(b.messages)) changed = true
808
- if (b.messages[0]?.role !== "system") {
809
- b.messages.unshift({ role: "system", content: "You are a helpful assistant." })
810
- changed = true
811
- }
812
- return changed ? JSON.stringify(b) : body
803
+ return systemOn(b) ? JSON.stringify(b) : body
813
804
  } catch {
814
805
  return body
815
806
  }
816
807
  }
817
808
 
809
+ // systemOn makes withSystem's changes to a chat's object; true when it made any
810
+ function systemOn(b) {
811
+ if (!Array.isArray(b?.messages) || !b.messages.length) return false
812
+ let changed = unflag(b.messages)
813
+ if (b.max_completion_tokens !== undefined) {
814
+ if (b.max_tokens === undefined) b.max_tokens = b.max_completion_tokens
815
+ delete b.max_completion_tokens
816
+ changed = true
817
+ }
818
+ for (const m of b.messages) {
819
+ if (m?.role !== "developer") continue
820
+ m.role = "system"
821
+ changed = true
822
+ }
823
+ if (asReasoning(b.messages)) changed = true
824
+ if (b.messages[0]?.role !== "system") {
825
+ b.messages.unshift({ role: "system", content: "You are a helpful assistant." })
826
+ changed = true
827
+ }
828
+ return changed
829
+ }
830
+
818
831
  // WorkBuddy's channel check reads the system and assistant messages only:
819
832
  // tried on 2026-10-21 with real accounts on both builds (glm-5.3,
820
833
  // deepseek-v4.1-flash, gpt-5.5), each refused line was served in the user's
@@ -841,7 +854,11 @@ function textOf(content) {
841
854
  // systemKey is the text of a chat's system messages, which a chat refused
842
855
  // once is known by: the same agent's next chat goes with systemAsUser at once
843
856
  function systemKey(body) {
844
- const msgs = chatOf(body)?.messages
857
+ return systemKeyOf(chatOf(body))
858
+ }
859
+
860
+ function systemKeyOf(chat) {
861
+ const msgs = chat?.messages
845
862
  if (!Array.isArray(msgs)) return ""
846
863
  const key = msgs.filter((m) => m?.role === "system").map((m) => textOf(m.content)).join("\n")
847
864
  return key.trim() === DEFAULT_SYSTEM ? "" : key
@@ -849,15 +866,21 @@ function systemKey(body) {
849
866
 
850
867
  function systemAsUser(body) {
851
868
  const b = chatOf(body)
869
+ return systemAsUserOn(b) ? JSON.stringify(b) : null
870
+ }
871
+
872
+ // systemAsUserOn makes systemAsUser's move on a chat's object; false when
873
+ // there's nothing to move
874
+ function systemAsUserOn(b) {
852
875
  const msgs = b?.messages
853
- if (!Array.isArray(msgs)) return null
876
+ if (!Array.isArray(msgs)) return false
854
877
  let i = 0
855
878
  const lead = []
856
879
  for (; i < msgs.length && msgs[i]?.role === "system"; i++) lead.push(textOf(msgs[i].content))
857
880
  const said = lead.join("\n\n").trim()
858
881
  const rest = msgs.slice(i).map((m) => (m?.role === "system" ? { ...m, role: "user" } : m))
859
882
  const moved = said && said !== DEFAULT_SYSTEM
860
- if (!moved && rest.every((m, k) => m === msgs[i + k])) return null
883
+ if (!moved && rest.every((m, k) => m === msgs[i + k])) return false
861
884
  if (moved) {
862
885
  const text = INSTRUCTIONS[0] + said + INSTRUCTIONS[1]
863
886
  const u = rest.findIndex((m) => m?.role === "user")
@@ -867,7 +890,7 @@ function systemAsUser(body) {
867
890
  else rest[u] = { ...rest[u], content: text }
868
891
  }
869
892
  b.messages = [{ role: "system", content: DEFAULT_SYSTEM }, ...rest]
870
- return JSON.stringify(b)
893
+ return true
871
894
  }
872
895
 
873
896
  // normalizeToolChoice converts the object form emitted by OpenAI clients to
@@ -877,42 +900,47 @@ function normalizeToolChoice(body) {
877
900
  if (typeof body !== "string") return body
878
901
  try {
879
902
  const b = JSON.parse(body)
880
- if (!b || typeof b !== "object" || Array.isArray(b) || !("tool_choice" in b)) return body
881
- const choice = b.tool_choice
882
- if (typeof choice === "string") {
883
- if (choice.trim().toLowerCase() === "none") {
884
- delete b.tool_choice
885
- delete b.tools
886
- delete b.functions
887
- }
888
- return JSON.stringify(b)
889
- }
890
- if (!choice || typeof choice !== "object" || Array.isArray(choice)) {
891
- delete b.tool_choice
892
- return JSON.stringify(b)
893
- }
894
- const type = typeof choice.type === "string" ? choice.type.trim().toLowerCase() : ""
895
- if (type === "none") {
896
- delete b.tool_choice
897
- delete b.tools
898
- delete b.functions
899
- } else if (type === "auto" || type === "required") {
900
- b.tool_choice = type
901
- } else if (type === "function") {
902
- const fn = choice.function && typeof choice.function === "object" && !Array.isArray(choice.function)
903
- ? choice.function
904
- : undefined
905
- const name = typeof fn?.name === "string" ? fn.name.trim() : ""
906
- b.tool_choice = name || "auto"
907
- } else {
908
- delete b.tool_choice
909
- }
910
- return JSON.stringify(b)
903
+ return toolChoiceOn(b) ? JSON.stringify(b) : body
911
904
  } catch {
912
905
  return body
913
906
  }
914
907
  }
915
908
 
909
+ // toolChoiceOn makes normalizeToolChoice's change to a chat's object; true
910
+ // when it made one (a string choice other than none is WorkBuddy's already)
911
+ function toolChoiceOn(b) {
912
+ if (!b || typeof b !== "object" || Array.isArray(b) || !("tool_choice" in b)) return false
913
+ const choice = b.tool_choice
914
+ if (typeof choice === "string") {
915
+ if (choice.trim().toLowerCase() !== "none") return false
916
+ delete b.tool_choice
917
+ delete b.tools
918
+ delete b.functions
919
+ return true
920
+ }
921
+ if (!choice || typeof choice !== "object" || Array.isArray(choice)) {
922
+ delete b.tool_choice
923
+ return true
924
+ }
925
+ const type = typeof choice.type === "string" ? choice.type.trim().toLowerCase() : ""
926
+ if (type === "none") {
927
+ delete b.tool_choice
928
+ delete b.tools
929
+ delete b.functions
930
+ } else if (type === "auto" || type === "required") {
931
+ b.tool_choice = type
932
+ } else if (type === "function") {
933
+ const fn = choice.function && typeof choice.function === "object" && !Array.isArray(choice.function)
934
+ ? choice.function
935
+ : undefined
936
+ const name = typeof fn?.name === "string" ? fn.name.trim() : ""
937
+ b.tool_choice = name || "auto"
938
+ } else {
939
+ delete b.tool_choice
940
+ }
941
+ return true
942
+ }
943
+
916
944
  // ---- tools WorkBuddy takes ----------------------------------------------------
917
945
 
918
946
  // WorkBuddy AI answers 400 code 11133 "Invalid request parameters" (the
@@ -1261,6 +1289,40 @@ function fitted(body) {
1261
1289
  }
1262
1290
  }
1263
1291
 
1292
+ // prepared is a request's body read once for every change the loader makes
1293
+ // to it and everything it reads off it (plugins#461): a long chat, 45 MiB
1294
+ // of history, was parsed six times on its way, once by each step, and
1295
+ // written out again whole by normalizeToolChoice even when nothing changed.
1296
+ // chat is the parsed body, changed whether any step changed it; the caller
1297
+ // writes it out once, when it did. Each step is the one its own function
1298
+ // above makes, in the same order.
1299
+ function prepared(body, native) {
1300
+ if (!native && (body instanceof ArrayBuffer || ArrayBuffer.isView(body))) body = new TextDecoder().decode(body)
1301
+ const none = { body, chat: null, names: new Map(), changed: false }
1302
+ if (typeof body !== "string") return none
1303
+ let chat
1304
+ try {
1305
+ chat = JSON.parse(body)
1306
+ } catch {
1307
+ return none
1308
+ }
1309
+ if (chat === null || typeof chat !== "object") return none
1310
+ if (native) return { body, chat, names: new Map(), changed: unsignedOn(chat) }
1311
+ try {
1312
+ let changed = systemOn(chat)
1313
+ const fit = fitTools(chat)
1314
+ if (fit.changed) changed = true
1315
+ if (toolChoiceOn(chat)) changed = true
1316
+ return { body, chat, names: fit.names, changed }
1317
+ } catch {
1318
+ // a shape a step can't take: the steps one by one, each keeping the
1319
+ // body as it was when it can't, as they always have
1320
+ const fit = fitted(withSystem(body))
1321
+ const out = normalizeToolChoice(fit.body)
1322
+ return { body: out, chat: chatOf(out), names: fit.names, changed: false }
1323
+ }
1324
+ }
1325
+
1264
1326
  // namedBack gives the model's calls in a chat answer (one JSON body, or an
1265
1327
  // SSE stream's chunks) the names the client gave its tools.
1266
1328
  function namedBack(v, names) {
@@ -2302,16 +2364,13 @@ function makePlugin(site) {
2302
2364
  // /v1/messages, where a chat is refused 11128.
2303
2365
  const native = isMessages(req ? req.url : String(input), body)
2304
2366
  let names = {}
2305
- if (!native) {
2306
- body = withSystem(body)
2307
- const fit = fitted(body)
2308
- body = normalizeToolChoice(fit.body)
2309
- names = fit.names
2310
- } else {
2311
- body = withoutUnsigned(body)
2312
- }
2367
+ const p = prepared(body, native)
2368
+ let chat = p.chat
2369
+ let changed = p.changed
2370
+ body = p.body
2371
+ if (!native) names = p.names
2313
2372
  chatHeaders(site, headers)
2314
- attend(headers, body, session)
2373
+ attendChat(headers, chat, session)
2315
2374
  const post = (b) =>
2316
2375
  fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: b }).catch((e) => unanswered(site, a, b, e, init?.signal))
2317
2376
  // a 401 is answered once, on whichever try meets it: by a
@@ -2358,11 +2417,13 @@ function makePlugin(site) {
2358
2417
  }
2359
2418
  // a model known to turn tool results then the user's message away
2360
2419
  // gets the empty reply between them up front (see endsAfterTools)
2361
- const model = chatOf(body)?.model
2420
+ const model = chat?.model
2362
2421
  // a system prompt WorkBuddy refused before goes as the user's
2363
2422
  // words at once (see systemAsUser)
2364
- const sysKey = native ? "" : systemKey(body)
2365
- if (sysKey && refusedSystems.has(sysKey)) body = systemAsUser(body) ?? body
2423
+ const sysKey = native ? "" : systemKeyOf(chat)
2424
+ if (sysKey && refusedSystems.has(sysKey) && systemAsUserOn(chat)) changed = true
2425
+ if (changed) body = JSON.stringify(chat)
2426
+ chat = null
2366
2427
  const shaped = (b) => (!native && endsAfterTools.has(model) ? replyAfterTools(b) ?? b : b)
2367
2428
  let res = await send(shaped(body))
2368
2429
  // a signature WorkBuddy's own signing didn't make — a session
@@ -2592,4 +2653,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
2592
2653
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
2593
2654
 
2594
2655
  // for tests
2595
- export const _internal = { 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, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
2656
+ export const _internal = { 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.41",
3
+ "version": "0.1.42",
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",