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.
@@ -0,0 +1,46 @@
1
+ # Contributing
2
+
3
+ Thanks for taking a look. This plugin is a single package that mounts two halves (a usage dashboard and a quota
4
+ router) into one DeepSeek Harness Settings page.
5
+
6
+ ## Ground rules
7
+
8
+ - Node 22+ (the tests use `zlib.zstdDecompressSync` and the built-in `fetch`).
9
+ - No runtime dependencies. The plugin must stay installable with `npm i` and nothing else.
10
+ - Keep both halves independent: `usage.js` must not import from `router.js` and vice versa; only `index.js` composes
11
+ them.
12
+ - The dashboard is read-only towards the account pool. Only `router.js` writes the pool state file.
13
+
14
+ ## Local workflow
15
+
16
+ ```bash
17
+ node test.mjs # unit + integration, no network, no model quota
18
+ node --check client.js # the browser bundle is a plain ES module factory
19
+ ```
20
+
21
+ `test.mjs` starts a local HTTP server that answers the usage endpoint per API key, so a full failover can be
22
+ exercised offline.
23
+
24
+ If you change the UI, also load the settings page in a running Harness (`dsh` desktop app) and check that the section
25
+ still renders; the client bundle is only reloaded on app start.
26
+
27
+ ## Translations
28
+
29
+ UI strings live in `client.js` as `DICT`. The extra locales are generated from `i18n/ui-<lang>.json` by:
30
+
31
+ ```bash
32
+ node scripts/merge-i18n.mjs
33
+ ```
34
+
35
+ Add a language by dropping `i18n/ui-<lang>.json` (same keys as `zh`) and re-running the script.
36
+
37
+ ## Pull requests
38
+
39
+ - One topic per PR, with a short description of the behaviour change.
40
+ - Add or update assertions in `test.mjs` for anything behavioural.
41
+ - Update `README.md` (and, if you can, the other `README.<lang>.md` files) when the user-visible behaviour changes.
42
+
43
+ ## Reporting bugs
44
+
45
+ Include the plugin version, the Harness version, your OS, and — if it is a routing problem — the JSON from
46
+ `GET /opencode-quota-router.json` with keys masked (they already are).
package/README.ja.md ADDED
@@ -0,0 +1,111 @@
1
+ # dsh-opencode-routing
2
+
3
+ [English](README.md) · [简体中文](README.zh-CN.md) · [繁體中文](README.zh-TW.md) · **日本語** · [한국어](README.ko.md) · [Русский](README.ru.md)
4
+
5
+ 2 つの既存プラグインを 1 つのパッケージに統合した [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)プラグインです。**A→B の自動フェイルオーバーとクォータルーティングを備えた OpenCode アカウントプール**と、**アカウント別 / モデル別の使用量ダッシュボード**を提供します。どちらも 1 つの設定ページ「OpenCode アカウントと使用量」に置かれています。
6
+
7
+ - **クライアント**: 1 つの `settings.section` を 60 秒ごとに更新します。アカウントは追加・編集・並べ替え・プローブができ、モデルカードには機能タグが付き、2 つの表はドラッグで横にスクロールできます。UI は 10 言語に対応します(ブラウザの言語が優先)。
8
+ - **ホスト**: 2 つのエンドポイント — `GET /opencode-usage.json`(使用量。`?refresh=1` で強制更新)と
9
+ `GET|POST /opencode-quota-router.json`(プールとルーティング。アクションは後述)。
10
+
11
+ ## インストール
12
+
13
+ ```bash
14
+ npm i dsh-opencode-routing
15
+ ```
16
+
17
+ パッケージは独自の `cordis.patch.yml` を同梱しています。`opencode-go` / `opencode-free` プロバイダーを宣言し、**単一**の insert(`id: opencode-router`)を持ちます。`usage:` と `router:` のサブオブジェクトがそれぞれの側の設定を保持します(両方とも `path` を持つため、1 つのフラットな名前空間を共有することはできません)。
18
+
19
+ ```yaml
20
+ - insert:
21
+ - id: opencode-router
22
+ name: 'dsh-opencode-routing'
23
+ config:
24
+ usageUrl: https://opencode.ai/inference/go/v1/usage
25
+ apiKeyRef: OPENCODE_API_KEY
26
+ usage:
27
+ path: /opencode-usage
28
+ plan: go
29
+ router:
30
+ path: /opencode-quota-router
31
+ routes:
32
+ - primary: { provider: opencode-go, model: deepseek-v4.1-flash }
33
+ fallback: { provider: deepseek-account, model: deepseek-flash }
34
+ ```
35
+
36
+ トップレベルのキーは両方の側に適用されます(`usageUrl`、`apiKeyRef`、`probeIntervalMs` など)。サブオブジェクト内のキーはその側だけを上書きします。
37
+
38
+ ## アカウントプールとフェイルオーバー
39
+
40
+ プールの順序は使用順です。常に 1 つのアカウントだけが「カレント」であり、そのキーはプロバイダーが読む認証情報参照(既定は `OPENCODE_API_KEY`)にミラーされます。したがってアカウントの切り替えにプロバイダー設定の変更は不要です。`resolveApiKey` は `agent/request` フックの後に、ストリーミング呼び出しのたびに実行されます。
41
+
42
+ - **A が枯渇したら B が引き継ぐ**: 公式の許容量が使い切られたことが確認されたうえでの `QUOTA` / `ACCOUNT_QUOTA` / 429、または認証系のエラー(`AUTH`、`INVALID_CREDENTIAL`、`MISSING_CREDENTIAL`、`ACCOUNT_SIGN_IN_REQUIRED`、`CLIENT_UPGRADE_REQUIRED`)では、カレントアカウントを使用不可としてマークし(ウィンドウのリセットまで、認証失敗の場合は 30 分間)、ルーティングを次の有効なアカウントへ移して**同じステップを再試行**します — ターンがダウングレードされることはありません。
43
+ - **単一モデルの隔離**: 1 つのモデルだけが許容量を超え、プールにはまだ予算がある場合、そのモデルだけを隔離します(既定 30 分)。他のすべてのモデルはプランをそのまま使い続けます。
44
+ - **プール全体の枯渇**: そのときに限り、ルートは `fallback`(既定は DeepSeek アカウント)へ退避し、30 分ごと(またはローリングウィンドウのリセット時)にゼロトークンの `GET /inference/go/v1/usage` プローブを行って、自動でプランに戻ります。
45
+ - **リセット**: 設定の「封禁マークのリセット」は、枯渇/認証のマークをすべてクリアし、即座にプールへ戻します。アカウントとキーはそのまま残ります。
46
+
47
+ ## 使用量ダッシュボード
48
+
49
+ 公式の `/inference/go/v1/usage` エンドポイントは、丸められたローリング / 週次 / 月次のパーセントだけを返し、**モデル別の内訳は返しません**。一方プランはモデルごとに個別に計量します(5 時間 = 月間許容量の 20 %、週 = 50 %、月 = 100 %)。そのためページには 2 種類の数値が並びます。
50
+
51
+ - **公式パーセント**(アカウントごとに 1 行): API からそのまま得た実際のアカウント使用量で、他のクライアントや他のマシンも含みます。
52
+ - **モデル別の金額**(モデル表): ローカル推定値です。`$DSH_HOME/sessions` を過去 40 日分スキャンし直し、プロバイダーが `opencode` で始まる `assistant/message` イベントだけを、公開料金(DeepSeek の 2 倍のピーク時間帯を含む)で計算します。他のツールからの使用量にはローカル記録がありません。
53
+
54
+ 「使用量を表示」はアカウント別のダイアログを開き、3 つのウィンドウ、それらのリセット時刻、プローブ結果を表示します(そのアカウントだけをプローブし、モデルを切り替えることはありません)。
55
+
56
+ 「キーを編集」は `GET /opencode-quota-router.json?key=<account id>` からアカウントの現在のキーを取得し、入力欄に事前入力します(ローカル専用のエンドポイントで、一覧のスナップショットはマスク値しか持ちません)。モデル名はベンダーの表記に従います(`deepseek-v4.1-flash` → `DeepSeek V4.1 Flash`)。公開ブランドマークのあるベンダーはインライン SVG で描画され、それ以外はレターバッジのままです。「次回プローブ」は実際のタイマー(既定 30 分)で、ページは毎秒再描画されるためカウントダウンします。ページを開くだけであればスナップショットを読むだけで、プローブは発生しません。
57
+
58
+ 「今どちらが得か」はサブスクリプションのレバレッジから導かれます。Go は $10 で定価 $100 分の許容量(≈ ¥710)を提供するため、OpenCode は常に公式価格の 10 %、つまり 90 % 安くなります。実データが公式 API に有利になることはありません。カード隅のボタンでサンプルデータに切り替わり、そのケースがどう見えるかを確認できます。
59
+
60
+ 機能の列は同じカタログエンドポイント(`/inference/go/v1/models`)から取得します。`modalities.input` と `capabilities` は text / image / video / audio / PDF、ツール呼び出し、推論、さらにコンテキスト長として描画されます。価格がゼロのモデルは「無限」と表示されます。
61
+
62
+ ### ピーク / オフピークと祝日
63
+
64
+ DeepSeek のオフピーク時間帯は、北京時間の月〜金 09:00–12:00 と 14:00–18:00 以外のすべて(昼休み、夜間、週末、**法定祝日**を含む)で、価格はちょうどピーク時の半額です。OpenCode は UTC の月〜金 01–04 と 06–10 だけをピークとして扱い、**中国の法定祝日を考慮しません**(anomalyco/opencode#53071、メンテナー曰く*「Go の価格は当社独自のスケジュールに従っており、DeepSeek の祝日カレンダーには従いません。」*)。
65
+
66
+ プラグインは 2026–2031 年の中国法定祝日表を同梱しています。これが公式側に適用されるのは `holidayAware: true`(`usage.holidayAware`)が設定されているときだけです。OpenCode はそうした日もピーク料金を請求し続けるため、既定では無効です。表は `usage.js` の `HOLIDAYS` にあり、`isChinaHoliday()` を単独で呼び出すこともできます。
67
+
68
+ サブスクリプションの価格とその許容量は `usage.planPriceUsd` / `usage.planAllowanceUsd` で上書きできます。ページ上の割引はこの 2 つの数値から導かれるため、OpenCode 側のプラン変更はパッチではなく設定の編集で済みます。
69
+
70
+ ## 設定 API
71
+
72
+ `GET /opencode-quota-router.json` は `{mode, reason, active, accounts[], quarantine[], routes[], lastFailure}` を返します。アカウントのキーは常にマスクされます(`hint`)。同じパスに `{action, ...}` を付けて `POST` します。
73
+
74
+ | アクション | 引数 | 効果 |
75
+ | --- | --- | --- |
76
+ | `add` | `label?`, `key` | アカウントを作成(キーは 8 文字以上)、保存してプローブ |
77
+ | `update` | `id`, `label?` / `enabled?` / `key?` | 名前の変更、無効化/有効化、キーのローテーション |
78
+ | `remove` | `id` | アカウントとそのキーを削除(常に 1 つは残ります) |
79
+ | `activate` | `id` | 今すぐそのアカウントに切り替え |
80
+ | `move` | `id`, `direction: up\|down` | 並べ替え |
81
+ | `probe` | `id?` | すべてのアカウント、または 1 つだけをプローブ |
82
+ | `reset` | — | 枯渇/認証マークをクリアしてプールに戻す |
83
+
84
+ `GET /opencode-usage.json` は `{plan, configuredPlan, detectedPlan, holidayAware, peak, usage, accounts[], activeAccount, models[], totals}` を返します。`?refresh=1` を付けると、先に最新の公式スナップショットを強制取得します。
85
+
86
+ ## 言語
87
+
88
+ UI 文字列は `client.js` の `DICT` にあります。`zh`(ソース)、`zh-tw`、`en`、さらに `es` / `fr` / `de` / `pt` / `ja` / `ko` / `ru`(`scripts/merge-i18n.mjs` が翻訳ファイルからインライン化します。キーが見つからない場合は `en`、次にキー自身へフォールバックします)。`localeOf()` はブラウザ言語のプレフィックスを照合し、簡体字中国語と繁体字中国語を区別します。
89
+
90
+ ## テスト
91
+
92
+ ```bash
93
+ node test.mjs
94
+ ```
95
+
96
+ モデルのクォータは不要です。偽の context、偽の認証情報ストア、偽の webServer、そして API キーごとに応答するローカルの使用量エンドポイントを使います。対象範囲: 複合マウント、アカウントの採用、単一モデルの隔離、キーのミラリングを伴う A→B フェイルオーバー、アカウント別プローブ、プール全体のフォールバック、リセット、設定アクションの検証、使用量ビュー(機能タグ、無制限モデル、ピーク/オフピークの計算、請求行)、単一アカウント時の縮退。
97
+
98
+ ## 旧パッケージとの関係
99
+
100
+ `dsh-opencode-quota-router` と `dsh-opencode-usage-settings` はその前身であり、それらが行っていたことはすべてここに統合されています。どちらも npm で非推奨とマークされ、このパッケージを指しています。リポジトリ:
101
+ [`2869949486/dsh-opencode-routing`](https://github.com/2869949486/dsh-opencode-routing)(現行)、
102
+ [`dsh-opencode-quota-router`](https://github.com/2869949486/dsh-opencode-quota-router)、
103
+ [`dsh-opencode-usage`](https://github.com/2869949486/dsh-opencode-usage)。
104
+
105
+ ## コントリビュート
106
+
107
+ Issue と pull request を歓迎します — [CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。PR を開く前に `node test.mjs` を実行してください。
108
+
109
+ ## ライセンス
110
+
111
+ MIT — [LICENSE](LICENSE) を参照してください。
package/README.ko.md ADDED
@@ -0,0 +1,154 @@
1
+ # dsh-opencode-routing
2
+
3
+ [English](README.md) · [简体中文](README.zh-CN.md) · [繁體中文](README.zh-TW.md) · [日本語](README.ja.md) · **한국어** · [Русский](README.ru.md)
4
+
5
+ 이전의 두 플러그인을 하나의 패키지로 합친 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) 플러그인입니다:
6
+ **A→B 자동 페일오버와 쿼터 라우팅을 갖춘 OpenCode 계정 풀**, 그리고 **계정별 / 모델별 사용량 대시보드**가 함께 들어 있습니다.
7
+ 둘 다 "OpenCode 계정 및 사용량"이라는 하나의 Settings 페이지에 있습니다.
8
+
9
+ - **클라이언트**: `settings.section` 하나를 60초마다 갱신하며, 계정 추가·편집·순서 변경·프로브가 가능합니다;
10
+ 모델 카드에는 기능 태그가 붙고, 두 테이블 모두 드래그로 이동하며, UI는 10개 언어를 지원합니다(브라우저 언어 우선).
11
+ - **호스트**: 엔드포인트는 두 개입니다 — `GET /opencode-usage.json`(사용량, `?refresh=1`로 강제 갱신)과
12
+ `GET|POST /opencode-quota-router.json`(풀과 라우팅, 액션은 아래 참조).
13
+
14
+ ## 설치
15
+
16
+ ```bash
17
+ npm i dsh-opencode-routing
18
+ ```
19
+
20
+ 패키지에는 자체 `cordis.patch.yml`이 포함되어 있습니다: `opencode-go` / `opencode-free` 프로바이더를 선언하고
21
+ **하나의** insert(`id: opencode-router`)를 둡니다. `usage:`와 `router:` 하위 객체가 각 절반의 설정을 담습니다
22
+ (양쪽 모두 `path`를 가지므로 하나의 평면 네임스페이스를 공유할 수 없습니다).
23
+
24
+ ```yaml
25
+ - insert:
26
+ - id: opencode-router
27
+ name: 'dsh-opencode-routing'
28
+ config:
29
+ usageUrl: https://opencode.ai/inference/go/v1/usage
30
+ apiKeyRef: OPENCODE_API_KEY
31
+ usage:
32
+ path: /opencode-usage
33
+ plan: go
34
+ router:
35
+ path: /opencode-quota-router
36
+ routes:
37
+ - primary: { provider: opencode-go, model: deepseek-v4.1-flash }
38
+ fallback: { provider: deepseek-account, model: deepseek-flash }
39
+ ```
40
+
41
+ 최상위 키는 양쪽 절반에 모두 적용되고(`usageUrl`, `apiKeyRef`, `probeIntervalMs`, …), 하위 객체 안의 키는
42
+ 해당 절반만 덮어씁니다.
43
+
44
+ ## 계정 풀과 페일오버
45
+
46
+ 풀 순서가 곧 사용 순서입니다. 어느 시점이든 정확히 하나의 계정이 "current"이며, 그 키는 프로바이더가 읽는
47
+ 자격 증명 참조(기본값 `OPENCODE_API_KEY`)로 미러링됩니다. 따라서 계정 전환에는 프로바이더 설정 변경이 필요하지
48
+ 않습니다: `resolveApiKey`는 매 스트리밍 호출마다 `agent/request` 훅 이후에 실행됩니다.
49
+
50
+ - **A가 고갈되면 B가 넘겨받습니다**: 공식 허용량 소진이 확인된 `QUOTA` / `ACCOUNT_QUOTA` / 429, 또는 인증 계열
51
+ 오류(`AUTH`, `INVALID_CREDENTIAL`, `MISSING_CREDENTIAL`, `ACCOUNT_SIGN_IN_REQUIRED`,
52
+ `CLIENT_UPGRADE_REQUIRED`)가 나면 현재 계정을 사용 불가로 표시하고(윈도 리셋 시까지, 인증 실패는 30분간),
53
+ 라우팅을 다음 활성 계정으로 옮긴 뒤 **같은 스텝을 재시도**합니다 — 턴이 하위 등급으로 떨어지는 일은 없습니다.
54
+ - **단일 모델 격리**: 한 모델만 허용량이 소진되었고 풀에 아직 예산이 남아 있으면 그 모델만 격리하며(기본 30분),
55
+ 나머지 모델은 계속 플랜을 사용합니다.
56
+ - **풀 전체 고갈**: 그때서야 라우트가 `fallback`(기본은 DeepSeek 계정)으로 내려가고, 30분마다(또는 롤링 윈도
57
+ 리셋 시점에) 토큰을 소모하지 않는 `GET /inference/go/v1/usage` 프로브를 돌려 자동으로 플랜으로 복귀합니다.
58
+ - **리셋**: Settings의 "금지 표시 초기화"는 소진/인증 표시를 전부 지우고 즉시 풀로 돌아갑니다. 계정과 키는
59
+ 건드리지 않습니다.
60
+
61
+ ## 사용량 대시보드
62
+
63
+ 공식 `/inference/go/v1/usage` 엔드포인트는 롤링 / 주간 / 월간 비율을 반올림한 값만 돌려주고 **모델별 분해는
64
+ 제공하지 않습니다**. 반면 플랜은 모델마다 따로 과금합니다(5시간 = 월 허용량의 20%, 주 = 50%, 월 = 100%).
65
+ 그래서 페이지는 두 세트의 숫자를 보여줍니다:
66
+
67
+ - **공식 비율**(계정당 한 줄): API에서 그대로 가져온 값 — 다른 클라이언트와 다른 머신의 사용량까지 포함한 실제
68
+ 계정 사용량입니다.
69
+ - **모델별 금액**(모델 테이블): 로컬 추정치입니다. `$DSH_HOME/sessions`를 최근 40일 범위로 다시 스캔하면서
70
+ provider가 `opencode`로 시작하는 `assistant/message` 이벤트만 공개 요율(DeepSeek의 2배 피크 구간 포함)로
71
+ 계산합니다. 다른 도구에서 발생한 사용량은 로컬 기록이 없습니다.
72
+
73
+ "사용량 보기"는 세 개의 윈도, 각 윈도의 리셋 시각, 프로브 결과가 담긴 계정별 다이얼로그를 엽니다(해당 계정만
74
+ 프로브하며 모델은 절대 전환하지 않습니다).
75
+
76
+ "키 편집"은 `GET /opencode-quota-router.json?key=<account id>`에서 계정의 현재 키를 가져와 입력란을 미리
77
+ 채웁니다(로컬 전용 엔드포인트이며, 목록 스냅샷에는 항상 마스킹된 값만 실립니다). 모델 이름은 벤더 표기를
78
+ 따르며(`deepseek-v4.1-flash` → `DeepSeek V4.1 Flash`), 공개된 브랜드 마크가 있는 벤더는 인라인 SVG로, 나머지는
79
+ 문자 배지로 렌더링합니다. "다음 프로브"는 실제 타이머(기본 30분)이고 페이지가 1초마다 다시 렌더링되므로
80
+ 카운트다운됩니다. 페이지를 여는 것은 스냅샷을 읽기만 할 뿐 프로브를 트리거하지 않습니다.
81
+
82
+ "지금 어느 쪽이 더 저렴한가"는 구독 레버리지에서 파생됩니다: Go는 정가 기준 $100어치 허용량을 $10에
83
+ 판매하므로(≈ ¥710) OpenCode는 항상 공식 가격의 10% — 90% 저렴합니다. 실제 데이터는 공식 API에 유리한 적이
84
+ 없으며, 카드 모서리의 버튼으로 샘플 데이터로 전환하면 그러한 경우가 어떻게 보이는지 확인할 수 있습니다.
85
+
86
+ 기능 열은 같은 카탈로그 엔드포인트(`/inference/go/v1/models`)에서 옵니다: `modalities.input`과 `capabilities`를
87
+ 텍스트 / 이미지 / 비디오 / 오디오 / PDF, 툴 호출, 추론, 컨텍스트 길이로 렌더링합니다. 가격이 0인 모델은
88
+ "무제한"으로 표시합니다.
89
+
90
+ ### 피크 / 비피크와 공휴일
91
+
92
+ DeepSeek의 비피크 구간은 베이징 시간 기준 월–금 09:00–12:00과 14:00–18:00을 제외한 모든 시간(점심시간, 야간,
93
+ 주말, **법정 공휴일** 포함)이며 가격은 피크의 정확히 절반입니다. OpenCode는 UTC 월–금 01–04와 06–10만 피크로
94
+ 취급하고 **중국 법정 공휴일은 무시합니다**(anomalyco/opencode#53071, 메인테이너: *"Go pricing follows our own
95
+ schedule, not DeepSeek's holiday calendar."*).
96
+
97
+ 플러그인에는 2026–2031 중국 법정 공휴일 테이블이 들어 있습니다. 이는 `holidayAware: true`(`usage.holidayAware`)가
98
+ 설정된 경우에만 공식 쪽에 적용되며, OpenCode가 그날에도 피크 요금을 부과하기 때문에 기본값은 꺼짐입니다.
99
+ 테이블은 `usage.js`의 `HOLIDAYS`에 있고, `isChinaHoliday()`는 단독으로 호출할 수 있습니다.
100
+
101
+ 구독 가격과 허용량은 `usage.planPriceUsd` / `usage.planAllowanceUsd`로 덮어쓸 수 있습니다. 페이지의 할인율은
102
+ 이 두 숫자에서 계산되므로, OpenCode 쪽의 플랜 변경은 패치가 아니라 설정 수정으로 끝납니다.
103
+
104
+ ## 설정 API
105
+
106
+ `GET /opencode-quota-router.json`은 `{mode, reason, active, accounts[], quarantine[], routes[], lastFailure}`를
107
+ 반환하며, 계정 키는 항상 마스킹됩니다(`hint`). 같은 경로에 `{action, ...}`로 `POST`합니다:
108
+
109
+ | action | 인자 | 동작 |
110
+ | --- | --- | --- |
111
+ | `add` | `label?`, `key` | 계정 생성(키 ≥ 8자), 저장 후 프로브 |
112
+ | `update` | `id`, `label?` / `enabled?` / `key?` | 이름 변경, 비활성/활성, 키 교체 |
113
+ | `remove` | `id` | 계정과 키 삭제(항상 하나는 남김) |
114
+ | `activate` | `id` | 지금 해당 계정으로 전환 |
115
+ | `move` | `id`, `direction: up\|down` | 순서 변경 |
116
+ | `probe` | `id?` | 모든 계정 또는 하나만 프로브 |
117
+ | `reset` | — | 소진/인증 표시를 지우고 풀로 복귀 |
118
+
119
+ `GET /opencode-usage.json`은 `{plan, configuredPlan, detectedPlan, holidayAware, peak, usage, accounts[],
120
+ activeAccount, models[], totals}`를 반환하며, `?refresh=1`은 먼저 최신 공식 스냅샷을 강제로 가져옵니다.
121
+
122
+ ## 언어
123
+
124
+ UI 문자열은 `client.js`의 `DICT`에 있습니다: `zh`(원본), `zh-tw`, `en`, 그리고 `es` / `fr` / `de` / `pt` / `ja` /
125
+ `ko` / `ru`(`scripts/merge-i18n.mjs`가 번역 파일에서 인라인하며, 키가 없으면 `en`으로, 그다음에는 키 자체로
126
+ 폴백합니다). `localeOf()`는 브라우저 언어 프리픽스를 매칭하고 중국어 간체와 번체를 구분합니다.
127
+
128
+ ## 테스트
129
+
130
+ ```bash
131
+ node test.mjs
132
+ ```
133
+
134
+ 모델 쿼터는 필요하지 않습니다: 가짜 컨텍스트, 가짜 자격 증명 저장소, 가짜 webServer, 그리고 API 키별로 응답하는
135
+ 로컬 사용량 엔드포인트를 사용합니다. 커버 범위: 복합 마운트, 계정 채택, 단일 모델 격리, 키 미러링을 포함한 A→B
136
+ 페일오버, 계정별 프로브, 풀 전체 폴백, 리셋, 설정 액션 검증, 사용량 뷰(기능 태그, 무제한 모델, 피크/비피크
137
+ 계산, 과금 라인), 단일 계정 저하 모드.
138
+
139
+ ## 이전 패키지와의 관계
140
+
141
+ `dsh-opencode-quota-router`와 `dsh-opencode-usage-settings`가 전신이며, 두 패키지가 하던 모든 일이 이곳에
142
+ 병합되었습니다. 둘 다 npm에서 deprecated로 표시되어 이 패키지를 가리킵니다. 저장소:
143
+ [`2869949486/dsh-opencode-routing`](https://github.com/2869949486/dsh-opencode-routing)(현재),
144
+ [`dsh-opencode-quota-router`](https://github.com/2869949486/dsh-opencode-quota-router),
145
+ [`dsh-opencode-usage`](https://github.com/2869949486/dsh-opencode-usage).
146
+
147
+ ## 기여
148
+
149
+ 이슈와 풀 리퀘스트를 환영합니다 — [CONTRIBUTING.md](CONTRIBUTING.md)를 참고하세요. PR을 올리기 전에
150
+ `node test.mjs`를 실행하세요.
151
+
152
+ ## 라이선스
153
+
154
+ MIT — [LICENSE](LICENSE)를 참고하세요.
package/README.md CHANGED
@@ -1,21 +1,25 @@
1
1
  # dsh-opencode-routing
2
2
 
3
- 一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)插件,把原先分开的两个插件合成一个:
4
- **OpenCode 账户池 + 额度路由** 和 **用量看板** 现在装在同一个包里,设置里也只有一个页面
5
- 「OpenCode 账户与用量」。
3
+ [English](README.md) · [简体中文](README.zh-CN.md) · [繁體中文](README.zh-TW.md) · [日本語](README.ja.md) · [한국어](README.ko.md) · [Русский](README.ru.md)
6
4
 
7
- - **客户端**:`settings.section` 槽位,60 秒自动刷新;账户池可增删改、模型表带能力标签、两张表都能按住左右拖动;界面支持 10 种语言(跟随浏览器语言)。
8
- - **Host**:两个只读/管理接口 —— `GET /opencode-usage.json`(用量,`?refresh=1` 强制刷新)与 `GET|POST /opencode-quota-router.json`(账户池与路由,动作见下)。
5
+ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) plugin that merges two earlier plugins
6
+ into one package: an **OpenCode account pool with automatic A→B failover and quota routing**, plus a
7
+ **per-account / per-model usage dashboard**. Both live on a single Settings page, "OpenCode 账户与用量".
9
8
 
10
- ## 安装
9
+ - **Client**: one `settings.section`, refreshed every 60 s; accounts can be added, edited, reordered and probed;
10
+ model cards carry capability tags; both tables pan by dragging; the UI speaks 10 languages (browser language wins).
11
+ - **Host**: two endpoints — `GET /opencode-usage.json` (usage, `?refresh=1` forces a refresh) and
12
+ `GET|POST /opencode-quota-router.json` (pool and routing; actions below).
13
+
14
+ ## Install
11
15
 
12
16
  ```bash
13
17
  npm i dsh-opencode-routing
14
18
  ```
15
19
 
16
- 包自带 `cordis.patch.yml`:里面同时声明 `opencode-go` / `opencode-free` 两个 provider 和**一条** insert
17
- (`id: opencode-router`)。`usage:` 与 `router:` 两个子对象分别对应看板与路由各自的配置项(两边都有 `path`,
18
- 所以必须分开写)。
20
+ The package ships its own `cordis.patch.yml`: it declares the `opencode-go` / `opencode-free` providers and a
21
+ **single** insert (`id: opencode-router`). The `usage:` and `router:` sub-objects hold each half's settings (both
22
+ halves own a `path`, so they cannot share one flat namespace).
19
23
 
20
24
  ```yaml
21
25
  - insert:
@@ -34,98 +38,123 @@ npm i dsh-opencode-routing
34
38
  fallback: { provider: deepseek-account, model: deepseek-flash }
35
39
  ```
36
40
 
37
- 顶层键对两半都生效(`usageUrl`、`apiKeyRef`、`probeIntervalMs` 之类),子对象里的键只覆盖自己那一半。
41
+ Top-level keys apply to both halves (`usageUrl`, `apiKeyRef`, `probeIntervalMs`, …); keys inside a sub-object only
42
+ override that half.
43
+
44
+ ## Account pool and failover
45
+
46
+ Pool order is usage order. Exactly one account is "current" at any time, and its key is mirrored into the credential
47
+ reference the provider reads (default `OPENCODE_API_KEY`). Switching accounts therefore needs no provider config
48
+ change: `resolveApiKey` runs after the `agent/request` hook, on every streaming call.
38
49
 
39
- ## 账户池与轮替
50
+ - **A runs dry, B takes over**: on `QUOTA` / `ACCOUNT_QUOTA` / 429 once the official allowance is confirmed spent, or
51
+ on an auth-class error (`AUTH`, `INVALID_CREDENTIAL`, `MISSING_CREDENTIAL`, `ACCOUNT_SIGN_IN_REQUIRED`,
52
+ `CLIENT_UPGRADE_REQUIRED`), the current account is marked unusable (until the window resets, or for 30 minutes on
53
+ auth failure), routing moves to the next enabled account and **retries the same step** — the turn is never downgraded.
54
+ - **Single-model quarantine**: when only one model is out of allowance and the pool still has budget, only that model
55
+ is quarantined (30 minutes by default); every other model keeps using the plan.
56
+ - **Whole pool dry**: only then does the route fall back to its `fallback` (DeepSeek account by default), with a
57
+ zero-token `GET /inference/go/v1/usage` probe every 30 minutes (or at the rolling window reset) that returns to the
58
+ plan automatically.
59
+ - **Reset**: "重置封禁标记" in Settings clears every exhausted/auth mark and returns to the pool immediately. Accounts and
60
+ keys are untouched.
40
61
 
41
- 账户顺序就是使用顺序。任何时候只有一个「当前账户」,它的密钥会被镜像到 provider 读取的那个凭据引用
42
- (默认 `OPENCODE_API_KEY`),因此切换账户不需要改任何 provider 配置:`resolveApiKey` 是在
43
- `agent/request` 钩子之后、每次流式请求时现取的。
62
+ ## Usage dashboard
44
63
 
45
- - **A 用完自动用 B**:出现 `QUOTA` / `ACCOUNT_QUOTA` / 429 且官方额度确认耗尽,或出现鉴权类错误
46
- (`AUTH`、`INVALID_CREDENTIAL`、`MISSING_CREDENTIAL`、`ACCOUNT_SIGN_IN_REQUIRED`、`CLIENT_UPGRADE_REQUIRED`)时,
47
- 当前账户被标记为不可用(额度用尽到窗口重置,鉴权失败 30 分钟),路由切到下一个启用账户并在**同一步重试**,
48
- 不会把整轮对话降级。
49
- - **单模型隔离**:只有一个模型报耗尽、池里还有额度时,只隔离这个模型(默认 30 分钟),其它模型继续走套餐。
50
- - **整池耗尽**:才把这条路整体回退到 `fallback`(默认 DeepSeek 账户),并每 30 分钟(或 rolling 窗口重置时刻)
51
- 用零 token 的 `GET /inference/go/v1/usage` 探测,恢复后自动回到套餐。
52
- - **重置**:设置页的「重置封禁标记」清空所有「额度用尽 / 鉴权失败」标记并立刻回到账户池,不动账户与密钥。
64
+ The official `/inference/go/v1/usage` endpoint only returns rounded rolling / weekly / monthly percentages and
65
+ **no per-model breakdown**, while the plan meters each model separately (5 h = 20 % of the monthly allowance, week =
66
+ 50 %, month = 100 %). So the page shows two sets of numbers:
53
67
 
54
- ## 用量看板
68
+ - **Official percentages** (one line per account): straight from the API — real account usage, including other
69
+ clients and other machines.
70
+ - **Per-model money** (model table): a local estimate. It rescans `$DSH_HOME/sessions` for the last 40 days and only
71
+ counts `assistant/message` events whose provider starts with `opencode`, priced at the public rates (including
72
+ DeepSeek's 2× peak window). Usage from other tools has no local record.
55
73
 
56
- 官方 `/inference/go/v1/usage` 只给一组四舍五入的 rolling / weekly / monthly 百分比,**没有按模型明细**;
57
- 而套餐是按模型各自计额度(5 小时 = 月额度 20%、周 = 50%、月 = 100%)。所以页面有两套数:
74
+ "查看用量" opens a per-account dialog with the three windows, their reset times and the probe result (it probes that
75
+ account only and never switches models).
58
76
 
59
- - **官方百分比**(每个账户一行):来自接口,是账户真实用量,别的客户端、别的机器用了也算进去。
60
- - **按模型金额**(模型表):本机估算 —— 回扫 `$DSH_HOME/sessions` 近 40 天的会话日志,只统计 provider 以
61
- `opencode` 开头的 `assistant/message`,按公开单价(含 DeepSeek 峰谷 2×)折算。别的工具用掉的额度没有本地记录。
77
+ The peak card reports **both calendars side by side** — the official API and OpenCode — with the current period and how
78
+ long (in hours, one decimal) until it flips. Capability chips on the model cards carry a tooltip explaining what the tag
79
+ means, the header links to the OpenCode Console subscription page, and a floating arrow returns to the top once you have
80
+ scrolled down. Every price cell shows the cached-input, uncached-input and output line, so a cached hit (the green row)
81
+ can be compared directly with what the official API would charge for the same tokens.
62
82
 
63
- 「查看用量」按钮按账户弹窗展示三个窗口的百分比、重置时间与探测结果(只探测该账户,不切换模型)。
83
+ "编辑密钥" fetches the account's current key from `GET /opencode-quota-router.json?key=<account id>` and pre-fills the
84
+ input (a local-only endpoint; the list snapshot only ever carries a mask). Model names follow vendor casing
85
+ (`deepseek-v4.1-flash` → `DeepSeek V4.1 Flash`); vendors with a published brand mark render as an inline SVG, the rest
86
+ keep a letter badge. "下次探测" is a real timer (30 minutes by default) and the page re-renders every second, so it
87
+ counts down; opening the page only reads the snapshot and never triggers a probe.
64
88
 
65
- 「编辑密钥」直接用 `GET /opencode-quota-router.json?key=<账户 id>` 取回该账户的当前密钥并预填输入框
66
- (本机端点;列表快照始终只给掩码)。模型名按厂商习惯大写(`deepseek-v4.1-flash` → `DeepSeek V4.1 Flash`),
67
- 能取到品牌图标的厂商显示 SVG 图标,取不到的保留字母徽标。「下次探测」是真实的定时器(默认 30 分钟一次),
68
- 页面每秒重绘一次,所以会像倒计时一样走;打开页面本身只读快照,不会触发探测。
89
+ "此刻用哪个更划算" is derived from the subscription leverage: Go charges $10 for $100 of list-priced allowance
90
+ (≈ ¥710), so OpenCode always costs 10 % of the official price — 90 % cheaper. Real data never favours the official
91
+ API; the button in the card's corner switches to sample data so you can see what that case would look like.
69
92
 
70
- 「此刻用哪个更划算」按套餐杠杆算:Go 付 $10 拿到 $100 官方价额度(≈¥710),所以走 OpenCode 恒为官方价的
71
- 10%(便宜 90%)。真实数据里不会出现官方更便宜,卡片右下角的按钮可以切到示例数据,看那种情况长什么样。
93
+ The capability column comes from the same catalog endpoint (`/inference/go/v1/models`): `modalities.input` and
94
+ `capabilities` are rendered as text / image / video / audio / PDF, tool calls, reasoning, plus context length.
95
+ Zero-priced models show "无限" (unlimited).
72
96
 
73
- 模型表的「能力」列来自同一个模型目录接口(`/inference/go/v1/models`)的
74
- `modalities.input` 与 `capabilities`,标出 文本 / 图像 / 视频 / 音频 / PDF、工具调用、推理,以及上下文长度;
75
- 价格为 0 的模型显示「无限」。
97
+ ### Peak / off-peak and holidays
76
98
 
77
- ### 峰谷与节假日
99
+ DeepSeek's off-peak window is everything outside Beijing time Mon–Fri 09:00–12:00 and 14:00–18:00 (lunch break,
100
+ nights, weekends and **statutory holidays** included) at exactly half the peak price. OpenCode only treats UTC Mon–Fri
101
+ 01–04 and 06–10 as peak and **ignores Chinese statutory holidays** (anomalyco/opencode#53071, maintainer:
102
+ *"Go pricing follows our own schedule, not DeepSeek's holiday calendar."*).
78
103
 
79
- 官方 API 的空闲时段是北京周一至周五 09:00–12:00、14:00–18:00 之外(含午休、夜间、周末、**法定节假日**),
80
- 价格正好是峰价一半。OpenCode 只照 UTC 周一至周五 01–04、06–10 两个窗口算峰,**不认中国法定节假日**
81
- (anomalyco/opencode#53071 维护者原话:*"Go pricing follows our own schedule, not DeepSeek's holiday calendar."*)。
104
+ The plugin ships the 2026–2031 Chinese statutory holiday table. It is only applied to the official side when
105
+ `holidayAware: true` is set (`usage.holidayAware`); it defaults to off because OpenCode keeps charging peak on those
106
+ days. The table lives in `usage.js` as `HOLIDAYS`, and `isChinaHoliday()` can be called on its own.
82
107
 
83
- 插件里带 2026–2031 的中国法定节假日表,只有把 `holidayAware: true` 写进配置(`usage.holidayAware`)才会计入
84
- 官方侧折算;默认关闭,因为 OpenCode 侧照旧按峰计费。表在 `usage.js` 的 `HOLIDAYS`,`isChinaHoliday()`
85
- 可单独调用。
108
+ The subscription price and its allowance can be overridden with `usage.planPriceUsd` / `usage.planAllowanceUsd`; the
109
+ discount on the page is derived from those two numbers, so a plan change from OpenCode is a config edit, not a patch.
86
110
 
87
- ## 设置接口
111
+ ## Settings API
88
112
 
89
- `GET /opencode-quota-router.json` 返回 `{mode, reason, active, accounts[], quarantine[], routes[], lastFailure}`,
90
- 账户里的密钥永远是掩码(`hint`)。`POST` 同一路径,body `{action, ...}`:
113
+ `GET /opencode-quota-router.json` returns `{mode, reason, active, accounts[], quarantine[], routes[], lastFailure}`;
114
+ account keys are always masked (`hint`). `POST` to the same path with `{action, ...}`:
91
115
 
92
- | action | 参数 | 作用 |
116
+ | action | arguments | effect |
93
117
  | --- | --- | --- |
94
- | `add` | `label?`, `key` | 新建账户(密钥≥8 字符),写入凭据库并探测 |
95
- | `update` | `id`, `label?` / `enabled?` / `key?` | 改名、停用/启用、换密钥 |
96
- | `remove` | `id` | 删除账户并清掉它的密钥(至少留一个) |
97
- | `activate` | `id` | 立刻切到该账户 |
98
- | `move` | `id`, `direction: up\|down` | 调整顺序 |
99
- | `probe` | `id?` | 探测全部账户;带 `id` 只探测这一个 |
100
- | `reset` | — | 清空封禁标记,回到账户池 |
118
+ | `add` | `label?`, `key` | create an account (key ≥ 8 chars), store it and probe |
119
+ | `update` | `id`, `label?` / `enabled?` / `key?` | rename, disable/enable, rotate the key |
120
+ | `remove` | `id` | delete an account and its key (one is always kept) |
121
+ | `activate` | `id` | switch to it now |
122
+ | `move` | `id`, `direction: up\|down` | reorder |
123
+ | `probe` | `id?` | probe every account, or just one |
124
+ | `reset` | — | clear the exhausted/auth marks and return to the pool |
101
125
 
102
- `GET /opencode-usage.json` 返回 `{plan, configuredPlan, detectedPlan, holidayAware, peak, usage, accounts[],
103
- activeAccount, models[], totals}`;`?refresh=1` 先强制刷新官方快照再返回。
126
+ `GET /opencode-usage.json` returns `{plan, configuredPlan, detectedPlan, holidayAware, peak, usage, accounts[],
127
+ activeAccount, models[], totals}`; `?refresh=1` forces a fresh official snapshot first.
104
128
 
105
- ## 多语言
129
+ ## Languages
106
130
 
107
- 界面文案在 `client.js` 的 `DICT` 里:`zh`(源)、`zh-tw`、`en`,以及 `es` / `fr` / `de` / `pt` / `ja` / `ko` / `ru`
108
- (由 `_merge_i18n.mjs` 从翻译文件内联,缺键回退到 `en`,再回退到键名)。`localeOf()` 按浏览器语言前缀匹配,
109
- 中文区分简繁。
131
+ UI strings live in `DICT` in `client.js`: `zh` (source), `zh-tw`, `en`, plus `es` / `fr` / `de` / `pt` / `ja` / `ko` /
132
+ `ru` (inlined from translation files by `scripts/merge-i18n.mjs`; a missing key falls back to `en`, then to the key
133
+ itself). `localeOf()` matches the browser language prefix and separates Simplified from Traditional Chinese.
110
134
 
111
- ## 测试
135
+ ## Tests
112
136
 
113
137
  ```bash
114
138
  node test.mjs
115
139
  ```
116
140
 
117
- 不需要任何模型额度:假 context、假凭据库、假 webServer,本地起一个 usage 端点按 API key 返回不同百分比,
118
- 覆盖组合插件挂载、账户收养、单模型隔离、A→B 切换与密钥镜像、按账户探测、整池耗尽回退、重置、设置动作校验、
119
- 用量视图(能力标签、无限额度、峰谷换算)与单账户退化。
141
+ No model quota is required: fake context, fake credential store, fake webServer, and a local usage endpoint that
142
+ answers per API key. Coverage: composite mounting, account adoption, single-model quarantine, A→B failover with key
143
+ mirroring, per-account probing, whole-pool fallback, reset, settings-action validation, the usage view (capability
144
+ tags, unlimited models, peak/off-peak maths, billing lines) and the single-account degradation.
145
+
146
+ ## Relation to the old packages
147
+
148
+ `dsh-opencode-quota-router` and `dsh-opencode-usage-settings` are its predecessors; everything they did is merged
149
+ here. Both are marked deprecated on npm and point at this package. Repositories:
150
+ [`2869949486/dsh-opencode-routing`](https://github.com/2869949486/dsh-opencode-routing) (current),
151
+ [`dsh-opencode-quota-router`](https://github.com/2869949486/dsh-opencode-quota-router),
152
+ [`dsh-opencode-usage`](https://github.com/2869949486/dsh-opencode-usage).
120
153
 
121
- ## 与旧包的关系
154
+ ## Contributing
122
155
 
123
- `dsh-opencode-quota-router` 与 `dsh-opencode-usage-settings` 是它的前身,功能已全部并入本包;
124
- 旧包在 npm 上标记为 deprecated,指向这里。仓库分别是
125
- [`2869949486/dsh-opencode-routing`](https://github.com/2869949486/dsh-opencode-routing)(当前)、
126
- [`dsh-opencode-quota-router`](https://github.com/2869949486/dsh-opencode-quota-router)、
127
- [`dsh-opencode-usage`](https://github.com/2869949486/dsh-opencode-usage)。
156
+ Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Run `node test.mjs` before opening a PR.
128
157
 
129
158
  ## License
130
159
 
131
- MIT
160
+ MIT — see [LICENSE](LICENSE).