dsh-opencode-routing 2.2.1 → 2.4.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-opencode-routing",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "description": "dsh plugin: OpenCode account pool with automatic A-to-B failover and quota routing, plus a per-account / per-model usage dashboard, in one Settings page.",
6
6
  "exports": {
@@ -16,6 +16,13 @@
16
16
  "client.js",
17
17
  "cordis.patch.yml",
18
18
  "README.md",
19
+ "README.zh-CN.md",
20
+ "README.zh-TW.md",
21
+ "README.ja.md",
22
+ "README.ko.md",
23
+ "README.ru.md",
24
+ "CONTRIBUTING.md",
25
+ "LICENSE",
19
26
  "test.mjs",
20
27
  "locale/*.json",
21
28
  "icon.svg"
@@ -42,6 +49,9 @@
42
49
  "multi-account",
43
50
  "quota",
44
51
  "router",
52
+ "routing",
53
+ "failover",
54
+ "i18n",
45
55
  "billing"
46
56
  ],
47
57
  "repository": {
package/test.mjs CHANGED
@@ -14,7 +14,7 @@ import assert from 'node:assert/strict'
14
14
 
15
15
  import * as accounts from './index.js'
16
16
  import { apply as applyRouter, isExhausted, maskKey, nextProbeAt, retryAfter } from './router.js'
17
- import { apply as applyUsage, isChinaHoliday, peakMultiplier, peakSnapshot } from './usage.js'
17
+ import { apply as applyUsage, isChinaHoliday, peakClock, peakMultiplier, peakSnapshot } from './usage.js'
18
18
 
19
19
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
20
20
 
@@ -70,6 +70,24 @@ assert.equal(isExhausted({ rolling: { status: 'ok', percent: 100 } }), true)
70
70
  assert.equal(holiday.models.every((row) => row.cheaper === 'go'), true)
71
71
  const plain = peakSnapshot(7.1, plainPeak, 'go')
72
72
  assert.ok(Math.abs(plain.models[0].ratio - 0.1) < 0.001, 'both peak: exactly the 10x discount')
73
+
74
+ // The page reports both calendars separately with a countdown to the flip.
75
+ const clockHoliday = peakClock(holidayPeak)
76
+ assert.equal(clockHoliday.holiday, true)
77
+ assert.equal(clockHoliday.official.peak, false, 'the official API is off-peak on a holiday')
78
+ assert.equal(clockHoliday.opencode.peak, true, 'OpenCode keeps billing the holiday as peak')
79
+ assert.equal(typeof clockHoliday.opencode.hours, 'number')
80
+ assert.ok(clockHoliday.opencode.hours > 0 && clockHoliday.opencode.hours <= 5, 'the window ends within 5 hours')
81
+
82
+ const clockPlain = peakClock(plainPeak)
83
+ assert.equal(clockPlain.official.peak, true)
84
+ assert.equal(clockPlain.opencode.peak, true)
85
+ assert.ok(clockPlain.opencode.hours >= 1.9 && clockPlain.opencode.hours <= 2.1, 'two hours left in the morning window')
86
+
87
+ const clockQuiet = peakClock(Date.parse('2026-02-24T12:00:00Z')) // Beijing 20:00
88
+ assert.equal(clockQuiet.official.peak, false)
89
+ assert.equal(clockQuiet.opencode.peak, false)
90
+ assert.ok(clockQuiet.opencode.hours > 0, 'a flip always lies ahead')
73
91
  }
74
92
  console.log('pure logic ok')
75
93
 
@@ -357,6 +375,23 @@ await sleep(300)
357
375
  assert.equal(freeRow.remaining, undefined, 'free models have no remaining quota')
358
376
  assert.equal(freeRow.inputs.includes('video'), true)
359
377
  assert.equal(data.peak.plan.leverage, 10)
378
+
379
+ // Every model breaks down into the three lines DeepSeek bills on, cheapest first
380
+ const peakFlash = data.peak.models.find((entry) => entry.model === 'deepseek-v4.1-flash')
381
+ assert.deepEqual(peakFlash.rows.map((line) => line.kind), ['cacheRead', 'input', 'output'])
382
+ assert.equal(peakFlash.rows[0].cheaper, 'go', 'the cached line is still cheaper on OpenCode')
383
+ assert.equal(Math.round(peakFlash.rows[0].ratio * 100) / 100, 0.1)
384
+ }
385
+
386
+ // A plan change is a config edit, not a code change: the discount is derived
387
+ {
388
+ const offPeak = Date.UTC(2026, 10, 3, 16, 0)
389
+ const cheaper = peakSnapshot(7.1, offPeak, 'go', { priceUsd: 10, allowanceUsd: 50 })
390
+ assert.equal(cheaper.plan.leverage, 5, 'a smaller allowance halves the discount')
391
+ assert.equal(cheaper.plan.overridden, true)
392
+ const dearer = peakSnapshot(7.1, offPeak, 'go', { priceUsd: 50, allowanceUsd: 100 })
393
+ assert.equal(dearer.plan.leverage, 2)
394
+ assert.equal(dearer.models[0].rows[0].cheaper, 'go', 'still cheaper, just less so')
360
395
  }
