@magpie-community/opencode-workbuddy-auth 0.1.13 → 0.1.15

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 +14 -0
  2. package/index.mjs +143 -7
  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:
@@ -119,6 +124,15 @@ is used.
119
124
 
120
125
  A model's reasoning levels are its variants (`reasoningEffort`).
121
126
 
127
+ ## Credits in magpie
128
+
129
+ magpie's usage card shows the account's credits from the resource summary.
130
+ It also says when the soonest credits left run out (`runsOutAt`, and how
131
+ many: `runsOut`), from the paid and free package lists WorkBuddy's own
132
+ client reads. The daily Free Plan slice isn't counted: it ends at the same
133
+ midnight on every account. magpie's smart routing spends the account whose
134
+ credits run out first.
135
+
122
136
  ## Not here
123
137
 
124
138
  magpie's built-in WorkBuddy does more than this package:
package/index.mjs CHANGED
@@ -487,8 +487,11 @@ function unflag(messages) {
487
487
  }
488
488
 
489
489
  // 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.
490
+ // WorkBuddy's default one, as its app does, takes FLAGGED's words out of it,
491
+ // and applies the request spellings WorkBuddy's chat endpoint accepts. The
492
+ // OpenAI-compatible client normally uses max_completion_tokens and may call a
493
+ // reasoning prompt developer; WorkBuddy's endpoint takes max_tokens and
494
+ // system instead.
492
495
  function withSystem(body) {
493
496
  // a body magpie or OpenCode hands over as bytes is read as the text it is
494
497
  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) body = new TextDecoder().decode(body)
