@magpie-community/opencode-workbuddy-auth 0.1.14 → 0.1.16

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 (3) hide show
  1. package/README.md +5 -0
  2. package/index.mjs +79 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -82,6 +82,11 @@ Each chat carries WorkBuddy's headers:
82
82
  A chat that doesn't open with a system message is given WorkBuddy's
83
83
  default one, as the app does.
84
84
 
85
+ The loader also translates OpenAI-compatible request spellings WorkBuddy
86
+ doesn't take: `max_completion_tokens` becomes `max_tokens`, `developer`
87
+ messages become `system`, and an object `tool_choice` becomes WorkBuddy's
88
+ string form.
89
+
85
90
  A chat's tools are given shapes WorkBuddy's models take. Without them
86
91
  WorkBuddy AI answers 400 "Invalid request parameters" (11133), DeepSeek's
87
92
  models to each shape below and Kimi's, GPT's, Gemini's and GLM's to some:
package/index.mjs CHANGED
@@ -77,9 +77,27 @@ const SITES = {
77
77
  platform: "workbuddy-ai",
78
78
  ai: true,
79
79
  models: AI_MODELS,
80
+ // models whose images WorkBuddy AI drops though its config says they
81
+ // take them (see textOnly)
82
+ noImages: /^glm-/,
80
83
  },
81
84
  }
82
85
 
86
+ // textOnly says whether site drops the images sent to model, whatever its
87
+ // config says. On 2026-10-13, with a real WorkBuddy AI account (Yirasumi on
88
+ // Discord), www.workbuddy.ai's /v3/config marked every model
89
+ // supportsImages: true, but a chat to glm-5.3-flash, glm-5.3 or glm-5.2
90
+ // came back saying "[System reminder: the current model does not support
91
+ // images. Content filtered. ...]": the server had taken the image out.
92
+ // Its other models (deepseek-v4.1-flash, kimi, gemini, grok, gpt) read
93
+ // them, and WorkBuddy (China)'s glm-5.3-flash does too, so this is WorkBuddy
94
+ // AI's GLM models alone. Said text-only, magpie's image-describing model
95
+ // reads the image for them; a user who said the model takes images keeps
96
+ // their own answer (magpie's settings, over what this list says).
97
+ function textOnly(site, model) {
98
+ return !!site.noImages?.test(model)
99
+ }
100
+
83
101
  const NPM = "@ai-sdk/openai-compatible"
84
102
  const hex = () => randomBytes(16).toString("hex")
85
103
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
@@ -487,8 +505,11 @@ function unflag(messages) {
487
505
  }
488
506
 
489
507
  // withSystem gives a chat that doesn't open with a system message
490
- // WorkBuddy's default one, as its app does, and takes FLAGGED's words out
491
- // of it.
508
+ // WorkBuddy's default one, as its app does, takes FLAGGED's words out of it,
509
+ // and applies the request spellings WorkBuddy's chat endpoint accepts. The
510
+ // OpenAI-compatible client normally uses max_completion_tokens and may call a
511
+ // reasoning prompt developer; WorkBuddy's endpoint takes max_tokens and
512
+ // system instead.
492
513
  function withSystem(body) {
493
514
  // a body magpie or OpenCode hands over as bytes is read as the text it is
494
515
  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) body = new TextDecoder().decode(body)
@@ -497,6 +518,16 @@ function withSystem(body) {
497
518
  const b = JSON.parse(body)
498
519
  if (!Array.isArray(b?.messages) || !b.messages.length) return body
499
520
  let changed = unflag(b.messages)
521
+ if (b.max_completion_tokens !== undefined) {
522
+ if (b.max_tokens === undefined) b.max_tokens = b.max_completion_tokens
523
+ delete b.max_completion_tokens
524
+ changed = true
525
+ }
526
+ for (const m of b.messages) {
527
+ if (m?.role !== "developer") continue
528
+ m.role = "system"
529
+ changed = true
530
+ }
500
531
  if (b.messages[0]?.role !== "system") {
501
532
  b.messages.unshift({ role: "system", content: "You are a helpful assistant." })
502
533
  changed = true
@@ -507,6 +538,49 @@ function withSystem(body) {
507
538
  }
508
539
  }
509
540
 
541
+ // normalizeToolChoice converts the object form emitted by OpenAI clients to
542
+ // WorkBuddy's string form. It runs after fitted(), so a named choice follows a
543
+ // tool name that was made safe for WorkBuddy.
544
+ function normalizeToolChoice(body) {
545
+ if (typeof body !== "string") return body
546
+ try {
547
+ const b = JSON.parse(body)
548
+ if (!b || typeof b !== "object" || Array.isArray(b) || !("tool_choice" in b)) return body
549
+ const choice = b.tool_choice
550
+ if (typeof choice === "string") {
551
+ if (choice.trim().toLowerCase() === "none") {
552
+ delete b.tool_choice
553
+ delete b.tools
554
+ delete b.functions
555
+ }
556
+ return JSON.stringify(b)
557
+ }
558
+ if (!choice || typeof choice !== "object" || Array.isArray(choice)) {
559
+ delete b.tool_choice
560
+ return JSON.stringify(b)
561
+ }
562
+ const type = typeof choice.type === "string" ? choice.type.trim().toLowerCase() : ""
563
+ if (type === "none") {
564
+ delete b.tool_choice
565
+ delete b.tools
566
+ delete b.functions
567
+ } else if (type === "auto" || type === "required") {
568
+ b.tool_choice = type
569
+ } else if (type === "function") {
570
+ const fn = choice.function && typeof choice.function === "object" && !Array.isArray(choice.function)
571
+ ? choice.function
572
+ : undefined
573
+ const name = typeof fn?.name === "string" ? fn.name.trim() : ""
574
+ b.tool_choice = name || "auto"
575
+ } else {
576
+ delete b.tool_choice
577
+ }
578
+ return JSON.stringify(b)
579
+ } catch {
580
+ return body
581
+ }
582
+ }
583
+
510
584
  // ---- tools WorkBuddy takes ----------------------------------------------------
511
585
 
512
586
  // WorkBuddy AI answers 400 code 11133 "Invalid request parameters" (the
@@ -1124,7 +1198,7 @@ async function liveModels(site, a) {
1124
1198
  if (d) {
1125
1199
  m.context = d.maxInputTokens
1126
1200
  m.output = d.maxOutputTokens
1127
- m.images = d.supportsImages === true
1201
+ m.images = d.supportsImages === true && !textOnly(site, id)
1128
1202
  const r = creditsOf(d.credits)
1129
1203
  if (r !== undefined) {
1130
1204
  m.free = r === 0
@@ -1220,7 +1294,7 @@ function makePlugin(site) {
1220
1294
  if (body === undefined && req) body = await req.clone().text()
1221
1295
  body = withSystem(body)
1222
1296
  const fit = fitted(body)
1223
- body = fit.body
1297
+ body = normalizeToolChoice(fit.body)
1224
1298
  attend(headers, body, session)
1225
1299
  const res = await fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body })
1226
1300
  return res.status >= 400 ? explained(res) : kept(await renamed(res, fit.names))
@@ -1338,4 +1412,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
1338
1412
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
1339
1413
 
1340
1414
  // for tests
1341
- export const _internal = { runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
1415
+ export const _internal = { textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-workbuddy-auth",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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",