361
396
  console.log('usage view ok')
362
397
 
package/usage.js CHANGED
@@ -230,6 +230,11 @@ function normalize(config = {}) {
230
230
  // our own schedule, not DeepSeek's holiday calendar"), so this stays off by
231
231
  // default — the dashboard is about the Go bill.
232
232
  holidayAware: config.holidayAware === true,
233
+ // The subscription price and its monthly allowance can be overridden here,
234
+ // so a plan change from OpenCode needs a config edit, not a patch: the
235
+ // discount shown on the page is derived from these two numbers.
236
+ planPriceUsd: Number.isFinite(config.planPriceUsd) && config.planPriceUsd > 0 ? config.planPriceUsd : undefined,
237
+ planAllowanceUsd: Number.isFinite(config.planAllowanceUsd) && config.planAllowanceUsd > 0 ? config.planAllowanceUsd : undefined,
233
238
  limits: LIMITS[plan],
234
239
  }
235
240
  }
@@ -287,45 +292,106 @@ export function peakMultiplier(model, at, holidayAware = false) {
287
292
  return PEAK.utcWeekdayHours.some(([from, to]) => hour >= from && hour < to) ? PEAK.factor : 1
288
293
  }
289
294
 
295
+ /** Is `side` billing at peak right now?
296
+ *
297
+ * The two sides keep different calendars: DeepSeek's own API treats Chinese
298
+ * statutory holidays as off-peak, while OpenCode bills by its weekday schedule
299
+ * only (anomalyco/opencode#53071). Both use the same wall-clock windows, so the
300
+ * only difference is the holiday rule. */
301
+ export function peakAt(side, at = Date.now()) {
302
+ const date = new Date(at)
303
+ const day = date.getUTCDay()
304
+ if (day === 0 || day === 6) return false
305
+ const hour = date.getUTCHours()
306
+ if (!PEAK.utcWeekdayHours.some(([from, to]) => hour >= from && hour < to)) return false
307
+ return side === 'official' ? !isChinaHoliday(at) : true
308
+ }
309
+
310
+ /** The peak windows start and end on these hours, so a flip can only happen at
311
+ * one of them — no need to scan every minute. */
312
+ const PEAK_BOUNDARIES = PEAK.utcWeekdayHours.flatMap(([from, to]) => [from, to])
313
+
314
+ /** Peak state for both sides, plus how long until each one flips, in hours with
315
+ * one decimal. */
316
+ export function peakClock(at = Date.now()) {
317
+ const side = (name) => {
318
+ const peak = peakAt(name, at)
319
+ const base = new Date(at)
320
+ let next = null
321
+ for (let day = 0; day < 14 && next === null; day += 1) {
322
+ for (const hour of PEAK_BOUNDARIES) {
323
+ const point = Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), base.getUTCDate() + day, hour, 0, 0, 0)
324
+ if (point <= at) continue
325
+ if (peakAt(name, point) === peak) continue
326
+ next = point
327
+ break
328
+ }
329
+ }
330
+ const hours = next === null ? null : Math.round(((next - at) / HOUR) * 10) / 10
331
+ return { peak, next, hours }
332
+ }
333
+ return { at, holiday: isChinaHoliday(at), official: side('official'), opencode: side('opencode') }
334
+ }
335
+
290
336
  /** What each plan costs per month, in USD. The plan's allowance is metered at
291
337
  * DeepSeek's list price, so the subscription buys its allowance at a fixed
292
338
  * discount — Go pays $10 for $100 of list-priced usage, i.e. 10x. */
293
339
  const PLAN_PRICES = { go: 10, 'go-plus': 40, 'go-teams': 20 }
294
340
 
