@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.
- package/README.md +14 -0
- package/index.mjs +143 -7
- 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,
|
|
491
|
-
//
|
|
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
|
-
|
|
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.
|
|
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",
|