@magpie-community/opencode-workbuddy-auth 0.2.11 → 0.2.12

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 +9 -3
  2. package/index.mjs +64 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -361,7 +361,7 @@ credits are read from the enterprise meter
361
361
  page reads: the cycle's credits used of its limit, and when the cycle
362
362
  resets (magpie #1904).
363
363
 
364
- ## The free model's daily window (WorkBuddy AI)
364
+ ## The free model's daily window (both builds)
365
365
 
366
366
  The credits above are not the only limit. `deepseek-v4.1-flash` is served at
367
367
  `x0.00` and costs the plan nothing, yet WorkBuddy AI refuses it long before
@@ -431,8 +431,14 @@ What is measured, and what is easy to read wrongly:
431
431
  rather than +08:00 guessed.
432
432
 
433
433
  A window that has lapsed reports nothing rather than 0%: nothing is
434
- exhausted, and the next request opens a fresh one. The China build is not
435
- covered; its limit has not been measured.
434
+ exhausted, and the next request opens a fresh one. The China build
435
+ (`copilot.tencent.com`) carries the same window: two Free-plan accounts
436
+ refused on 2026-11-02 at 200,389,043 and 200,485,923 tokens, so its cap is
437
+ measured at about 2e8 — and its refusals name the reset in Chinese
438
+ (`将在 … UTC+8 重置`), which reads as an edge the same way. Its card stops
439
+ at a **soft cap of 1.9e8**, 5% short of the measured one, so routing rests
440
+ the model before the vendor refuses it; the headroom absorbs the account's
441
+ spending on other machines, which this ledger cannot see.
436
442
 
437
443
  ## When free models work but a credit model does not
438
444
 
package/index.mjs CHANGED
@@ -2210,8 +2210,9 @@ function capacity(v) {
2210
2210
  // compact is a count as magpie says one: whole, else to two places.
2211
2211
  const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/0+$/, "").replace(/\.$/, ""))
2212
2212
 
2213
- // The window WorkBuddy AI rate limits its free models by, which its credit
2214
- // meter does not show.
2213
+ // The window WorkBuddy rate limits deepseek-v4.1-flash by, which its credit
2214
+ // meter does not show: WorkBuddy AI measured at about 1e8 over ten days, the
2215
+ // China build at about 2e8 on 2026-11-02 (see FREE_WINDOW_CN).
2215
2216
  //
2216
2217
  // A free account's usage() reports only the credits window, so a plan with
2217
2218
  // 160 credits untouched reads 0% while chats to deepseek-v4.1-flash are
@@ -2223,6 +2224,11 @@ const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/
2223
2224
  // reset at 2026-10-20 19:47:41 UTC+8, alternatively, you can switch to
2224
2225
  // the other models to continue using it.
2225
2226
  //
2227
+ // The China build (copilot.tencent.com) says the same in its own words:
2228
+ //
2229
+ // 429 WorkBuddy: 您的使用量已超出频率限制,将在 2026-11-02 19:17:02
2230
+ // UTC+8 重置,您也可以切换其他模型继续使用。
2231
+ //
2226
2232
  // Measured on a free plan over ten days (one account, deepseek-v4.1-flash
2227
2233
  // only), counting in + cache_read + out:
2228
2234
  //
@@ -2244,6 +2250,12 @@ const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/
2244
2250
  // were seen between a reset and the next request, each opening the new
2245
2251
  // window.
2246
2252
  //
2253
+ // The China build was measured after that, on 2026-11-02: two Free-plan
2254
+ // accounts on copilot.tencent.com, both spending the machine's ledger only
2255
+ // on this model, were refused at 200,389,043 and 200,485,923 tokens counted
2256
+ // the same way. Its cap sits at about 2e8 — and its refusals name the reset
2257
+ // in Chinese (将在 … UTC+8 重置), which windowReset reads as well.
2258
+ //
2247
2259
  // deepseek-v4.1-flash-sg is the same model under its paid id (the product
2248
2260
  // config's ModelRateLimitCap.lines[0] names it as paidId to this freeId,
2249
2261
  // allowPaidSwitch: true) and draws on the same window: it was refused in
