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/CONTRIBUTING.md +46 -0
- package/README.ja.md +111 -0
- package/README.ko.md +154 -0
- package/README.md +102 -73
- package/README.ru.md +166 -0
- package/README.zh-CN.md +141 -0
- package/README.zh-TW.md +111 -0
- package/client.js +1519 -1137
- package/package.json +11 -1
- package/test.mjs +36 -1
- package/usage.js +87 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-opencode-routing",
|
|
3
|
-
"version": "2.
|
|
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:
|
|
297
|
-
* DeepSeek's API bills the list price directly.
|
|
298
|
-
|
|
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 =
|
|
301
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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,
|