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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-opencode-routing",
3
- "version": "2.2.0",
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: 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') {
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 = PLAN_PRICES[plan] ?? PLAN_PRICES.go
301
- const allowanceUsd = LIMITS[plan]?.['deepseek-v4.1-flash'] ?? LIMITS.go['deepseek-v4.1-flash']
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
- // 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
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
- list,
315
- go: { factor: goFactor, burn, pay: goPay },
316
- official: { factor: officialFactor, pay: officialPay },
317
- cheaper: goPay <= officialPay ? 'go' : 'official',
318
- ratio: goPay / officialPay,
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,