@@ -2270,7 +2282,18 @@ const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/
2270
2282
  // ~/.config/magpie otherwise. No endpoint reports the window; a ledger that
2271
2283
  // cannot be read yields no window rather than a made-up one.
2272
2284
 
2273
- const FREE_WINDOW_CAP = 1e8
2285
+ // A window: the build's ledger rows it counts (provider), the cap it reports
2286
+ // against, and how deep its display rounds. Two builds, two windows, two
2287
+ // caps: the same phone number can be signed in to both, and their ledgers
2288
+ // stay apart — one build's spending must not fill the other's window.
2289
+ //
2290
+ // The China cap is a soft one. Measured at about 2e8 (two accounts refusing
2291
+ // at 200.39M and 200.49M), the card stops the model at 1.9e8: 5% of headroom
2292
+ // for the account's spending on other machines (which this ledger cannot
2293
+ // see), so routing rests the model before the vendor's refusal does. A
2294
+ // refusal in force still reads the window full whatever the sum says.
2295
+ const FREE_WINDOW_CN = { provider: "workbuddy", cap: 1.9e8, short: "190M" }
2296
+ const FREE_WINDOW_AI = { provider: "workbuddy-ai", cap: 1e8, short: "100M" }
2274
2297
  const FREE_WINDOW_SPAN = 24 * 3600 * 1000
2275
2298
  // The lead: how far back of an edge the read goes on. A chain read from a row
2276
2299
  // that opens a window needs the window the span makes of the rows above it,
@@ -2287,8 +2310,6 @@ const FREE_WINDOW_GAP = FREE_WINDOW_SPAN
2287
2310
  const FREE_WINDOW_MODEL = "deepseek-v4.1-flash"
2288
2311
  // both ids, so magpie's exact match finds the -sg one too
2289
2312
  const FREE_WINDOW_MODELS = ["deepseek-v4.1-flash", "deepseek-v4.1-flash-sg"]
2290
- // the provider whose rows count
2291
- const FREE_WINDOW_PROVIDER = "workbuddy-ai"
2292
2313
 
2293
2314
  // windowOffset is the UTC offset a refusal spells out, in minutes, from
2294
2315
  // "UTC+8", "UTC+08:00", "UTC-5" or "GMT+8". null when it spells out none,
@@ -2304,10 +2325,15 @@ function windowOffset(text) {
2304
2325
  }
2305
2326
 
2306
2327
  // windowReset is the instant a refusal names, or NaN when it names none or
