@magpie-community/opencode-workbuddy-auth 0.1.23 → 0.1.25
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 +35 -0
- package/index.mjs +59 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,6 +164,39 @@ credits are read from the enterprise meter
|
|
|
164
164
|
page reads: the cycle's credits used of its limit, and when the cycle
|
|
165
165
|
resets (magpie #1904).
|
|
166
166
|
|
|
167
|
+
## When free models work but a credit model does not
|
|
168
|
+
|
|
169
|
+
Check the failing account and model separately before concluding that the
|
|
170
|
+
wallet is empty:
|
|
171
|
+
|
|
172
|
+
- Use the exact API id from the account's model list. For example,
|
|
173
|
+
`glm-5.3-flash` is the id; `GLM-5.3-Flash` is its display name. A route
|
|
174
|
+
copied from another provider need not be a valid WorkBuddy model id.
|
|
175
|
+
- The WorkBuddy 5.6+ encrypted-desktop message means this sign-in cannot
|
|
176
|
+
be read, not that the account has zero credits. Use **Account (browser)**
|
|
177
|
+
for the same build/region. Keep the old entry until the new identity and
|
|
178
|
+
model list are confirmed. The desktop file is not decrypted or changed.
|
|
179
|
+
- An account restricted by WorkBuddy is a different failure. Code 11140
|
|
180
|
+
with its unusual-activity message calls for **Submit feedback** in
|
|
181
|
+
WorkBuddy; signing in repeatedly or changing the model does not lift it.
|
|
182
|
+
See [magpie#1849](https://github.com/yetone/magpie/issues/1849).
|
|
183
|
+
- Inspect each attempted provider/account in magpie's routing details.
|
|
184
|
+
A ZCode trial quota failure, another provider's insufficient credits, or
|
|
185
|
+
one account's restriction is not the balance of every WorkBuddy account.
|
|
186
|
+
|
|
187
|
+
A model response confirms that the selected account can answer. It does
|
|
188
|
+
not, without a matching vendor usage record, prove how many credits that
|
|
189
|
+
request deducted. A usage-read error means the balance is unknown.
|
|
190
|
+
|
|
191
|
+
The encrypted-desktop result can be reproduced without an account using
|
|
192
|
+
`packages/workbuddy/usage.test.mjs` in an isolated temporary HOME (and
|
|
193
|
+
USERPROFILE on Windows). It covers sign-in, usage and chat against a
|
|
194
|
+
synthetic sealed desktop file, and checks that the file stays unchanged.
|
|
195
|
+
A successful browser authorization alone is not a migration: compare the
|
|
196
|
+
returned identity with the intended desktop account before replacing its
|
|
197
|
+
entry. An already signed-in browser can simply renew another saved account.
|
|
198
|
+
|
|
199
|
+
|
|
167
200
|
## Not here
|
|
168
201
|
|
|
169
202
|
magpie's built-in WorkBuddy does more than this package:
|
|
@@ -172,3 +205,5 @@ magpie's built-in WorkBuddy does more than this package:
|
|
|
172
205
|
- several accounts under one provider.
|
|
173
206
|
|
|
174
207
|
OpenCode's plugin API has no place for these.
|
|
208
|
+
|
|
209
|
+
Credit multipliers accept the international catalog's `x0.34 credits` text as well as `x0.34`. An empty credits value (for example Auto) stays unknown, never free.
|
package/index.mjs
CHANGED
|
@@ -1136,17 +1136,67 @@ const STATUS_TEXT = {
|
|
|
1136
1136
|
}
|
|
1137
1137
|
const statusText = (n) => STATUS_TEXT[n] || `HTTP ${n}`
|
|
1138
1138
|
|
|
1139
|
-
//
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
const res = await fetch(site.endpoint + path, {
|
|
1139
|
+
// meterAsked is the billing API's reply to path, asked with a's sign-in.
|
|
1140
|
+
function meterAsked(site, a, path, body = {}, signal = AbortSignal.timeout(20000)) {
|
|
1141
|
+
return fetch(site.endpoint + path, {
|
|
1143
1142
|
method: "POST",
|
|
1144
1143
|
headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "WorkBuddy/" + UA_VERSION,
|
|
1145
1144
|
Authorization: "Bearer " + a.access, "X-User-Id": a.uid ?? "", "X-Domain": domainOf(site, a), "X-Product": "SaaS",
|
|
1146
1145
|
"X-IDE-Type": "WorkBuddy", ...enterpriseHeaders(a) },
|
|
1147
1146
|
body: JSON.stringify(body),
|
|
1148
|
-
signal
|
|
1147
|
+
signal,
|
|
1149
1148
|
})
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// EDGE_READS is how much of a request WorkBuddy's front door (APISIX, on
|
|
1152
|
+
// both sites) reads before it answers a sign-in it refuses. A refused
|
|
1153
|
+
// request with a longer body gets no answer at all: the connection is
|
|
1154
|
+
// reset, and the chat fails as "ECONNRESET: The socket connection was
|
|
1155
|
+
// closed unexpectedly", which says nothing of the sign-in (wyp on Discord,
|
|
1156
|
+
// 2026-10-15: a Claude Code session past 2 MiB failed on every turn).
|
|
1157
|
+
// Measured on 2026-10-15 on www.workbuddy.ai and copilot.tencent.com: a
|
|
1158
|
+
// body of 2097152 bytes with a refused token got 401, one of 2097153 a
|
|
1159
|
+
// reset; with an accepted token 20 MB went through.
|
|
1160
|
+
const EDGE_READS = 2 * 1024 * 1024
|
|
1161
|
+
|
|
1162
|
+
const byteLength = (b) =>
|
|
1163
|
+
typeof b === "string" ? Buffer.byteLength(b) : b instanceof ArrayBuffer || ArrayBuffer.isView(b) ? b.byteLength : 0
|
|
1164
|
+
|
|
1165
|
+
// unanswered is what a chat whose fetch failed with err comes to: a chat
|
|
1166
|
+
// past EDGE_READS that the connection was reset on is asked about at the
|
|
1167
|
+
// billing API, which answers a refused sign-in with its 401 whatever the
|
|
1168
|
+
// size. A 401 there is the chat's own answer, said as one; anything else
|
|
1169
|
+
// leaves err as it was, as does a smaller chat, which the front door
|
|
1170
|
+
// answers itself. The chat isn't sent again.
|
|
1171
|
+
async function unanswered(site, a, body, err, signal) {
|
|
1172
|
+
if (signal?.aborted || err?.name === "AbortError" || err?.name === "TimeoutError") throw err
|
|
1173
|
+
const size = byteLength(body)
|
|
1174
|
+
if (size <= EDGE_READS) throw err
|
|
1175
|
+
let status = 0
|
|
1176
|
+
try {
|
|
1177
|
+
const res = await meterAsked(site, a, "/billing/meter/get-user-resource-summary", {}, AbortSignal.timeout(20000))
|
|
1178
|
+
status = res.status
|
|
1179
|
+
await res.body?.cancel()
|
|
1180
|
+
} catch {
|
|
1181
|
+
throw err
|
|
1182
|
+
}
|
|
1183
|
+
if (status !== 401) throw err
|
|
1184
|
+
const mb = (size / (1024 * 1024)).toFixed(1)
|
|
1185
|
+
const message =
|
|
1186
|
+
`${site.name} refused this account's sign-in (401). It closes the connection on a refused request over 2 MiB ` +
|
|
1187
|
+
`(this one was ${mb} MiB) instead of answering, which is why it read as ECONNRESET. ` +
|
|
1188
|
+
(a.source === "desktop" ? `Sign in to ${site.name} desktop again.` : `Sign in to ${site.name} again in magpie.`)
|
|
1189
|
+
return new Response(JSON.stringify({ error: { message, type: "authentication_error", code: 401 } }), {
|
|
1190
|
+
status: 401,
|
|
1191
|
+
statusText: "Unauthorized",
|
|
1192
|
+
headers: { "content-type": "application/json" },
|
|
1193
|
+
})
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// meter asks the billing API as magpie's built-in did, its errors said
|
|
1197
|
+
// the same: WorkBuddy's message, else its code, else the HTTP status.
|
|
1198
|
+
async function meter(site, a, path, body = {}) {
|
|
1199
|
+
const res = await meterAsked(site, a, path, body)
|
|
1150
1200
|
let env = {}
|
|
1151
1201
|
try {
|
|
1152
1202
|
env = JSON.parse(await res.text()) ?? {}
|
|
@@ -1353,7 +1403,7 @@ function modelOf(site, provider, m) {
|
|
|
1353
1403
|
// rated. The same rule as magpie's built-in (wbCredits).
|
|
1354
1404
|
function creditsOf(c) {
|
|
1355
1405
|
if (c == null || c === "") return undefined
|
|
1356
|
-
const s = String(c).trim().toLowerCase().replace(/^x/, "")
|
|
1406
|
+
const s = String(c).trim().toLowerCase().replace(/\s+credits?$/, "").replace(/^x/, "")
|
|
1357
1407
|
const n = Number(s)
|
|
1358
1408
|
return s !== "" && Number.isFinite(n) && n >= 0 ? n : undefined
|
|
1359
1409
|
}
|
|
@@ -1555,7 +1605,8 @@ function makePlugin(site) {
|
|
|
1555
1605
|
const fit = fitted(body)
|
|
1556
1606
|
body = normalizeToolChoice(fit.body)
|
|
1557
1607
|
attend(headers, body, session)
|
|
1558
|
-
const send = (b) =>
|
|
1608
|
+
const send = (b) =>
|
|
1609
|
+
fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: b }).catch((e) => unanswered(site, a, b, e, init?.signal))
|
|
1559
1610
|
// a model known to turn tool results then the user's message away
|
|
1560
1611
|
// gets the empty reply between them up front (see endsAfterTools)
|
|
1561
1612
|
const model = chatOf(body)?.model
|
|
@@ -1711,4 +1762,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
1711
1762
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
1712
1763
|
|
|
1713
1764
|
// for tests
|
|
1714
|
-
export const _internal = { 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 }
|
|
1765
|
+
export const _internal = { 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 }
|
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.25",
|
|
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",
|