@magpie-community/opencode-workbuddy-auth 0.1.38 → 0.1.40

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 +20 -4
  2. package/index.mjs +92 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -124,7 +124,11 @@ estimate.
124
124
 
125
125
  Each chat carries WorkBuddy's headers:
126
126
  - `Authorization`, `X-User-Id`, `X-Domain`, `X-Product: SaaS`, `X-IDE-Type: WorkBuddy`;
127
- - `User-Agent: WorkBuddy/5.5.6`. Without it, WorkBuddy answers error 10085;
127
+ - the desktop CLI's `User-Agent`, `WorkBuddy/<app> <appName>/<app> CLI/<cli>`:
128
+ `WorkBuddy/5.9.3 WorkBuddy/5.9.3 CLI/2.165.0` on the China build and
129
+ `WorkBuddy/5.8.4 workbuddy-ai/5.8.4 CLI/2.161.0` on WorkBuddy AI, each
130
+ build's current app on 2026-10-28; `X-IDE-Version` is the app's version.
131
+ A User-Agent not starting `WorkBuddy/<version>` is answered error 10085;
128
132
  - the client's, which WorkBuddy's usage list shows as the client that asked
129
133
  (使用端): `X-Agent-Purpose: conversation`, `X-IDE-Name: WorkBuddy`,
130
134
  `X-IDE-Version`, `X-Agent-Intent: craft`, `X-Requested-With`. Both builds
@@ -139,7 +143,15 @@ Each chat carries WorkBuddy's headers:
139
143
  else the chat's first user message; nothing is made up when neither names
140
144
  one, a made-up conversation of the request's own being what splits one
141
145
  conversation into many in the usage detail;
142
- - WorkBuddy AI also takes `X-Agent-Type: main`.
146
+ - what the CLI adds to a model request alone: `X-Agent-Type: main` on both
147
+ builds, `X-Agent-Runtime: local-agent` on the China build (new in 5.9),
148
+ `Accept: application/json`, and the `X-Stainless-*` headers of the OpenAI
149
+ SDK it sends through (openai 6.25.0 on Electron 37's Node v22.21.1, a
150
+ 1200s timeout, no retries).
151
+
152
+ Not sent: `X-Device-Token`, which the desktop app gets from Tencent's
153
+ native device SDK before each prompt, and a `traceparent`, which the CLI
154
+ adds only while its server-side tracing is on.
143
155
 
144
156
  A chat that doesn't open with a system message is given WorkBuddy's
145
157
  default one, as the app does.
@@ -164,8 +176,12 @@ refusal rests the account.
164
176
 
165
177
  The loader also translates OpenAI-compatible request spellings WorkBuddy
166
178
  doesn't take: `max_completion_tokens` becomes `max_tokens`, `developer`
167
- messages become `system`, and an object `tool_choice` becomes WorkBuddy's
168
- string form.
179
+ messages become `system`, an object `tool_choice` becomes WorkBuddy's
180
+ string form, and an earlier assistant turn's `reasoning_content` (or
181
+ `reasoning_details` text) goes as `reasoning`, the field WorkBuddy's own
182
+ client sends. WorkBuddy drops `reasoning_content`, so a client such as Grok
183
+ Build that sends it lost its earlier thinking; nothing is folded into the
184
+ message's content.
169
185
 
170
186
  A chat's tools are given shapes WorkBuddy's models take. Without them
171
187
  WorkBuddy AI answers 400 "Invalid request parameters" (11133), DeepSeek's
package/index.mjs CHANGED
@@ -6,7 +6,8 @@
6
6
  // the token as WorkBuddy's desktop app does) or is taken from WorkBuddy
7
7
  // desktop's own sign-in. Its chats are chat completions at <endpoint>/v2,
8
8
  // streamed only, with WorkBuddy's headers: a request whose User-Agent isn't
9
- // WorkBuddy/<version> is refused (error 10085).
9
+ // WorkBuddy/<version> is refused (error 10085). Each build's headers are
10
+ // those of its current desktop app (see SITES' app, cli and appName).
10
11
 
11
12
  import { createHash, randomBytes } from "node:crypto"
12
13
  import { readFileSync } from "node:fs"
@@ -14,7 +15,6 @@ import { homedir } from "node:os"
14
15
  import { join } from "node:path"
15
16
 
16
17
  const APP_VERSION = "2.0.0" // the version WorkBuddy's sign-in page is opened with
17
- const UA_VERSION = "5.5.6" // WorkBuddy/<this> is the User-Agent the API takes
18
18
  const POLL_MS = 1000
19
19
  const SIGN_IN_MS = 5 * 60 * 1000
20
20
  const EARLY_MS = 60 * 1000 // a token this close to its end is refreshed before a request