2307
- // names it in an offset that cannot be read.
2328
+ // names it in an offset that cannot be read. Both builds name it — WorkBuddy
2329
+ // AI in its English "will reset at …", the China build in its Chinese
2330
+ // "将在 … 重置" — with the time in the same shape, so the two patterns share
2331
+ // the group numbering and the offset below reads the "UTC+8" either way.
2308
2332
  function windowReset(text) {
2309
2333
  const s = String(text ?? "")
2310
- const at = /reset at (\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(s)
2334
+ const at =
2335
+ /reset at (\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(s) ||
2336
+ /将在\s*(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(s)
2311
2337
  if (!at) return NaN
2312
2338
  const off = windowOffset(s)
2313
2339
  if (off === null) return NaN
@@ -2316,17 +2342,18 @@ function windowReset(text) {
2316
2342
 
2317
2343
  // freeWindowOf is the window account is in now, from rows (magpie's usage
2318
2344
  // records), or null when it has lapsed — the next request opens a new one.
2319
- // now is passed in so the reading is testable.
2345
+ // now is passed in so the reading is testable; win is the build's window
2346
+ // (which build's rows count, what cap they fill), AI by default.
2320
2347
  //
2321
2348
  // The window is a chain: it begins at the first request answered after the
2322
2349
  // previous one ended, so where this window began depends on every window
2323
2350
  // before it. A window ends at a refusal's reset when the ledger holds one,
2324
2351
  // and otherwise a day after it began. readLedgerTail (which see) stops on
2325
2352
  // both, so that these rows read the same as the whole ledger.
2326
- function freeWindowOf(rows, account, now = Date.now()) {
2353
+ function freeWindowOf(rows, account, now = Date.now(), win = FREE_WINDOW_AI) {
2327
2354
  const mine = (Array.isArray(rows) ? rows : []).filter(
2328
2355
  (r) =>
2329
- r?.provider === FREE_WINDOW_PROVIDER &&
2356
+ r?.provider === win.provider &&
2330
2357
  r.providerAccount === account &&
2331
2358
  typeof r.model === "string" &&
2332
2359
  r.model.startsWith(FREE_WINDOW_MODEL) &&
@@ -2403,11 +2430,11 @@ function freeWindowOf(rows, account, now = Date.now()) {
2403
2430
 
2404
2431
  const card = (amount, resetsAt, refused) => ({
2405
2432
  name: "Free model tokens",
2406
- used: Math.min(100, (100 * amount) / FREE_WINDOW_CAP),
2433
+ used: Math.min(100, (100 * amount) / win.cap),
2407
2434
  amount,
2408
- limit: FREE_WINDOW_CAP,
2435
+ limit: win.cap,
2409
2436
  unit: "tokens",
2410
- display: `${tokensShort(amount)} / 100M`,
2437
+ display: `${tokensShort(amount)} / ${win.short}`,
2411
2438
  resetsAt: new Date(resetsAt).toISOString(),
2412
2439
  span: FREE_WINDOW_SPAN / 1000,
2413
2440
  models: FREE_WINDOW_MODELS.slice(),
@@ -2425,7 +2452,7 @@ function freeWindowOf(rows, account, now = Date.now()) {
2425
2452
  if (!Number.isFinite(t) || t <= now || t <= start) continue
2426
2453
  if (live === null || t > live) live = t
2427
2454
  }
2428
- if (live !== null) return card(FREE_WINDOW_CAP, live, true)
2455
+ if (live !== null) return card(win.cap, live, true)
2429
2456
 
2430
2457
  return card(used, end, false)
2431
2458
  }
@@ -2493,7 +2520,7 @@ function freeWindowLedger(cwd = process.cwd(), env = process.env, home = homedir
2493
2520
  // ledger mid-write, the one row holding the free window's start was
2494
2521
  // dropped without a word, and the panel showed no window at all until the
2495
2522
  // next read happened to succeed.
2496
- function freeWindowRows(account, now = Date.now(), cwd = process.cwd(), env = process.env, home = homedir()) {
2523
+ function freeWindowRows(account, now = Date.now(), cwd = process.cwd(), env = process.env, home = homedir(), win = FREE_WINDOW_AI) {
2497
2524
  if (!account) return null
2498
2525
  let found = null
2499
2526
  for (const path of freeWindowLedger(cwd, env, home)) {
@@ -2510,11 +2537,14 @@ function freeWindowRows(account, now = Date.now(), cwd = process.cwd(), env = pr
2510
2537
  // way this ledger is written; one rewritten in place (a rotation or a
2511
2538
  // compaction) moves mtime even when its size lands the same.
2512
2539
  const file = path + "|" + st.mtimeMs + ":" + st.size
2513
- const key = file + "|" + account
2540
+ // the key carries the account and the build: one account's rows are its
2541
+ // own, and the two builds' chains over one ledger must not answer for
2542
+ // each other (the same phone number can be signed in to both)
2543
+ const key = file + "|" + win.provider + "|" + account
2514
2544
  if (ledgerParsed?.key === key) return ledgerParsed.rows
2515
2545
  let got
2516
2546
  try {
2517
- got = readLedgerTail(path, st, now, undefined, account)
2547
+ got = readLedgerTail(path, st, now, undefined, account, win)
2518
2548
  } catch {
2519
2549
  return null
2520
2550
  }
@@ -2574,11 +2604,12 @@ function rowAccount(line) {
2574
2604
  }
2575
2605
 
2576
2606
  // ofAccount is whether a row belongs to the chain read for account: this
2577
- // account's own row of the free model, either id. A refusal of another
2578
- // account's, or of a paid model, says nothing about where this chain opens.
2579
- function ofAccount(row, account) {
2607
+ // account's own row of the build's free model, either id. A refusal of
2608
+ // another account's, of the other build, or of a paid model says nothing
2609
+ // about where this chain opens.
2610
+ function ofAccount(row, account, win = FREE_WINDOW_AI) {
2580
2611
  return (
2581
- row?.provider === FREE_WINDOW_PROVIDER &&
2612
+ row?.provider === win.provider &&
2582
2613
  row.providerAccount === account &&
2583
2614
  typeof row.model === "string" &&
2584
2615
  row.model.startsWith(FREE_WINDOW_MODEL)
@@ -2625,7 +2656,7 @@ function ofAccount(row, account) {
2625
2656
  // plugin_accounts.go). The label then no longer equals the accountId rows are
2626
2657
  // written under, no row is found for this account at all, and the card
2627
2658
  // reports no free window.
2628
- function readLedgerTail(path, st, now, chunks, account) {
2659
+ function readLedgerTail(path, st, now, chunks, account, win = FREE_WINDOW_AI) {
2629
2660
  const CHUNK = 1 << 16
2630
2661
  const lapsed = now - FREE_WINDOW_SPAN // a read past this holds no window now
2631
2662
  const fd = openSync(path, "r")
@@ -2684,7 +2715,7 @@ function readLedgerTail(path, st, now, chunks, account) {
2684
2715
  if (!(firstPass && tornEnd && i === lines.length - 1)) bad++
2685
2716
  continue
2686
2717
  }
2687
- if (!ofAccount(row, account)) continue
2718
+ if (!ofAccount(row, account, win)) continue
2688
2719
  rev.push(row)
2689
2720
  chain.push(row)
2690
2721
  if (Number(row.status) === 200) answered = true
@@ -2699,7 +2730,7 @@ function readLedgerTail(path, st, now, chunks, account) {
2699
2730
  if (!reach || !Number.isFinite(t) || t >= reach) {
2700
2731
  try {
2701
2732
  const row = JSON.parse(carry)
2702
- if (ofAccount(row, account)) rev.push(row)
2733
+ if (ofAccount(row, account, win)) rev.push(row)
2703
2734
  } catch {
2704
2735
  bad++
2705
2736
  }
@@ -3712,12 +3743,15 @@ function makePlugin(site) {
3712
3743
  await spareFree(site, a, out.windows)
3713
3744
  // the free model's own window, which the credit meter above does
3714
3745
  // not show: what actually refuses a chat long before 160 credits
3715
- // are spent. WorkBuddy AI alone; the China build has no such cap
3716
- // measured (see freeWindowOf)
3717
- if (site.ai) {
3746
+ // are spent. Both builds: WorkBuddy AI at 1e8 (ten days of
3747
+ // refusals), the China build's refusals measured at about 2e8 on
3748
+ // 2026-11-02, its card stopping at the 1.9e8 soft cap (see the
3749
+ // window definitions above)
3750
+ {
3718
3751
  const account = a.accountId || a.user || a.uid || ""
3719
- const rows = freeWindowRows(account)
3720
- const w = rows && freeWindowOf(rows, account)
3752
+ const win = site.ai ? FREE_WINDOW_AI : FREE_WINDOW_CN
3753
+ const rows = freeWindowRows(account, Date.now(), process.cwd(), process.env, homedir(), win)
3754
+ const w = rows && freeWindowOf(rows, account, Date.now(), win)
3721
3755
  if (w) out.windows.push(w)
3722
3756
  }
3723
3757
  return { ...out, signIn: said }
@@ -3862,4 +3896,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
3862
3896
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
3863
3897
 
3864
3898
  // for tests
3865
- export const _internal = { planCodes, idFor, OFF_TAKEN, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unsaid, FILTER_SAYS, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, REFUSED_DOC, commaPaused, commaPausedOn, opened, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
3899
+ export const _internal = { planCodes, idFor, OFF_TAKEN, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unsaid, FILTER_SAYS, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, REFUSED_DOC, commaPaused, commaPausedOn, opened, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_CN, FREE_WINDOW_AI, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, 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.2.11",
3
+ "version": "0.2.12",
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",