dsh-opencode-routing 2.2.0 → 2.3.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 +98 -67
- package/README.ru.md +166 -0
- package/README.zh-CN.md +141 -0
- package/README.zh-TW.md +111 -0
- package/client.js +1221 -990
- package/package.json +11 -1
- package/router.js +12 -0
- package/test.mjs +48 -0
- package/usage.js +45 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-opencode-routing",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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/router.js
CHANGED
|
@@ -967,6 +967,18 @@ export function apply(ctx, config = {}) {
|
|
|
967
967
|
|
|
968
968
|
async function handleApi(request, response) {
|
|
969
969
|
try {
|
|
970
|
+
if (request.method === 'GET') {
|
|
971
|
+
// Settings can ask for one account's key so "edit key" opens with the
|
|
972
|
+
// current value filled in. Local-only endpoint, never in the snapshot.
|
|
973
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1')
|
|
974
|
+
const reveal = url.searchParams.get('key')
|
|
975
|
+
if (reveal !== null) {
|
|
976
|
+
const account = state.accounts.find((entry) => entry.id === reveal)
|
|
977
|
+
if (account === undefined) throw new Error(`没有账户 ${reveal}`)
|
|
978
|
+
sendJson(response, 200, { id: account.id, key: (await accountKey(account)) ?? null })
|
|
979
|
+
return
|
|
980
|
+
}
|
|
981
|
+
}
|
|
970
982
|
if (request.method === 'POST') {
|
|
971
983
|
const text = await readBody(request)
|
|
972
984
|
let body
|
package/test.mjs
CHANGED
|
@@ -357,6 +357,23 @@ await sleep(300)
|
|
|
357
357
|
assert.equal(freeRow.remaining, undefined, 'free models have no remaining quota')
|
|
358
358
|
assert.equal(freeRow.inputs.includes('video'), true)
|
|
359
359
|
assert.equal(data.peak.plan.leverage, 10)
|
|
360
|
+
|
|
361
|
+
// Every model breaks down into the three lines DeepSeek bills on, cheapest first
|
|
362
|
+
const peakFlash = data.peak.models.find((entry) => entry.model === 'deepseek-v4.1-flash')
|
|
363
|
+
assert.deepEqual(peakFlash.rows.map((line) => line.kind), ['cacheRead', 'input', 'output'])
|
|
364
|
+
assert.equal(peakFlash.rows[0].cheaper, 'go', 'the cached line is still cheaper on OpenCode')
|
|
365
|
+
assert.equal(Math.round(peakFlash.rows[0].ratio * 100) / 100, 0.1)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// A plan change is a config edit, not a code change: the discount is derived
|
|
369
|
+
{
|
|
370
|
+
const offPeak = Date.UTC(2026, 10, 3, 16, 0)
|
|
371
|
+
const cheaper = peakSnapshot(7.1, offPeak, 'go', { priceUsd: 10, allowanceUsd: 50 })
|
|
372
|
+
assert.equal(cheaper.plan.leverage, 5, 'a smaller allowance halves the discount')
|
|
373
|
+
assert.equal(cheaper.plan.overridden, true)
|
|
374
|
+
const dearer = peakSnapshot(7.1, offPeak, 'go', { priceUsd: 50, allowanceUsd: 100 })
|
|
375
|
+
assert.equal(dearer.plan.leverage, 2)
|
|
376
|
+
assert.equal(dearer.models[0].rows[0].cheaper, 'go', 'still cheaper, just less so')
|
|
360
377
|
}
|
|
361
378
|
console.log('usage view ok')
|
|
362
379
|
|
|
@@ -369,6 +386,37 @@ console.log('usage view ok')
|
|
|
369
386
|
}
|
|
370
387
|
console.log('single-account fallback ok')
|
|
371
388
|
|
|
389
|
+
// "Edit key" pre-fills from the stored value; the snapshot never carries it
|
|
390
|
+
{
|
|
391
|
+
const reveal = async (query) => {
|
|
392
|
+
const handler = apiRoutes.get('/opencode-quota-router.json')
|
|
393
|
+
const response = {
|
|
394
|
+
status: 0,
|
|
395
|
+
body: undefined,
|
|
396
|
+
writeHead(status) {
|
|
397
|
+
this.status = status
|
|
398
|
+
},
|
|
399
|
+
end(chunk) {
|
|
400
|
+
this.body = JSON.parse(chunk)
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
await handler({ method: 'GET', url: `/opencode-quota-router.json${query}` }, response)
|
|
404
|
+
return response
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const hit = await reveal('?key=a1')
|
|
408
|
+
assert.equal(hit.status, 200)
|
|
409
|
+
assert.equal(hit.body.key, 'oc_sk_test', 'the stored key comes back for the editor')
|
|
410
|
+
|
|
411
|
+
const missing = await reveal('?key=zzz')
|
|
412
|
+
assert.equal(missing.status, 400, 'an unknown account is rejected')
|
|
413
|
+
assert.match(missing.body.error, /zzz/)
|
|
414
|
+
|
|
415
|
+
const snapshot = await api('GET')
|
|
416
|
+
assert.equal(JSON.stringify(snapshot.body).includes('oc_sk_test'), false, 'the snapshot stays masked')
|
|
417
|
+
}
|
|
418
|
+
console.log('reveal key ok')
|
|
419
|
+
|
|
372
420
|
for (const dispose of disposers) await dispose()
|
|
373
421
|
server.close()
|
|
374
422
|
console.log('all accounts checks passed')
|
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
|
}
|
|
@@ -292,40 +297,60 @@ export function peakMultiplier(model, at, holidayAware = false) {
|
|
|
292
297
|
* discount — Go pays $10 for $100 of list-priced usage, i.e. 10x. */
|
|
293
298
|
const PLAN_PRICES = { go: 10, 'go-plus': 40, 'go-teams': 20 }
|
|
294
299
|
|
|
300
|
+
/** The three lines DeepSeek bills on, in the order the page shows them: a cached
|
|
301
|
+
* prompt token is by far the cheapest, so it leads. */
|
|
302
|
+
export const BILLING_KINDS = ['cacheRead', 'input', 'output']
|
|
303
|
+
|
|
295
304
|
/** 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
|
-
|
|
305
|
+
* hand over: OpenCode charges the list price against a discounted allowance,
|
|
306
|
+
* while DeepSeek's API bills the list price directly.
|
|
307
|
+
*
|
|
308
|
+
* `overrides` lets the settings page (or a future price change) replace the
|
|
309
|
+
* plan price and its allowance without touching this file: the discount is
|
|
310
|
+
* derived, so editing either number re-derives the verdict on its own. */
|
|
311
|
+
export function peakSnapshot(cnyPerUsd, at = Date.now(), plan = 'go', overrides = {}) {
|
|
299
312
|
const fx = Number.isFinite(cnyPerUsd) && cnyPerUsd > 0 ? cnyPerUsd : OFFICIAL.defaultCnyPerUsd
|
|
300
|
-
const priceUsd =
|
|
301
|
-
|
|
313
|
+
const priceUsd = Number.isFinite(overrides.priceUsd) && overrides.priceUsd > 0
|
|
314
|
+
? overrides.priceUsd
|
|
315
|
+
: PLAN_PRICES[plan] ?? PLAN_PRICES.go
|
|
316
|
+
const listed = LIMITS[plan]?.['deepseek-v4.1-flash'] ?? LIMITS.go['deepseek-v4.1-flash']
|
|
317
|
+
const allowanceUsd = Number.isFinite(overrides.allowanceUsd) && overrides.allowanceUsd > 0
|
|
318
|
+
? overrides.allowanceUsd
|
|
319
|
+
: listed
|
|
302
320
|
const leverage = allowanceUsd / priceUsd
|
|
303
321
|
const routes = []
|
|
304
322
|
for (const [model, official] of Object.entries(OFFICIAL.prices)) {
|
|
305
323
|
const goFactor = peakMultiplier(model, at, false)
|
|
306
324
|
const officialFactor = peakMultiplier(model, at, true)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
325
|
+
const rows = BILLING_KINDS.map((kind) => {
|
|
326
|
+
// One million tokens of this kind is the yardstick; output dominates a
|
|
327
|
+
// coding bill, so the headline verdict still reads off the output line.
|
|
328
|
+
const list = official[kind]
|
|
329
|
+
const burn = list * goFactor // what the request subtracts from the allowance
|
|
330
|
+
const goPay = burn / leverage // what it actually costs you, given the discount
|
|
331
|
+
const officialPay = list * officialFactor
|
|
332
|
+
return { kind, list, go: { factor: goFactor, burn, pay: goPay }, official: { factor: officialFactor, pay: officialPay }, cheaper: goPay <= officialPay ? 'go' : 'official', ratio: goPay / officialPay }
|
|
333
|
+
})
|
|
334
|
+
const headline = rows[rows.length - 1]
|
|
312
335
|
routes.push({
|
|
313
336
|
model,
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
337
|
+
rows,
|
|
338
|
+
list: headline.list,
|
|
339
|
+
go: headline.go,
|
|
340
|
+
official: headline.official,
|
|
341
|
+
cheaper: headline.cheaper,
|
|
342
|
+
ratio: headline.ratio,
|
|
319
343
|
})
|
|
320
344
|
}
|
|
321
345
|
return {
|
|
322
346
|
at,
|
|
323
347
|
cnyPerUsd: fx,
|
|
324
348
|
holiday: isChinaHoliday(at),
|
|
325
|
-
plan: { id: plan, priceUsd, priceCny: priceUsd * fx, allowanceUsd, allowanceCny: allowanceUsd * fx, leverage },
|
|
349
|
+
plan: { id: plan, priceUsd, priceCny: priceUsd * fx, allowanceUsd, allowanceCny: allowanceUsd * fx, leverage, overridden: Boolean(overrides.priceUsd || overrides.allowanceUsd) },
|
|
326
350
|
go: { peak: peakMultiplier('deepseek-v4.1-flash', at, false) === PEAK.factor },
|
|
327
351
|
official: { peak: peakMultiplier('deepseek-v4.1-flash', at, true) === PEAK.factor },
|
|
328
352
|
factor: PEAK.factor,
|
|
353
|
+
kinds: BILLING_KINDS,
|
|
329
354
|
models: routes,
|
|
330
355
|
}
|
|
331
356
|
}
|
|
@@ -659,7 +684,10 @@ export function apply(ctx, config = {}) {
|
|
|
659
684
|
startedAt,
|
|
660
685
|
windowStarts: starts,
|
|
661
686
|
holidayAware: cfg.holidayAware,
|
|
662
|
-
peak: peakSnapshot(cfg.cnyPerUsd, Date.now(), detectedPlan ?? cfg.plan
|
|
687
|
+
peak: peakSnapshot(cfg.cnyPerUsd, Date.now(), detectedPlan ?? cfg.plan, {
|
|
688
|
+
priceUsd: cfg.planPriceUsd,
|
|
689
|
+
allowanceUsd: cfg.planAllowanceUsd,
|
|
690
|
+
}),
|
|
663
691
|
usage: usageSnapshot,
|
|
664
692
|
usageError,
|
|
665
693
|
accounts,
|