@@ -67,6 +67,16 @@ const SITES = {
67
67
  authID: "workbuddy-desktop",
68
68
  platform: "workbuddy",
69
69
  ai: false,
70
+ // the desktop app the requests are WorkBuddy's own client's of: its
71
+ // version, its bundled CLI's (@genie/agent-cli), and the CLI's
72
+ // product.json applicationName. Read from WorkBuddy 5.9.3.42398134,
73
+ // the update feed's current build on 2026-10-28.
74
+ app: "5.9.3",
75
+ cli: "2.165.0",
76
+ appName: "WorkBuddy",
77
+ // 5.9's CLI says on each chat where the agent runs; the desktop's own
78
+ // is "local-agent" (X-Agent-Runtime, absent in 5.8)
79
+ runtime: "local-agent",
70
80
  // where this build's API keys are made (see keyAccount)
71
81
  keys: "https://www.workbuddy.cn/profile/keys",
72
82
  models: CN_MODELS,
@@ -81,6 +91,11 @@ const SITES = {
81
91
  authID: "workbuddy-desktop-ai",
82
92
  platform: "workbuddy-ai",
83
93
  ai: true,
94
+ // WorkBuddy AI 5.8.4.41366630, its update feed's current build on
95
+ // 2026-10-28 (see the China build's)
96
+ app: "5.8.4",
97
+ cli: "2.161.0",
98
+ appName: "workbuddy-ai",
84
99
  keys: "https://www.workbuddy.ai/profile/keys",
85
100
  models: AI_MODELS,
86
101
  // models whose images WorkBuddy AI drops though its config says they
@@ -155,7 +170,7 @@ async function call(site, method, path, headers = {}, body) {
155
170
  headers: {
156
171
  "Content-Type": "application/json",
157
172
  Accept: "application/json",
158
- "User-Agent": "WorkBuddy/" + UA_VERSION,
173
+ "User-Agent": "WorkBuddy/" + site.app,
159
174
  ...headers,
160
175
  },
161
176
  body: body === undefined ? undefined : JSON.stringify(body),
@@ -312,15 +327,51 @@ function sign(site, a, headers) {
312
327
  }
313
328
  headers.set("X-Product", "SaaS")
314
329
  headers.set("X-IDE-Type", "WorkBuddy")
315
- headers.set("User-Agent", "WorkBuddy/" + UA_VERSION)
330
+ headers.set("User-Agent", cliAgent(site))
316
331
  headers.set("X-Requested-With", "XMLHttpRequest")
317
332
  headers.set("X-Agent-Intent", "craft")
318
333
  headers.set("X-Agent-Purpose", "conversation")
319
334
  headers.set("X-IDE-Name", "WorkBuddy")
320
- headers.set("X-IDE-Version", UA_VERSION)
335
+ headers.set("X-IDE-Version", site.app)
321
336
  for (const [k, v] of Object.entries(enterpriseHeaders(a))) headers.set(k, v)
322
- if (!site.ai) return
337
+ }
338
+
339
+ // cliAgent is the User-Agent WorkBuddy's desktop CLI sends its API calls
340
+ // (a chat, the product config) with: UserAgentHttpInterceptor's
341
+ // buildUserAgent puts the platform (CLIENT_INFO_PLATFORM "WorkBuddy", at
342
+ // the app's version) first, then the CLI's applicationName at the same
343
+ // version, then CLIENT_INFO_USER_AGENT_EXTENSION, "CLI/<its version>".
344
+ function cliAgent(site) {
345
+ return `WorkBuddy/${site.app} ${site.appName}/${site.app} CLI/${site.cli}`
346
+ }
347
+
348
+ // OPENAI_SDK and NODE are what the CLI's chats go out through: the OpenAI
349
+ // SDK it bundles (openai 6.25.0) and the Node its Electron runs it on
350
+ // (Electron 37.10.3, ELECTRON_RUN_AS_NODE).
351
+ const OPENAI_SDK = "6.25.0"
352
+ const NODE = "v22.21.1"
353
+ const SDK_OS = { darwin: "MacOS", win32: "Windows", linux: "Linux", freebsd: "FreeBSD", openbsd: "OpenBSD", android: "Android" }
354
+ const SDK_ARCH = { x64: "x64", arm64: "arm64", arm: "arm", ia32: "x32", x32: "x32" }
355
+
356
+ // chatHeaders sets on a chat what WorkBuddy's CLI adds to its model
357
+ // requests alone (axiosToFetchAdapter, both builds): the agent type
358
+ // (resolveModelCallAgentType, "main" for the conversation's own agent),
359
+ // the runtime where a build sends one, and the OpenAI SDK's own headers,
360
+ // which the CLI's fetch passes on. Its client has a 1200s timeout and
361
+ // no retries of its own.
362
+ function chatHeaders(site, headers) {
323
363
  headers.set("X-Agent-Type", "main")
364
+ if (site.runtime) headers.set("X-Agent-Runtime", site.runtime)
365
+ else headers.delete("X-Agent-Runtime")
366
+ headers.set("Accept", "application/json")
367
+ headers.set("X-Stainless-Retry-Count", "0")
368
+ headers.set("X-Stainless-Timeout", "1200")
369
+ headers.set("X-Stainless-Lang", "js")
370
+ headers.set("X-Stainless-Package-Version", OPENAI_SDK)
371
+ headers.set("X-Stainless-OS", SDK_OS[process.platform] ?? (process.platform ? `Other:${process.platform}` : "Unknown"))
372
+ headers.set("X-Stainless-Arch", SDK_ARCH[process.arch] ?? (process.arch ? `other:${process.arch}` : "unknown"))
373
+ headers.set("X-Stainless-Runtime", "node")
374
+ headers.set("X-Stainless-Runtime-Version", NODE)
324
375
  }
325
376
 
326
377
  // keyAccount is the account an API key (OpenCode's {type: "api", key}) is
@@ -705,6 +756,36 @@ function unflag(messages) {
705
756
  // OpenAI-compatible client normally uses max_completion_tokens and may call a
706
757
  // reasoning prompt developer; WorkBuddy's endpoint takes max_tokens and
707
758
  // system instead.
759
+ // An earlier turn's thinking goes back as `reasoning`, the field WorkBuddy's
760
+ // own client (CodeBuddy's agents converter) sets on an assistant message.
761
+ // WorkBuddy drops `reasoning_content`, which Grok Build, DeepSeek-style
762
+ // clients and magpie's own replies use: measured 2026-10-27 on
763
+ // www.workbuddy.ai, a codeword in reasoning_content reached none of
764
+ // kimi-k2.6, kimi-k3, glm-5.3, deepseek-v4.1-flash and minimax-m3, while
765
+ // the same words as `reasoning` reached kimi-k2.6 and minimax-m3.
766
+ // OpenRouter-style reasoning_details are read for their text the same way.
767
+ function reasoningOf(m) {
768
+ if (typeof m.reasoning_content === "string" && m.reasoning_content) return m.reasoning_content
769
+ if (Array.isArray(m.reasoning_details)) {
770
+ const text = m.reasoning_details.map((d) => (typeof d?.text === "string" ? d.text : typeof d?.summary === "string" ? d.summary : "")).join("")
771
+ if (text) return text
772
+ }
773
+ return ""
774
+ }
775
+
776
+ function asReasoning(messages) {
777
+ let changed = false
778
+ for (const m of messages) {
779
+ if (m?.role !== "assistant" || (m.reasoning_content === undefined && m.reasoning_details === undefined)) continue
780
+ const text = reasoningOf(m)
781
+ if (text && !(typeof m.reasoning === "string" && m.reasoning)) m.reasoning = text
782
+ delete m.reasoning_content
783
+ delete m.reasoning_details
784
+ changed = true
785
+ }
786
+ return changed
787
+ }
788
+
708
789
  function withSystem(body) {
709
790
  // a body magpie or OpenCode hands over as bytes is read as the text it is
710
791
  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) body = new TextDecoder().decode(body)
@@ -723,6 +804,7 @@ function withSystem(body) {
723
804
  m.role = "system"
724
805
  changed = true
725
806
  }
807
+ if (asReasoning(b.messages)) changed = true
726
808
  if (b.messages[0]?.role !== "system") {
727
809
  b.messages.unshift({ role: "system", content: "You are a helpful assistant." })
728
810
  changed = true
@@ -1428,7 +1510,7 @@ const statusText = (n) => STATUS_TEXT[n] || `HTTP ${n}`
1428
1510
  function meterAsked(site, a, path, body = {}, signal = AbortSignal.timeout(20000)) {
1429
1511
  return fetch(site.endpoint + path, {
1430
1512
  method: "POST",
1431
- headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "WorkBuddy/" + UA_VERSION,
1513
+ headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "WorkBuddy/" + site.app,
1432
1514
  Authorization: "Bearer " + a.access, "X-User-Id": a.uid ?? "", "X-Domain": domainOf(site, a), "X-Product": "SaaS",
1433
1515
  "X-IDE-Type": "WorkBuddy", ...enterpriseHeaders(a) },
1434
1516
  body: JSON.stringify(body),
@@ -1961,7 +2043,7 @@ function effortsOf(d, site, id) {
1961
2043
  async function liveModels(site, a) {
1962
2044
  const headers = new Headers()
1963
2045
  sign(site, a, headers)
1964
- headers.set("User-Agent", `CLI/${APP_VERSION} WorkBuddy/${UA_VERSION}`)
2046
+ headers.set("User-Agent", cliAgent(site))
1965
2047
  headers.set("X-Requested-With", "XMLHttpRequest")
1966
2048
  headers.set("Accept", "application/json")
1967
2049
  const res = await fetch(site.endpoint + "/v3/config", { headers, signal: AbortSignal.timeout(15000) })
@@ -2211,6 +2293,7 @@ function makePlugin(site) {
2211
2293
  } else {
2212
2294
  body = withoutUnsigned(body)
2213
2295
  }
2296
+ chatHeaders(site, headers)
2214
2297
  attend(headers, body, session)
2215
2298
  const post = (b) =>
2216
2299
  fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: b }).catch((e) => unanswered(site, a, b, e, init?.signal))
@@ -2492,4 +2575,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
2492
2575
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
2493
2576
 
2494
2577
  // for tests
2495
- 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, 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 }
2578
+ 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 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-workbuddy-auth",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
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",