@@ -497,6 +500,16 @@ function withSystem(body) {
497
500
  const b = JSON.parse(body)
498
501
  if (!Array.isArray(b?.messages) || !b.messages.length) return body
499
502
  let changed = unflag(b.messages)
503
+ if (b.max_completion_tokens !== undefined) {
504
+ if (b.max_tokens === undefined) b.max_tokens = b.max_completion_tokens
505
+ delete b.max_completion_tokens
506
+ changed = true
507
+ }
508
+ for (const m of b.messages) {
509
+ if (m?.role !== "developer") continue
510
+ m.role = "system"
511
+ changed = true
512
+ }
500
513
  if (b.messages[0]?.role !== "system") {
501
514
  b.messages.unshift({ role: "system", content: "You are a helpful assistant." })
502
515
  changed = true
@@ -507,6 +520,49 @@ function withSystem(body) {
507
520
  }
508
521
  }
509
522
 
523
+ // normalizeToolChoice converts the object form emitted by OpenAI clients to
524
+ // WorkBuddy's string form. It runs after fitted(), so a named choice follows a
525
+ // tool name that was made safe for WorkBuddy.
526
+ function normalizeToolChoice(body) {
527
+ if (typeof body !== "string") return body
528
+ try {
529
+ const b = JSON.parse(body)
530
+ if (!b || typeof b !== "object" || Array.isArray(b) || !("tool_choice" in b)) return body
531
+ const choice = b.tool_choice
532
+ if (typeof choice === "string") {
533
+ if (choice.trim().toLowerCase() === "none") {
534
+ delete b.tool_choice
535
+ delete b.tools
536
+ delete b.functions
537
+ }
538
+ return JSON.stringify(b)
539
+ }
540
+ if (!choice || typeof choice !== "object" || Array.isArray(choice)) {
541
+ delete b.tool_choice
542
+ return JSON.stringify(b)
543
+ }
544
+ const type = typeof choice.type === "string" ? choice.type.trim().toLowerCase() : ""
545
+ if (type === "none") {
546
+ delete b.tool_choice
547
+ delete b.tools
548
+ delete b.functions
549
+ } else if (type === "auto" || type === "required") {
550
+ b.tool_choice = type
551
+ } else if (type === "function") {
552
+ const fn = choice.function && typeof choice.function === "object" && !Array.isArray(choice.function)
553
+ ? choice.function
554
+ : undefined
555
+ const name = typeof fn?.name === "string" ? fn.name.trim() : ""
556
+ b.tool_choice = name || "auto"
557
+ } else {
558
+ delete b.tool_choice
559
+ }
560
+ return JSON.stringify(b)
561
+ } catch {
562
+ return body
563
+ }
564
+ }
565
+
510
566
  // ---- tools WorkBuddy takes ----------------------------------------------------
511
567
 
512
568
  // WorkBuddy AI answers 400 code 11133 "Invalid request parameters" (the
@@ -917,13 +973,13 @@ const statusText = (n) => STATUS_TEXT[n] || `HTTP ${n}`
917
973
 
918
974
  // meter asks the billing API as magpie's built-in did, its errors said
919
975
  // the same: WorkBuddy's message, else its code, else the HTTP status.
920
- async function meter(site, a, path) {
976
+ async function meter(site, a, path, body = {}) {
921
977
  const res = await fetch(site.endpoint + path, {
922
978
  method: "POST",
923
979
  headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "WorkBuddy/" + UA_VERSION,
924
980
  Authorization: "Bearer " + a.access, "X-User-Id": a.uid ?? "", "X-Domain": domainOf(site, a), "X-Product": "SaaS",
925
981
  "X-IDE-Type": "WorkBuddy", ...enterpriseHeaders(a) },
926
- body: "{}",
982
+ body: JSON.stringify(body),
927
983
  signal: AbortSignal.timeout(20000),
928
984
  })
929
985
  let env = {}
@@ -969,6 +1025,76 @@ function usageOf(sum, plan) {
969
1025
  return out
970
1026
  }
971
1027
 
1028
+ // The packages' codes, as WorkBuddy's own client lists them when it asks
1029
+ // for each package's credits (its COMMODITY_CODES): the API refuses a
1030
+ // detail list without them (10001 "PackageCodes required").
1031
+ const CODE = (n, s) => `TCACA_code_${n}_${s}`
1032
+ const PAID_CODES = [CODE("002", "AkiJS3ZHF5"), CODE("005", "maRGyrHhw1"), CODE("003", "FAnt7lcmRT"), CODE("023", "4xbGhMrE6q"),
1033
+ CODE("026", "BaESVICNoi"), CODE("027", "0FCGVA6vSa"), CODE("009", "0XmEQc2xOf"), CODE("038", "OhvqZtiPKr"), CODE("036", "lupO5WgNdG")]
1034
+ const DAILY_CODE = CODE("001", "PqouKr6QWV")
1035
+ const FREE_CODES = [DAILY_CODE, CODE("008", "cfWoLwvjU4"), CODE("035", "ArVxJcGDsm"), CODE("006", "DbXS0lrypC"), CODE("039", "KRcQj7wUat"),
1036
+ CODE("040", "mi9rCYg46x"), CODE("007", "nzdH5h4Nl0"), CODE("028", "NtpWi0jzXs"), CODE("037", "WxOD3MpI2o"), CODE("029", "6wCGEWquYy"),
1037
+ CODE("030", "BjSt89qTvr")]
1038
+ // a slice package (CapacityType 4) is refreshed each day: its credits are
1039
+ // lost at the same midnight on every account, so they tell none apart
1040
+ const SLICE = "4"
1041
+
1042
+ // beijing is WorkBuddy's own time ("2026-10-13 00:39:27" is UTC+8, as its
1043
+ // DeductionEndTime for the same package says).
1044
+ const beijing = (ms) => new Date(ms + 8 * 3600_000).toISOString().slice(0, 19).replace("T", " ")
1045
+
1046
+ // packagesOf is every package of the account with credits, paid and free,
1047
+ // as WorkBuddy's client asks for them: active and used-up (Status 0, 3),
1048
+ // up to 10 pages of 200.
1049
+ async function packagesOf(site, a, now = Date.now()) {
1050
+ const day = beijing(now).slice(0, 10)
1051
+ const lists = [
1052
+ ["/billing/meter/get-user-resource-paid-packages", { PackageCodes: PAID_CODES, NeedRenewInfo: true }],
1053
+ ["/billing/meter/get-user-resource-free-packages", { PackageCodes: FREE_CODES, SlicePeriodStartTime: day + " 00:00:00", SlicePeriodEndTime: day + " 23:59:59" }],
1054
+ ]
1055
+ const out = []
1056
+ for (const [path, extra] of lists) {
1057
+ for (let page = 1; page <= 10; page++) {
1058
+ const d = await meter(site, a, path, { PageNumber: page, PageSize: 200, Status: [0, 3], ...extra })
1059
+ const got = d?.Accounts ?? []
1060
+ out.push(...got)
1061
+ if (got.length < 200 || out.length >= (d?.TotalCount ?? 0)) break
1062
+ }
1063
+ }
1064
+ return out
1065
+ }
1066
+
1067
+ // endOf is when a package's credits are lost: its DeductionEndTime, else
1068
+ // ExpiredTime, else the cycle's end (UTC+8), as WorkBuddy's client reads it.
1069
+ function endOf(p) {
1070
+ const ms = Number(p?.DeductionEndTime)
1071
+ if (ms > 0) return ms
1072
+ for (const v of [p?.ExpiredTime, p?.CycleEndTime]) {
1073
+ if (typeof v !== "string" || !v.trim()) continue
1074
+ const t = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(v.trim()) ? Date.parse(v.trim().replace(" ", "T") + "+08:00") : Date.parse(v)
1075
+ if (t > 0) return t
1076
+ }
1077
+ return 0
1078
+ }
1079
+
1080
+ // runsOutOf is the soonest moment credits left in one of the account's
1081
+ // packages are lost, and how many (cdredfox on X): smart routing spends
1082
+ // the account whose credits expire first. Only packages the summary
1083
+ // counts, with credits left, and not the daily slice; null when none.
1084
+ function runsOutOf(sum, packages, now = Date.now()) {
1085
+ const counted = new Set((sum?.Packages ?? []).map((p) => p?.PackageCode))
1086
+ let at = 0, amount = 0
1087
+ for (const p of packages ?? []) {
1088
+ if (!counted.has(p?.PackageCode) || p?.PackageCode === DAILY_CODE || String(p?.CapacityType) === SLICE) continue
1089
+ const left = capacity(p?.CycleCapacityRemainPrecise ?? p?.CycleCapacityRemain)
1090
+ const end = endOf(p)
1091
+ if (!(left > 0) || end <= now) continue
1092
+ if (!at || end < at) at = end, amount = left
1093
+ else if (end === at) amount += left
1094
+ }
1095
+ return at ? { runsOutAt: new Date(at).toISOString(), runsOut: amount } : null
1096
+ }
1097
+
972
1098
  // ---- models -----------------------------------------------------------------
973
1099
 
974
1100
  // variants are the reasoning levels as OpenCode gives them to the AI SDK.
@@ -1150,7 +1276,7 @@ function makePlugin(site) {
1150
1276
  if (body === undefined && req) body = await req.clone().text()
1151
1277
  body = withSystem(body)
1152
1278
  const fit = fitted(body)
1153
- body = fit.body
1279
+ body = normalizeToolChoice(fit.body)
1154
1280
  attend(headers, body, session)
1155
1281
  const res = await fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body })
1156
1282
  return res.status >= 400 ? explained(res) : kept(await renamed(res, fit.names))
@@ -1166,7 +1292,17 @@ function makePlugin(site) {
1166
1292
  try {
1167
1293
  const a = await current(site, client, auth)
1168
1294
  // the meter has no /v2 prefix, on either site
1169
- return { ...usageOf(await meter(site, a, "/billing/meter/get-user-resource-summary"), auth.plan), signIn: "kept" }
1295
+ const sum = await meter(site, a, "/billing/meter/get-user-resource-summary")
1296
+ const out = usageOf(sum, auth.plan)
1297
+ // when the credits left run out, for routing to spend them first;
1298
+ // a list that can't be read leaves it unknown, the card stands
1299
+ const credits = out.windows.find((w) => w.name === "Credits")
1300
+ if (credits) {
1301
+ try {
1302
+ Object.assign(credits, runsOutOf(sum, await packagesOf(site, a)) ?? {})
1303
+ } catch {}
1304
+ }
1305
+ return { ...out, signIn: "kept" }
1170
1306
  } catch (e) {
1171
1307
  return { error: e?.message ?? String(e), signIn: "kept" }
1172
1308
  }
@@ -1258,4 +1394,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
1258
1394
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
1259
1395
 
1260
1396
  // for tests
1261
- export const _internal = { 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 }
1397
+ export const _internal = { 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.13",
3
+ "version": "0.1.15",
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",