341
+ /** The three lines DeepSeek bills on, in the order the page shows them: a cached
342
+ * prompt token is by far the cheapest, so it leads. */
343
+ export const BILLING_KINDS = ['cacheRead', 'input', 'output']
344
+
295
345
  /** Which route is cheaper at this instant, expressed in the money you actually
296
- * hand over: Go charges the list price against a 10x-discounted allowance, while
297
- * DeepSeek's API bills the list price directly. */
298
- export function peakSnapshot(cnyPerUsd, at = Date.now(), plan = 'go') {
346
+ * hand over: OpenCode charges the list price against a discounted allowance,
347
+ * while DeepSeek's API bills the list price directly.
348
+ *
349
+ * `overrides` lets the settings page (or a future price change) replace the
350
+ * plan price and its allowance without touching this file: the discount is
351
+ * derived, so editing either number re-derives the verdict on its own. */
352
+ export function peakSnapshot(cnyPerUsd, at = Date.now(), plan = 'go', overrides = {}) {
299
353
  const fx = Number.isFinite(cnyPerUsd) && cnyPerUsd > 0 ? cnyPerUsd : OFFICIAL.defaultCnyPerUsd
300
- const priceUsd = PLAN_PRICES[plan] ?? PLAN_PRICES.go
301
- const allowanceUsd = LIMITS[plan]?.['deepseek-v4.1-flash'] ?? LIMITS.go['deepseek-v4.1-flash']
354
+ const priceUsd = Number.isFinite(overrides.priceUsd) && overrides.priceUsd > 0
355
+ ? overrides.priceUsd
356
+ : PLAN_PRICES[plan] ?? PLAN_PRICES.go
357
+ const listed = LIMITS[plan]?.['deepseek-v4.1-flash'] ?? LIMITS.go['deepseek-v4.1-flash']
358
+ const allowanceUsd = Number.isFinite(overrides.allowanceUsd) && overrides.allowanceUsd > 0
359
+ ? overrides.allowanceUsd
360
+ : listed
302
361
  const leverage = allowanceUsd / priceUsd
303
362
  const routes = []
304
363
  for (const [model, official] of Object.entries(OFFICIAL.prices)) {
305
364
  const goFactor = peakMultiplier(model, at, false)
306
365
  const officialFactor = peakMultiplier(model, at, true)
307
- // One million output tokens is the yardstick: output dominates a coding bill.
308
- const list = official.output
309
- const burn = list * goFactor // what the request subtracts from the allowance
310
- const goPay = burn / leverage // what it actually costs you, given the discount
311
- const officialPay = list * officialFactor
366
+ const rows = BILLING_KINDS.map((kind) => {
367
+ // One million tokens of this kind is the yardstick; output dominates a
368
+ // coding bill, so the headline verdict still reads off the output line.
369
+ const list = official[kind]
370
+ const burn = list * goFactor // what the request subtracts from the allowance
371
+ const goPay = burn / leverage // what it actually costs you, given the discount
372
+ const officialPay = list * officialFactor
373
+ return { kind, list, go: { factor: goFactor, burn, pay: goPay }, official: { factor: officialFactor, pay: officialPay }, cheaper: goPay <= officialPay ? 'go' : 'official', ratio: goPay / officialPay }
374
+ })
375
+ const headline = rows[rows.length - 1]
312
376
  routes.push({
313
377
  model,
314
- list,
315
- go: { factor: goFactor, burn, pay: goPay },
316
- official: { factor: officialFactor, pay: officialPay },
317
- cheaper: goPay <= officialPay ? 'go' : 'official',
318
- ratio: goPay / officialPay,
378
+ rows,
379
+ list: headline.list,
380
+ go: headline.go,
381
+ official: headline.official,
382
+ cheaper: headline.cheaper,
383
+ ratio: headline.ratio,
319
384
  })
320
385
  }
321
386
  return {
322
387
  at,
323
388
  cnyPerUsd: fx,
324
389
  holiday: isChinaHoliday(at),
325
- plan: { id: plan, priceUsd, priceCny: priceUsd * fx, allowanceUsd, allowanceCny: allowanceUsd * fx, leverage },
390
+ plan: { id: plan, priceUsd, priceCny: priceUsd * fx, allowanceUsd, allowanceCny: allowanceUsd * fx, leverage, overridden: Boolean(overrides.priceUsd || overrides.allowanceUsd) },
326
391
  go: { peak: peakMultiplier('deepseek-v4.1-flash', at, false) === PEAK.factor },
327
392
  official: { peak: peakMultiplier('deepseek-v4.1-flash', at, true) === PEAK.factor },
328
393
  factor: PEAK.factor,
394
+ kinds: BILLING_KINDS,
329
395
  models: routes,
330
396
  }
331
397
  }
@@ -659,7 +725,11 @@ export function apply(ctx, config = {}) {
659
725
  startedAt,
660
726
  windowStarts: starts,
661
727
  holidayAware: cfg.holidayAware,
662
- peak: peakSnapshot(cfg.cnyPerUsd, Date.now(), detectedPlan ?? cfg.plan),
728
+ clock: peakClock(),
729
+ peak: peakSnapshot(cfg.cnyPerUsd, Date.now(), detectedPlan ?? cfg.plan, {
730
+ priceUsd: cfg.planPriceUsd,
731
+ allowanceUsd: cfg.planAllowanceUsd,
732
+ }),
663
733
  usage: usageSnapshot,
664
734
  usageError,
665
735
  accounts,