dsh-any-deepseek 0.0.16 → 0.0.18

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,149 @@
1
+ # 参与改进 dsh-any-deepseek
2
+
3
+ 这是个实用小工具,改动从「一个字段的文案」到「接一家新渠道」都欢迎。
4
+
5
+ **先读 [README.md](README.md)** —— 那里讲的是怎么用;这一份讲怎么改。
6
+
7
+ ---
8
+
9
+ ## 最有价值的三种贡献
10
+
11
+ ### 1. 接一个新渠道
12
+
13
+ 内置 5 家,接别家通常只改 [`src/presets.ts`](src/presets.ts) 一个文件。
14
+
15
+ ```ts
16
+ 'your-relay': {
17
+ displayName: 'Your Relay',
18
+ api: 'openai-completions',
19
+ baseURL: 'https://your-relay.example/v1',
20
+ // 🔴 用**这家自己的**拼写。各家模型名不同:
21
+ // deepseek-v4.1-flash / deepseek/deepseek-v4.1-flash / deepseek-flash
22
+ // 抄错不会在本地报错 —— 会在第一次真请求时被上游拒。
23
+ model: 'deepseek-v4.1-flash',
24
+ ...V41_FLASH_CAPS,
25
+ // 可选:这家有没有余额接口、是哪种形状(见 src/balance.ts 的四种 shape)
26
+ // balance: { shape: 'openai-billing' },
27
+ },
28
+ ```
29
+
30
+ 加完这一条,面板的规格下拉里就有它了 —— 不需要改别处。
31
+
32
+ **三条硬规矩**(都是付过代价的):
33
+
34
+ 1. **注释里不要写「我探过了」,除非真的探过。**
35
+ 这个仓里曾经有一条 `Probed live: the billing pair answers` 的注释,而那个路由**从没被调用过**,
36
+ 第一次真调用就 404。**假证据比没有注释更坏** —— 它让下一个读者不再去查。
37
+
38
+ 2. **`model` 填上游的拼写,不是我们的 id。**
39
+ 我们对外只暴露 `deepseek-v4.1-flash` 一个 id,各家的拼写由预设吸收。
40
+ **这正是这个插件存在的理由** —— 所以别在预设里图省事写我们的 id。
41
+
42
+ 三种拼写的真实例子(都在仓内):
43
+
44
+ | 渠道 | 它要的写法 | 说明 |
45
+ |---|---|---|
46
+ | OpenCode Go / Ollama / Open Design | `deepseek-v4.1-flash` | 中转站的写法 |
47
+ | DeepSeek Official | `deepseek-flash` | 官方自己的短拼写 |
48
+ | **Tencent TokenHub** | **`deepseek-v4-flash-202605`** | **`-202605` 是 id 的一部分,不是日期** |
49
+
50
+ ⚠️ 最后那一行是最典型的坑:把后缀"规整"掉看起来像在清理,实际是让每条请求都被上游拒。
51
+
52
+ 3. **没有余额接口就写「没有」,别猜一个形状。**
53
+ 判法用状态码,不用猜:不带 key 时,**真路由回 401**(存在、需鉴权),**假路径回 404**。
54
+ 然后逐个探候选路径 —— 全 404 就是真的没有。此时**不声明 `balance`**,
55
+ 面板会自动退回「测通」模式(发一次最小真实请求,成功就显示「余额未尽」)。
56
+ 反面教材:仓里 open-design 那条曾声明过 `openai-billing` 并注释"已探过",实际每次 404。
57
+
58
+ ### 2. 修一处文案
59
+
60
+ 面板上的每一句都应该是一个**能在页面上验证的事实**。读到含糊、过时、或替宿主猜原因的,直接改。
61
+
62
+ 判断标准(本项目一直在用):
63
+
64
+ - ❌ 「已安全保存」—— 读者无法验证。
65
+ - ✅ 「存在 `channels.<id>.apiKey`,会出现在 `--dump-config` 输出里」—— 可验证。
66
+ - ❌ 「只读部署,或设置服务未启用」—— 替宿主猜了两个**没查过**的原因。
67
+ - ✅ 「宿主拒绝了这个修改」+ 宿主自己的错误信息 —— 只说知道的事。
68
+
69
+ ### 3. 报一个「装了但没反应」
70
+
71
+ **静默失效是本项目最贵的一类缺陷** —— 装了不生效、写了没保存、功能存在但不可达,
72
+ 全都零报错。复现步骤比任何猜测都值钱。
73
+
74
+ 这类问题通常有这几个藏身处,如果你能指出是哪一处,修起来会快很多:
75
+
76
+ | 现象 | 可能的位置 |
77
+ |---|---|
78
+ | 面板整块不出现,控制台零错误 | `SETTINGS_NS` 与 `cordis.patch.yml` 的条目 id 不一致 |
79
+ | 面板在,但每次请求都失败、表格显示「还没有渠道」 | `inject` 里少了 `webServer`,路由被静默跳过 |
80
+ | 设置写不进去,但看不出原因 | schema 少了 `.volatile()`,或写入被 `.catch` 吞了 |
81
+ | provider 装了但模型选择器里没有 | 注册元数据(`inputModalities` / `reasoning`)没声明 |
82
+
83
+ ## 加一种余额形状(少见)
84
+
85
+ 如果某家有余额接口但形状是新的,改 [`src/balance.ts`](src/balance.ts):
86
+
87
+ 1. 给 `BalanceProbe.shape` 加一个值;
88
+ 2. 写一个 `parseXxx()`;
89
+ 3. 在 `balanceUrls()` 里给它路径;
90
+ 4. 在 `readBalance()` 里分发。
91
+
92
+ `balanceUrls()` 是**唯一**知道路径的地方 —— 这样预设就不可能和传输层意见相左。
93
+
94
+ 还有一条底线:**缺数字就报缺,绝不填 0。**
95
+ 「读不到」和「是空的」导致相反的行动,而只有一个是真的。
96
+
97
+ ## 开发
98
+
99
+ ```bash
100
+ pnpm install
101
+ pnpm run typecheck # 两个 tsconfig:node 半 + client 半
102
+ pnpm run test # 纯函数层,不需要装上 harness
103
+ pnpm run build # lib/index.js + lib/client.js
104
+ ```
105
+
106
+ 纯函数层(`presets` / `channels` / `route` / `usage-ledger` / `balance`)不 import harness,
107
+ 所以单测跑得很快。适配器与面板只通过宿主的公开接缝和它说话。
108
+
109
+ ## 判据必须承重
110
+
111
+ **每条新判据都要附一次「把它弄红」的实验结果。**
112
+
113
+ 把修复回滚,那条用例必须红;否则它只是装饰。这个仓的 CHANGELOG 里每个结论都带着它的负控制读数
114
+ (例如「去掉 `percent` 取反 ⇒ 精确红 3 条」)。
115
+
116
+ 三种特别容易写成**假判据**的形态,本项目都踩过:
117
+
118
+ 1. **判据与被判对象同源**。例:`bump` 改的是 CHANGELOG 首个标题,而 `version` 只读首个标题 ⇒
119
+ 永远读绿。要抓这类缺陷,判据必须碰**别的不变式**(现在是「集合包含」:原有每个标题都在,且恰好多一个)。
120
+ 2. **假传输层掩盖走错协议**。测试里传输层是假的,走错协议照样「通」。
121
+ 分发决策要用**结构判据**钉住(断言 `api: apiImplementations()` 存在、旧形态不回来)。
122
+ 3. **自写抽取器出错时不喊**。手写的 YAML 正则抽空了,用例照样绿。
123
+ ⇒ 抽取要**如实落盘原文**再看,不要在抽取器里顺手「清洗」。
124
+
125
+ ## 截图怎么更新
126
+
127
+ README 里的三张图来自 `scripts/screenshot-harness.tsx`,它渲染**真实面板组件**
128
+ (假数据、真 CSS/JSX/文案)—— 面板本身在 token 保护的 Web UI 后面,干净 profile 打开是空白页,
129
+ 所以直拍拿不到。
130
+
131
+ ```bash
132
+ msm("any-deepseek-develop", ["shot"]) # 打包成单文件页面
133
+ ```
134
+
135
+ 生成的页面在 `_tmp/shot/screenshot-harness.html`,用浏览器打开后截图即可。
136
+ 加 `?variant=add` 可以拍「添加渠道」表单那一种状态。
137
+
138
+ > ⚠️ 打包时有两个坑,都会让页面**空白且零报错**:产物里残留 `import`(内联模块无法解析)、
139
+ > 或外部 `<script src>` 在 `file://` 下被 CORS 拒。`shot` 已自带「不许有残留 import」的判据。
140
+
141
+ ## 发布
142
+
143
+ 维护者用的开发通道是 CCC 的 MSM `any-deepseek-develop`(safe-mode 下没有 bash)。
144
+ `stage check` 是「能不能装」的唯一判据 —— **`npm publish` 的退出码 0 不代表可安装**,
145
+ 新版本要一两分钟后才在 registry 上出现。
146
+
147
+ ## 许可
148
+
149
+ MIT
package/README.en.md CHANGED
@@ -1,277 +1,217 @@
1
1
  # dsh-any-deepseek
2
2
 
3
- Collapse several DeepSeek relays into **one** provider with **one** model: paste a key per channel
4
- in the settings panel, click to switch.
3
+ **Use DeepSeek credit from several providers inside DeepSeek Harness — with one extra row in the
4
+ model list, not N.**
5
5
 
6
- A small open-source tool (MIT). Fork it, add a relay, improve it — see
7
- [Contributing](#contributing).
6
+ You bought DeepSeek credit in several places: the official API, a relay, a cloud vendor, a cheap
7
+ one a friend recommended. This plugin collects them into **one** provider: paste a few keys, click
8
+ to switch — and usage is booked per real upstream, so you can see what each one cost this month.
8
9
 
9
- [中文](README.md) · English (this page)
10
+ ![Channel list](docs/screenshot-channels.png)
10
11
 
11
- ![Channels tab](docs/screenshot-channels.png)
12
+ [中文](README.md) · English (this page)
12
13
 
13
14
  ---
14
15
 
15
- ## What problem this solves
16
+ ## Before you install: is this for you?
16
17
 
17
- You have DeepSeek credit in several places: the official API, a relay, a cloud vendor, a
18
- colleague's cheap recommendation. In DeepSeek Harness, that would normally become **N entries in
19
- the model picker**, and every switch means editing configuration, changing the model id, changing
20
- the key — plus you have to remember yourself which relay spent how many tokens this month.
18
+ **Yes**, if any of these is true:
21
19
 
22
- After installing this plugin:
20
+ - You have **more than one** DeepSeek endpoint and want to switch between them without editing
21
+ configuration.
22
+ - You want **per-channel usage accounting** instead of one blended total.
23
+ - You use relays whose model ids differ (`deepseek-v4.1-flash` / `deepseek/deepseek-v4.1-flash` /
24
+ `deepseek-flash`) and don't want to keep track yourself.
23
25
 
24
- - The model picker has **one row**: `any-deepseek / deepseek-v4.1-flash`.
25
- - Any number of **channels** hang off it; each is an endpoint + a key + a display name.
26
- - Click a channel's dot and requests go there. **No automatic switching** (see
27
- [Why no failover](#why-no-failover)).
28
- - Usage is booked **per real upstream**.
26
+ **No**, if:
29
27
 
30
- ```
31
- ┌── channel A (official) ← currently selected
32
- composer ──────►│
33
- │ ├── channel B (a relay)
34
- ▼ └── channel C (another)
35
- any-deepseek
36
- / deepseek-v4.1-flash
37
- (the only model exposed)
38
- ```
28
+ - You have a single endpoint and it is the official one — the built-in `deepseek-official` already
29
+ covers you; this plugin is unnecessary.
30
+ - You want **automatic failover** (one endpoint dies, requests move to the next). v1 does not do
31
+ that — see [Why no failover](#why-no-failover).
32
+ - You want to **switch between models**. This is "one model, many sources", not a model router.
39
33
 
40
34
  ## Install
41
35
 
36
+ Requires DeepSeek Harness to be installed already. In a terminal:
37
+
42
38
  ```bash
43
39
  dsh plugin --profile web add dsh-any-deepseek
44
40
  ```
45
41
 
46
42
  Then **restart DSH** and open Settings → **Any DeepSeek**.
47
43
 
48
- > **A first install requires a restart**: the profile's layer list changed, and the composition
49
- > graph is built at boot. Editing an already-installed plugin's client half reloads in the
50
- > browser; its node half does not.
51
- >
52
- > `dsh plugin` forwards its arguments to pnpm, so `add <spec>` accepts a registry name, a tarball
53
- > path, or a local directory (installed as a symlink).
44
+ > **Why the restart**: the plugin is added to the profile's load list, which is read only at
45
+ > startup. Afterwards, changing this plugin's UI half needs only a browser refresh — but the
46
+ > **first install** needs the restart.
54
47
 
55
- ## Usage
48
+ ## First run: three steps
56
49
 
57
- ### Add a channel
50
+ ### 1. Add a channel
58
51
 
59
- Click **添加渠道**. Only two things are required:
52
+ Click **添加渠道** (Add channel). Two fields are required:
60
53
 
61
- | Field | Required | Meaning |
62
- |---|---|---|
63
- | **规格** (preset) | ✅ | Determines endpoint, upstream model id, and protocol — so you don't type them. |
64
- | **渠道 id** (channel id) | ✅ | The configuration key, `[a-z0-9][a-z0-9_-]*`. The same preset can be added twice; the second becomes `-2`. |
65
- | **显示名** (display name) | — | Display only. Tells two accounts on the same relay apart. **Renaming never touches the id.** |
66
- | **密钥** (key) | ✅ | The key itself. There is no name to invent. |
54
+ ![Add channel](docs/screenshot-add-channel.png)
67
55
 
68
- Bundled presets: **DeepSeek Official**, **OpenCode Go**, **Ollama Cloud**, **Open Design**,
69
- **CommandCode**. For another relay, see [Adding a channel](#adding-a-channel).
56
+ | Field | What it means |
57
+ |---|---|
58
+ | **规格** (preset) | Pick your provider. It determines the endpoint, model id, and protocol — so you don't type them. |
59
+ | **密钥** (key) | The key that provider gave you. Paste it. There is no name to invent. |
70
60
 
71
- ### Switch channels
61
+ **渠道 id** (channel id) is filled in for you — it is just the configuration key, and you can
62
+ change it. **显示名** (display name) defaults to the preset name; when you run two accounts on the
63
+ same provider, set something like "main" / "backup".
72
64
 
73
- Click the dot at the left of a row. The current channel is marked three ways — a colour bar down
74
- the left edge, a stronger border, and a tinted fill — plus `aria-current`.
65
+ Bundled presets: DeepSeek Official, OpenCode Go, Ollama Cloud, Open Design, CommandCode.
66
+ For another provider, see [Adding a provider](#adding-a-provider).
75
67
 
76
- **Switching invalidates the prompt cache for the running conversation**, so switch at a natural
77
- boundary or start a new session.
68
+ ### 2. Make sure it is selected
78
69
 
79
- ### Remaining balance
70
+ The first channel you add **becomes the current channel automatically** — its row gets a radio
71
+ dot, a colour bar down the left edge, and a 「当前」 badge.
80
72
 
81
- Click **查询余额** (on demand; nothing is scanned when the panel opens). Four outcomes are kept
82
- strictly apart, because each calls for a different action:
73
+ To switch later: click the dot at the left of a row.
83
74
 
84
- | Shown | Meaning |
85
- |---|---|
86
- | `642.44 CNY` / `55.48 USD` | An amount was read |
87
- | `rolling 100% · weekly 81% · monthly 75%` | This relay reports **usage windows**, not money |
88
- | **余额未尽** (no balance API; verified live: …) | No balance API, but a live request **succeeded** ⇒ the account is not blocked for lack of credit. **Not an amount** (see below) |
89
- | 不支持查询 / 读取失败 / 未配置密钥 | Three different kinds of "nothing" |
75
+ ### 3. Start using it
90
76
 
91
- > ⚠️ **"Not exhausted" is not "how much is left".** It is the result of one minimal real request
92
- > (a token or two) succeeding, which proves exactly three things: the endpoint is serving, the key
93
- > works, and the account is not blocked for lack of credit (an exhausted account answers
94
- > 402/quota, not 200). So the panel says only 「未尽」 and **never fabricates an amount** —
95
- > printing `0.00` would read as "you are out of credit", the opposite of the truth.
77
+ In the model picker, choose **DeepSeek V4.1 Flash** (the provider is `any-deepseek`).
78
+ Every request then goes to whichever channel you selected.
96
79
 
97
- > ⚠️ **A missing number is never rendered as `0`.** "We could not read it" and "it is empty" lead
98
- > to opposite actions.
80
+ ## Three things you will actually do
99
81
 
100
- ### Usage
82
+ ### Switch channels
101
83
 
102
- ![Usage tab](docs/screenshot-usage.png)
84
+ Click the dot at the left of a row.
103
85
 
104
- One row per channel, groupable by channel or by day.
86
+ > ⚠️ **Switching invalidates the prompt cache for the running conversation**, so switch at a topic
87
+ > boundary, or just start a new session.
105
88
 
106
- **Counting convention** (the panel prints it too): harness token counts are **disjoint** —
107
- **input** is uncached input only, cache reads and writes are separate columns, and the three
108
- together are the **billable input**. Treating "input" as the total makes it look under-reported.
89
+ ### See how much credit is left
109
90
 
110
- **Speed** counts generation time only (total minus time-to-first-token). Blending queueing +
111
- first-token wait + generation into one number makes "slow to start" and "slow throughout" read
112
- identically, and those two call for opposite fixes. `—` means unmeasured, **not zero**.
91
+ Click **查询余额** (Check balance). It does not run when the panel opens — each check calls every
92
+ provider's server.
113
93
 
114
- ## Configuration
94
+ Different providers can report different things, and the panel keeps them apart:
115
95
 
116
- Everything lives under one settings namespace — the **profile entry id** from
117
- `cordis.patch.yml`, which is what the panel binds to:
96
+ | What you see | Meaning |
97
+ |---|---|
98
+ | `642.44 CNY`, `55.48 USD` | This provider reports an amount |
99
+ | `rolling 100% · weekly 81% · monthly 75%` | This provider reports **subscription usage percentages**, not money |
100
+ | **余额未尽** (verified live: …) | This provider has **no balance API**, but a minimal live request **succeeded** ⇒ the account is not blocked |
101
+ | 不支持查询 / 读取失败 / 未配置密钥 | Three different kinds of "nothing" |
118
102
 
119
- ```yaml
120
- dsh-any-deepseek:
121
- current: opencode-go # the live channel
122
- channels:
123
- opencode-go:
124
- apiKey: sk-… # the key itself
125
- open-design:
126
- preset: open-design
127
- label: Open Design (main) # optional display name
128
- usage:
129
- enabled: true
130
- ```
103
+ > **"Not exhausted" is not "how much is left".** One successful request proves exactly three
104
+ > things: the endpoint is reachable, the key is valid, and the account is not blocked for lack of
105
+ > credit (an exhausted account answers 402, not 200). So the panel says only 「未尽」 and **never
106
+ > invents an amount**.
131
107
 
132
- `channels.<id>` may be either:
108
+ ### See what this month cost
133
109
 
134
- - **preset-backed** — `preset: opencode-go`, plus optional overrides; or
135
- - **fully declared** — `baseURL` + `model` required, `api` defaults to `openai-completions`,
136
- and `contextWindow` / `maxTokens` fall back to documented defaults (the panel shows ⚠ on the
137
- row when a default was applied rather than declared).
110
+ Switch to the **用量** (Usage) tab.
138
111
 
139
- ### Where the key lives (please read this part)
112
+ ![Usage](docs/screenshot-usage.png)
140
113
 
141
- `apiKey` holds the key **itself**, on the channel. The panel never asks you to invent a name for
142
- it — you paste the key and it is written here.
114
+ Group by channel or by day. "Input" counts only cache misses; cache reads and writes are separate
115
+ columns, and the three together are the billable input. In the speed column, `—` means
116
+ **unmeasured — not zero**.
143
117
 
144
- ⚠️ **That means the key is in your profile configuration.** `dsh --dump-config` prints patch
145
- layers verbatim and does **not** redact, so a diagnostic dump or a `git diff` of the profile
146
- includes it. **Do not share the profile document.** Environment-variable injection
147
- (`-e SOME_KEY=…`) is not consulted — nothing resolves a name.
118
+ ## Is my key safe
148
119
 
149
- > An earlier version hid the key behind a "credential reference name" and told you to set it "in
150
- > the credential centre" — a UI that does not exist. The current design is one field, one store,
151
- > one write.
120
+ **It is stored in your local configuration file, in plaintext.**
152
121
 
153
- `apiKeyEnv` is still **read**, for configurations written before this change: when its value
154
- matches the reference-name grammar (`^[A-Za-z_][A-Za-z0-9_]*$`) it is resolved as a name; when it
155
- does not, it cannot be one, so it is adopted as the key itself. `apiKey` always wins when both are
156
- present, so re-entering the key once in the panel migrates an entry.
122
+ The path is `channels.<channel-id>.apiKey` inside the profile. The plugin never sends it to any
123
+ third party, but:
157
124
 
158
- A channel with no key **still appears** in the panel — that is how you find and fix it. Requests
159
- through it fail with `MISSING_CREDENTIAL`, and its row shows a hollow dot.
125
+ > ⚠️ `dsh --dump-config` prints the configuration verbatim and does **not** redact it. So:
126
+ > **do not share your profile configuration**, and do not paste its `git diff` anywhere public.
160
127
 
161
- ### Protocols
128
+ If you need to share config for troubleshooting, delete the `apiKey` lines first.
162
129
 
163
- `openai-completions` is the default and the recommendation: almost every relay speaks it, it
164
- exposes the largest compatibility surface (~20 switches), and its usage normalization already
165
- reconciles the three cache-token spellings relays use
166
- (`prompt_tokens_details.cached_tokens`, `prompt_cache_hit_tokens`, top-level `cached_tokens`).
130
+ ## Troubleshooting
167
131
 
168
- Set `api` on one channel only when that relay speaks something else (`openai-responses`,
169
- `anthropic-messages`). The panel never changes protocol behind your back.
132
+ **The model list has no DeepSeek V4.1 Flash.**
133
+ → You have not restarted since installing. Restart DSH.
170
134
 
171
- ### Thinking level
135
+ **Settings has no "Any DeepSeek".**
136
+ → Same; or check you installed into the `web` profile: `dsh plugin --profile web list`.
172
137
 
173
- Each channel declares its selectable thinking levels, and the level picked in the harness is
174
- passed through to the relay:
138
+ **`MISSING_CREDENTIAL`.**
139
+ → That channel has no key yet. Its status dot is hollow; click 「填写密钥」 to add one.
175
140
 
176
- ```
177
- harness reasoningEffort → pi-ai ThinkingLevel → model.thinkingLevelMap → wire value
178
- ```
141
+ **`NO_CHANNEL_SELECTED`.**
142
+ → No channel is selected. The panel shows a hint at the bottom; click any row's dot.
179
143
 
180
- **There is no second mapping layer in this plugin**: the `thinkingLevelMap` the transport applies
181
- **is** the mapping, and a second one would drift from it. A level declared `null` is unsupported
182
- and is not offered in the picker.
144
+ **「查询余额」 shows 「读取失败」.**
145
+ → That provider did not answer properly just now; retry. If it never works, your network may not
146
+ reach it.
183
147
 
184
- ### Image input
148
+ **401 / 403 / out of credit.**
149
+ → The key is wrong or expired, or that account really is out of money.
185
150
 
186
- Supported. A channel can send and receive images, which requires declaring `input` with `image` —
187
- during **model registration** (`inputModalities`), not in the request path. This was a real
188
- defect: capability is a property of the provider, not of a channel or a single request.
151
+ **Answers got slower or worse after switching.**
152
+ → Providers do not behave identically. Use the 测试 (Test) button per channel, and the 用量 tab to
153
+ compare speeds.
189
154
 
190
155
  ## Why no failover
191
156
 
192
- v1 is deliberately manual. A failing channel reports its own error; it does not silently fall over
193
- to another:
157
+ A failing channel reports its own error; it never silently moves to another. Three reasons:
194
158
 
195
159
  - Silent failover makes "who answered this?" unknowable, while the usage table honestly records
196
- the other relay — two parts of the UI would disagree.
160
+ the other provider — the two parts would disagree.
197
161
  - Switching mid-conversation invalidates the prompt cache, costing more than the failure saved.
198
- - Failure is information. Hiding it means you never learn a relay is degrading.
199
-
200
- ## Adding a channel
201
-
202
- This is a **small practical plugin**, and adding a relay is the most valuable contribution —
203
- usually it touches one file.
204
-
205
- ### Add a preset (the common case)
206
-
207
- Edit [`src/presets.ts`](src/presets.ts) and add an entry to `CHANNEL_PRESETS`:
208
-
209
- ```ts
210
- 'your-relay': {
211
- displayName: 'Your Relay',
212
- api: 'openai-completions',
213
- baseURL: 'https://your-relay.example/v1',
214
- // 🔴 Use THIS relay's spelling. Model ids differ per relay:
215
- // deepseek-v4.1-flash / deepseek/deepseek-v4.1-flash / deepseek-flash …
216
- // A wrong spelling does not error here — the upstream rejects it on the first request.
217
- model: 'deepseek-v4.1-flash',
218
- ...V41_FLASH_CAPS,
219
- // Optional: does this relay expose a balance surface, and which shape?
220
- // balance: { shape: 'openai-billing' },
221
- },
222
- ```
162
+ - Failure is information. Hide it and you never learn a provider is degrading.
223
163
 
224
- That is enough — the preset picker gains the entry automatically.
164
+ ## Adding a provider
225
165
 
226
- ⚠️ **Two rules** (both from real costs):
166
+ Five are bundled; adding another usually touches one file. See
167
+ **[CONTRIBUTING.md](CONTRIBUTING.md)**.
227
168
 
228
- 1. **Do not write "probed live" in a comment unless you actually probed it.** This repo once
229
- carried a `Probed live: the billing pair answers` comment for a route that had never been
230
- called; the first real call returned 404. **A false claim is worse than no comment**, because
231
- it stops the next reader from checking.
232
- 2. **`model` is the upstream spelling, not our id.** We expose exactly one id,
233
- `deepseek-v4.1-flash`; per-relay spellings are absorbed by presets — that is the whole point
234
- of this plugin.
169
+ ## Configuration reference (advanced)
235
170
 
236
- ### Add a balance shape (rarer)
171
+ Everything lives under the `dsh-any-deepseek` namespace:
237
172
 
238
- If a relay has a balance API with a new shape, edit [`src/balance.ts`](src/balance.ts): add a
239
- `shape`, add a `parseXxx()`, give it a path in `balanceUrls()`, and dispatch it in `readBalance()`.
240
- `balanceUrls()` is the **only** place that knows the paths, so a preset cannot disagree with the
241
- transport.
242
-
243
- ### Gates (run before committing)
244
-
245
- ```bash
246
- pnpm run typecheck # two tsconfigs: node half + client half
247
- pnpm run test # pure layers only; no harness needed
248
- pnpm run build # lib/index.js + lib/client.js
173
+ ```yaml
174
+ dsh-any-deepseek:
175
+ current: opencode-go # the current channel
176
+ channels:
177
+ opencode-go:
178
+ apiKey: sk-… # the key itself
179
+ open-design:
180
+ preset: open-design # use a bundled preset
181
+ label: Open Design (main) # optional display name
182
+ my-relay:
183
+ baseURL: https://relay.example/v1 # or declare it fully
184
+ model: deepseek-v4.1-flash
185
+ apiKey: sk-…
186
+ usage:
187
+ enabled: true
249
188
  ```
250
189
 
251
- Changes must come with a test **that can go red**. The rule in this repo is that **a judgement must
252
- carry weight** — revert the fix and the test must fail; otherwise it is decoration. Every
253
- significant claim in the code carries its negative-control result.
190
+ **Protocol**: defaults to `openai-completions` (almost every relay speaks it). Set `api:
191
+ openai-responses` or `api: anthropic-messages` on **that one channel** only when it really speaks
192
+ something else.
254
193
 
255
- ## Known boundaries
194
+ **Thinking level**: each channel declares which levels it supports, and the level you pick in the
195
+ UI is the one sent upstream. Unsupported levels are not offered in the picker.
256
196
 
257
- - Does not modify the harness, and does not replace `deepseek-official` / `llm-pi-ai` routes.
258
- - Exposes exactly one model id. Upstream ids differ per relay and are mapped in presets.
259
- - **No cost estimation** (there is no price table).
260
- - **No automatic failover** (v1, see above).
261
- - **No multi-model aggregation** — this is "one model, many sources", not a model router.
197
+ **Images**: sending and receiving images is supported.
262
198
 
263
- ## Contributing
199
+ **Legacy config**: early versions stored the key in `apiKeyEnv`. That field is still read — a value
200
+ that looks like an environment-variable name is resolved as one, otherwise it is used as the key
201
+ itself. `apiKey` always wins, so re-entering the key once in the panel migrates the entry.
264
202
 
265
- Fork, file an issue, send a PR. Especially welcome:
203
+ ## Boundaries
266
204
 
267
- - **Add a relay** — one file, see above.
268
- - **Fix a sentence** — every line in the panel is supposed to be a verifiable fact. If one is
269
- vague or stale, change it.
270
- - **Report "installed but nothing happens"** — silent failure is the most expensive class of bug
271
- in this project, and a reproduction is worth more than any guess.
272
-
273
- Source: <https://github.com/tellmewhattodo/dsh-any-deepseek>
205
+ - Does not modify Harness itself, and does not replace the built-in `deepseek-official` route.
206
+ - Exposes exactly one model id (`deepseek-v4.1-flash`); per-provider upstream ids are mapped by
207
+ presets.
208
+ - **No cost estimation** — there is no price table, only token counts.
209
+ - **No automatic failover** (v1).
210
+ - **No multi-model aggregation**.
274
211
 
275
212
  ## License
276
213
 
277
- MIT
214
+ MIT. This is a small practical tool — forks, issues and PRs are welcome. See
215
+ [CONTRIBUTING.md](CONTRIBUTING.md).
216
+
217
+ <https://github.com/tellmewhattodo/dsh-any-deepseek>
package/README.md CHANGED
@@ -1,248 +1,202 @@
1
1
  # dsh-any-deepseek
2
2
 
3
- 把多家 DeepSeek 中转站收成**一个** provider、**一个**模型,在设置面板里粘 key、点一下切换。
3
+ **在 DeepSeek Harness 里,用多家渠道的 DeepSeek 额度,而模型列表里只多出一行。**
4
4
 
5
- 一个开源小工具(MIT)。欢迎改、欢迎加渠道 —— 见 [参与改进](#参与改进)。
5
+ 你在好几处买过 DeepSeek 的额度:官方、某家中转、某个云厂商、朋友推荐的便宜渠道。
6
+ 这个插件把它们收进**一个** provider,你粘几个 key、点一下就能换 —— 同时按真实渠道分开记账,
7
+ 你知道这个月每一家各花了多少 token。
6
8
 
7
- [English](README.en.md) · 中文(本页)
9
+ ![渠道列表](docs/screenshot-channels.png)
8
10
 
9
- ![渠道分页](docs/screenshot-channels.png)
11
+ [English](README.en.md) · 中文(本页)
10
12
 
11
13
  ---
12
14
 
13
- ## 它解决的是什么问题
15
+ ## 装之前先看一眼:它适合你吗
14
16
 
15
- 你在好几个地方买了 DeepSeek 的额度:官方、某家中转、某个云厂商、某个同事推荐的便宜渠道。
16
- 在 DeepSeek Harness 里,它们本来会变成**模型选择器里的 N 个条目**,而每次换渠道都要改配置、
17
- 换模型名、换 key,还得自己记住这个月哪个渠道花了多少 token。
17
+ **适合**,如果你符合下面任意一条:
18
18
 
19
- 装了这个插件之后:
19
+ - 你有**不止一个** DeepSeek 渠道,并且想在 Harness 里随意切换而不用改配置。
20
+ - 你想要**按渠道分开的用量统计**,而不是所有请求混成一个总数。
21
+ - 你用中转站,而它们的模型名各不相同(`deepseek-v4.1-flash` / `deepseek/deepseek-v4.1-flash` /
22
+ `deepseek-flash`),懒得每次自己对应。
20
23
 
21
- - 模型选择器里只有**一行**:`any-deepseek / deepseek-v4.1-flash`。
22
- - 下面挂**任意多个渠道**,每个渠道 = 一个端点 + 一个 key + 一个显示名。
23
- - 点哪个渠道的圆点,请求就发到哪个。**不会自动切换**(见 [为什么不做自动切换](#为什么不做自动切换))。
24
- - 用量按**真实上游**分别记账。
24
+ **不适合**,如果:
25
25
 
26
- ```
27
- ┌── 渠道 A(官方) ← 当前选中这个
28
- composer ──────►│
29
- │ ├── 渠道 B(某中转)
30
- ▼ └── 渠道 C(另一家)
31
- any-deepseek
32
- / deepseek-v4.1-flash
33
- (唯一对外模型)
34
- ```
26
+ - 你只有一个渠道,且它就是官方 —— Harness 自带的 `deepseek-official` 已经够用,不必装。
27
+ - 你想要**自动故障转移**(一家挂了自动换下一家)。本插件 v1 不做,见 [为什么不做自动切换](#为什么不做自动切换)。
28
+ - 你想要**多模型切换**。这个插件是「一个模型、多个来源」,不是模型路由器。
35
29
 
36
30
  ## 安装
37
31
 
32
+ 需要已经装好 DeepSeek Harness。在终端执行:
33
+
38
34
  ```bash
39
35
  dsh plugin --profile web add dsh-any-deepseek
40
36
  ```
41
37
 
42
38
  然后**重启 DSH**,打开 设置 → **Any DeepSeek**。
43
39
 
44
- > **首次安装必须重启**:profile 的分层列表变了,而装配图是启动时构建的。
45
- > 改已装插件的 client 半只需刷新浏览器;改它的 node 半则要重启。
46
- >
47
- > `dsh plugin` 会把参数转给 pnpm,所以 `add` 后面可以是 registry 名、tarball 路径或本地目录。
48
-
49
- ## 用法
50
-
51
- ### 加一个渠道
52
-
53
- 点 **添加渠道**,只有两样东西是必填的:
54
-
55
- | 字段 | 必填 | 说明 |
56
- |---|---|---|
57
- | **规格** | ✅ | 决定端点地址、上游模型名、协议。选它这三样就不用填了。 |
58
- | **渠道 id** | ✅ | 配置里的键名,`[a-z0-9][a-z0-9_-]*`。同规格可加多次,第二个自动变成 `-2`。 |
59
- | **显示名** | — | 只影响界面显示。同一家开两个号时靠它区分。**改名不动 id。** |
60
- | **密钥** | ✅ | 服务商给你的那串 key 本体,不需要先起名字。 |
61
-
62
- 内置规格:**DeepSeek 官方**、**OpenCode Go**、**Ollama Cloud**、**Open Design**、**CommandCode**。
63
- 想接别的中转,见 [加一个新渠道](#加一个新渠道要改什么)。
64
-
65
- ### 切换渠道
40
+ > **为什么必须重启**:插件被加进了 profile 的加载列表,而这份列表只在 DSH 启动时读取。
41
+ > 之后改这个插件的界面部分只需刷新浏览器,但**第一次装**一定要重启。
66
42
 
67
- 点行左边的圆点。当前渠道有三重标识:左侧色条、更强边框、底色,另加 `aria-current`。
43
+ ## 第一次用:三步
68
44
 
69
- **切换会让当前对话的 prompt 缓存失效**,所以尽量在自然的断点切,或新开一个会话。
45
+ ### 第 1 步:加一个渠道
70
46
 
71
- ### 看余额
47
+ 点 **添加渠道**,填两个必填项就能用:
72
48
 
73
- 点 **查询余额**(按需触发,不在打开面板时自动扫)。四种结果被严格区分,因为它们的下一步动作不同:
49
+ ![添加渠道](docs/screenshot-add-channel.png)
74
50
 
75
- | 显示 | 含义 |
51
+ | 填什么 | 说明 |
76
52
  |---|---|
77
- | `642.44 CNY` / `55.48 USD` | 读到了金额 |
78
- | `rolling 100% · weekly 81% · monthly 75%` | 这家只给**订阅窗口**,不给钱 |
79
- | **余额未尽**(无余额接口,实测可用:…) | 这家没有余额接口,但**实测能用** ⇒ 账户没欠费。**不代表余额数字**(见下) |
80
- | 不支持查询 / 读取失败 / 未配置密钥 | 三种不同的"没有" |
53
+ | **规格** | 选你用的那家。它决定地址、模型名、协议,所以这三样你不用管。 |
54
+ | **密钥** | 那家给你的 key,直接粘。不需要先起名字。 |
81
55
 
82
- > ⚠️ **余额未尽 ≠ 余额还有多少。** 它是一次最小真实请求(消耗一两个 token)成功的结果,
83
- > 只证明三件事:端点在线、key 可用、账户**没有**因为欠费被拒(欠费会答 402/quota,不答 200)。
84
- > 所以面板只写「未尽」,**绝不编一个金额**——写 `0.00` 会被读成"你没钱了",与事实正相反。
56
+ **渠道 id** 会自动填好(可以改,只是配置里的键名)。**显示名**留空的话用规格的默认名 ——
57
+ 同一家开两个号时,填上「主号」「备号」之类会好认。
85
58
 
86
- > ⚠️ **绝不用 0 代替"读不到"。** 缺数字就报缺。"读不到"与"是空的"导致相反的行动。
59
+ 内置规格:DeepSeek Official、OpenCode Go、Ollama Cloud、Open Design、CommandCode、Tencent TokenHub
60
+ (这些名字与下拉框里显示的完全一致)。
61
+ 要接别家,见 [接一个新渠道](#接一个新渠道)。
87
62
 
88
- ### 看用量
63
+ ### 第 2 步:确认它选中了
89
64
 
90
- ![用量分页](docs/screenshot-usage.png)
65
+ 第一个加进去的渠道会**自动成为当前渠道**,行首有圆点、左侧有色条、右侧有「当前」标记。
91
66
 
92
- 每个渠道一行,可以按渠道或按天分组。
67
+ 以后想换:点那一行左边的圆点即可。
93
68
 
94
- **口径**(面板里也印着):harness 的 token 计数是**不相交**的 —— **输入**只算未命中缓存的输入,
95
- **缓存读** / **缓存写**单独成列,三者相加才是**计费输入**。把「输入」直接当总量会得出
96
- "输入被少报了"的错误结论。
69
+ ### 第 3 步:开始用
97
70
 
98
- **速度**只算生成阶段(总时长减去首字等待)。把排队 + 首字 + 生成混成一个数,会让
99
- "想得久但吐得快"和"全程都慢"读数相同,而这两者的处理方式完全相反。
100
- `—` 表示没测到,**不是 0**。
71
+ 在模型的对话界面里选模型,选 **DeepSeek V4.1 Flash**(provider 名是 `any-deepseek`)。
72
+ 之后所有请求都会发到你选中的那个渠道。
101
73
 
102
- ## 配置
74
+ ## 日常会用到的三件事
103
75
 
104
- 设置都在这一个命名空间下 —— 它是 `cordis.patch.yml` 里的 **profile 条目 id**,面板绑定的就是它:
76
+ ### 换一个渠道
105
77
 
106
- ```yaml
107
- dsh-any-deepseek:
108
- current: opencode-go # 当前渠道
109
- channels:
110
- opencode-go:
111
- apiKey: sk-… # key 本体
112
- open-design:
113
- preset: open-design
114
- label: Open Design(主号) # 可选,显示名
115
- usage:
116
- enabled: true
117
- ```
78
+ 点行首的圆点。
118
79
 
119
- `channels.<id>` 有两种写法:
80
+ > ⚠️ **换渠道会让当前对话的 prompt 缓存失效**,所以尽量在话题切换时换,或者干脆新开一个会话。
120
81
 
121
- - **预设背书** — `preset: opencode-go`,可选覆盖若干字段;
122
- - **完全手写** — `baseURL` + `model` 必填,`api` 默认 `openai-completions`,
123
- `contextWindow` / `maxTokens` 有文档值兜底(走兜底时面板会在该行显示 ⚠)。
82
+ ### 看还剩多少钱
124
83
 
125
- ### key 存在哪(请读完这段)
84
+ 点 **查询余额**。它不会在打开面板时自动跑 —— 因为每次都要请求每一家的服务器。
126
85
 
127
- `apiKey` 存的是 **key 本人**,就在渠道条目上。面板不会让你给它起名字 —— 你粘 key,它就写在这儿。
86
+ 不同家能给出的东西不一样,面板会如实区分:
128
87
 
129
- ⚠️ **这意味着 key 在你的 profile 配置文件里。** `dsh --dump-config` 逐字打印 patch 层且
130
- **不脱敏**,所以一次诊断输出、一次 `git diff` 都会带上它。**别把 profile 文件发给别人。**
131
- 环境变量注入(`-e SOME_KEY=…`)不会被查询 —— 没有任何地方去解析一个名字。
88
+ | 你看到的 | 意思 |
89
+ |---|---|
90
+ | `642.44 CNY`、`55.48 USD` | 这家能报金额 |
91
+ | `rolling 100% · weekly 81% · monthly 75%` | 这家只给**订阅用量百分比**,不给钱 |
92
+ | **余额未尽**(实测可用:…) | 这家**没有余额接口**,但我发了一次最小请求,**它答了** ⇒ 账户没停 |
93
+ | 不支持查询 / 读取失败 / 未配置密钥 | 三种不同的「没拿到」 |
132
94
 
133
- > 早期版本把 key 藏在"凭据引用名"后面,并提示你"到凭据中心设置" —— 而这个界面本身并不存在。
134
- > 现在的做法是:一个字段、一个存储、一次写入。
95
+ > **「余额未尽」不等于「还剩多少」。** 一次成功的请求只证明三件事:地址通、key 对、账户没因为欠费被拒
96
+ > (欠费的账户会回 402,不会回 200)。所以这里只写「未尽」,**不会编一个金额给你**。
135
97
 
136
- `apiKeyEnv` 仍会**读**,为了兼容旧配置:值符合引用名语法(`^[A-Za-z_][A-Za-z0-9_]*$`)时当名字解析;
137
- 不符合时不可能是名字,于是当 key 本体用。`apiKey` 存在时永远优先,所以在面板里重填一次 key
138
- 就把旧条目迁移完了。
98
+ **哪些内置规格没有余额接口?** 目前是 **Open Design** 和 **Tencent TokenHub** ——
99
+ 这两家只开推理面,所以它们永远显示「余额未尽」而不是金额。这是如实报告,不是缺陷。
139
100
 
140
- 没配 key 的渠道**照样出现在面板里** —— 那是你发现并修复它的方式。通过它发请求会失败
141
- (`MISSING_CREDENTIAL`),该行的状态点是空心的。
101
+ ### 看这个月花了多少
142
102
 
143
- ### 协议
103
+ 切到 **用量** 分页。
144
104
 
145
- `openai-completions` 是默认也是推荐:几乎所有中转都说它,兼容位最多(约 20 个),
146
- 且 usage 的三种缓存字段拼写(`prompt_tokens_details.cached_tokens` /
147
- `prompt_cache_hit_tokens` / 顶层 `cached_tokens`)已由传输层归一。
105
+ ![用量统计](docs/screenshot-usage.png)
148
106
 
149
- 只有当某家真的说别的协议时才在**那一个渠道**上设 `api`(`openai-responses` / `anthropic-messages`)。
150
- 面板永远不会背着你在渠道间改协议。
107
+ 可以按渠道看,也可以按天看。「输入」只算没命中缓存的部分,缓存读/写单独列出 ——
108
+ 三者相加才是计费输入。速度一栏的 `—` 表示没测到,**不是 0**。
151
109
 
152
- ### 思考强度
110
+ ## 我的 key 安全吗
153
111
 
154
- 每个渠道声明自己支持哪些档位,harness 里选的档位会一路传到上游:
112
+ **它存在你本机的配置文件里,明文。**
155
113
 
156
- ```
157
- harness reasoningEffort → pi-ai ThinkingLevel → model.thinkingLevelMap → wire value
158
- ```
114
+ 位置是 profile 的 `channels.<渠道id>.apiKey`。它不会被这个插件发到任何第三方,
115
+ 但是:
159
116
 
160
- **本插件里没有第二层映射**:传输层用的 `thinkingLevelMap` **就是**映射层,再写一个必然与它漂移。
161
- 声明成 `null` 的档位表示不支持,不会出现在选择器里。
117
+ > ⚠️ `dsh --dump-config` 会把配置原样打印出来、**不做脱敏**。所以:
118
+ > **别把 profile 配置文件发给别人**,也别把它的 `git diff` 贴到公开的地方。
162
119
 
163
- ### 图片输入
120
+ 如果你需要分享配置做排查,先把 `apiKey` 那几行删掉。
164
121
 
165
- 支持。两个渠道都可以收发图片,前提是宿主把 `input` 声明为含 `image` —— 该声明在**注册模型时**给出
166
- (`inputModalities`),不是写在请求处理里。这是个踩过的坑:能力是 provider 的属性,
167
- 不是某条渠道或某次请求的属性。
122
+ ## 出问题了
168
123
 
169
- ## 为什么不做自动切换
124
+ **模型列表里没有 DeepSeek V4.1 Flash。**
125
+ → 装完还没重启。重启 DSH。
170
126
 
171
- v1 刻意只做手动。一个渠道失败就**报它自己的错**,不会静默转到另一个:
127
+ **设置里找不到「Any DeepSeek」。**
128
+ → 同上;或确认装的是 `web` 这个 profile:`dsh plugin --profile web list`。
172
129
 
173
- - 静默切换会让"这次答案是谁给的"变得不可知,而用量表会照实记另一个渠道 —— 两处口径不一致。
174
- - 半途换渠道会让 prompt 缓存失效,成本比你省下的那次失败更高。
175
- - 失败是信息。把它藏起来,你就不知道某个渠道正在坏掉。
130
+ **报 `MISSING_CREDENTIAL`。**
131
+ → 这个渠道还没填 key。面板里那一行的状态点是空心的,点「填写密钥」补上。
176
132
 
177
- ## 加一个新渠道要改什么
133
+ **报 `NO_CHANNEL_SELECTED`。**
134
+ → 一个渠道都没选中。面板底部会有提示,点任意一行的圆点即可。
178
135
 
179
- 这是一个**实用小插件**,加渠道是最有价值的贡献 —— 大多数情况下只需要动一个文件。
136
+ **「查询余额」显示「读取失败」。**
137
+ → 这家服务器这会儿没答好,稍后再试。如果一直这样,可能是网络到不了它。
180
138
 
181
- ### 只加一个预设(最常见)
139
+ **报 401 / 403 / 余额不足。**
140
+ → key 不对或过期,或者那家账户真的没钱了。
182
141
 
183
- 改 [`src/presets.ts`](src/presets.ts),往 `CHANNEL_PRESETS` 里加一条:
142
+ **换渠道以后回答变慢/变差。**
143
+ → 不同家的模型行为不完全一样。用「测试」按钮逐家试,用「用量」分页看各家的速度。
184
144
 
185
- ```ts
186
- 'your-relay': {
187
- displayName: 'Your Relay',
188
- api: 'openai-completions',
189
- baseURL: 'https://your-relay.example/v1',
190
- // 🔴 用**这家自己的**拼写。各家中转的模型名不同:
191
- // deepseek-v4.1-flash / deepseek/deepseek-v4.1-flash / deepseek-flash …
192
- // 抄错不会报错,只会在第一次请求时被上游拒。
193
- model: 'deepseek-v4.1-flash',
194
- ...V41_FLASH_CAPS,
195
- // 可选:这家有没有余额接口、是什么形状(见 src/balance.ts 的四种 shape)
196
- // balance: { shape: 'openai-billing' },
197
- },
198
- ```
145
+ ## 为什么不做自动切换
199
146
 
200
- 就够了 —— 面板的规格下拉会自动多出这一项。
147
+ 一个渠道失败就**报它自己的错**,不会静默换到另一家。三个理由:
201
148
 
202
- ⚠️ **两条规矩**(都有实际代价):
149
+ - 静默切换会让「这次答案是谁给的」变得不可知,而用量表会照实记另一家 —— 两处对不上。
150
+ - 半路换渠道会让 prompt 缓存失效,成本比你省下的那次失败更高。
151
+ - 失败是信息。藏起来,你就不知道某家正在坏掉。
203
152
 
204
- 1. **注释里不要写"我探过了",除非真的探过。** 这个仓里曾经有一条
205
- `Probed live: the billing pair answers` 的注释,而它从没被调用过,第一次真调用就 404。
206
- **假证据比没有注释更坏**,因为它让下一个读者不再去查。
207
- 2. **`model` 用上游的拼写,不是我们的 id。** 我们对外只暴露 `deepseek-v4.1-flash` 一个 id,
208
- 上游拼写由预设吸收 —— 这正是这个插件存在的理由。
153
+ ## 接一个新渠道
209
154
 
210
- ### 加一种余额形状(少见)
155
+ 内置了 5 家,接别家通常只需要改一个文件。见 **[CONTRIBUTING.md](CONTRIBUTING.md)**。
211
156
 
212
- 如果这家有余额接口但形状是新的一种,改 [`src/balance.ts`](src/balance.ts):
213
- 加一个 `shape`、加一个 `parseXxx()`、在 `balanceUrls()` 里给路径、在 `readBalance()` 里分发。
214
- `balanceUrls()` 是**唯一**知道路径的地方,所以预设无法与传输层意见相左。
157
+ ## 配置参考(进阶)
215
158
 
216
- ### 门禁(提交前必跑)
159
+ 设置都在 `dsh-any-deepseek` 这个命名空间下:
217
160
 
218
- ```bash
219
- pnpm run typecheck # 两个 tsconfig:node 半 + client 半
220
- pnpm run test # 纯函数层,不需要 harness
221
- pnpm run build # lib/index.js + lib/client.js
161
+ ```yaml
162
+ dsh-any-deepseek:
163
+ current: opencode-go # 当前渠道
164
+ channels:
165
+ opencode-go:
166
+ apiKey: sk-… # key 本体
167
+ open-design:
168
+ preset: open-design # 用内置规格
169
+ label: Open Design(主号) # 可选,显示名
170
+ my-relay:
171
+ baseURL: https://relay.example/v1 # 或者完全手写
172
+ model: deepseek-v4.1-flash
173
+ apiKey: sk-…
174
+ usage:
175
+ enabled: true
222
176
  ```
223
177
 
224
- 改动要带**能变红的用例**。这个仓的规矩是:**判据必须承重** —— 把修复回滚,那条用例必须红;
225
- 否则它只是装饰。仓里每条重要结论都附了它的负控制结果。
178
+ **协议**:默认 `openai-completions`(几乎所有中转都说它)。只有当某家真的说别的协议时,
179
+ 在**那一个**渠道上设 `api: openai-responses` 或 `api: anthropic-messages`。
226
180
 
227
- ## 已知边界
181
+ **思考强度**:每个渠道声明自己支持哪些档位,你在界面上选哪档就传到哪档。
182
+ 声明成不支持的档位不会出现在选择器里。
228
183
 
229
- - 不修改 harness,也不替代 `deepseek-official` / `llm-pi-ai` 路由。
230
- - 只暴露一个模型 id。上游模型 id 各家不同,由预设映射。
231
- - **不做费用估算**(没有价目表)。
232
- - **不做自动故障转移**(v1,见上)。
233
- - **不做多模型聚合** —— 这个插件就是"一个模型、多个来源",不是模型路由器。
184
+ **图片**:支持收发图片。
234
185
 
235
- ## 参与改进
186
+ **兼容旧配置**:早期版本把 key 存在 `apiKeyEnv` 里。这个字段仍会被读:
187
+ 值像环境变量名就当名字解析,不像就直接当 key 用。`apiKey` 存在时永远优先 ——
188
+ 所以在面板里重填一次 key 就迁移完了。
236
189
 
237
- 欢迎 fork、提 issue、发 PR。特别是:
190
+ ## 边界
238
191
 
239
- - **加一家渠道** —— 见上,改一个文件。
240
- - **修一处文案** —— 面板上的每一句都应该是一个可验证的事实;读到含糊或过时的,直接改。
241
- - **报一个"装了但没反应"** —— 这类静默失效是本项目最花时间的一类缺陷,
242
- 复现步骤比任何猜测都值钱。
243
-
244
- 源码:<https://github.com/tellmewhattodo/dsh-any-deepseek>
192
+ - 不修改 Harness 本体,也不替代自带的 `deepseek-official` 路由。
193
+ - 只暴露一个模型 id(`deepseek-v4.1-flash`);各家的上游模型名由规格映射。
194
+ - **不做费用估算** —— 没有价目表,只有 token 数。
195
+ - **不做自动故障转移**(v1)。
196
+ - **不做多模型聚合**。
245
197
 
246
198
  ## 许可
247
199
 
248
- MIT
200
+ MIT。这是个实用小工具,欢迎 fork、提 issue、发 PR —— 见 [CONTRIBUTING.md](CONTRIBUTING.md)。
201
+
202
+ <https://github.com/tellmewhattodo/dsh-any-deepseek>
Binary file
package/lib/index.js CHANGED
@@ -306,6 +306,50 @@ const CHANNEL_PRESETS = {
306
306
  ...V41_FLASH_CAPS,
307
307
  compat: { ...DEEPSEEK_THINKING_COMPAT }
308
308
  },
309
+ /**
310
+ * Tencent Cloud's TokenHub — a first-party forwarding endpoint for DeepSeek.
311
+ *
312
+ * Owner request (2026-10-23): 「我本机dsh还配了tokenhub这个渠道,这个渠道是腾讯云的渠道,
313
+ * 它的模型名称是固定的0605后缀就是向官方的转发」.
314
+ *
315
+ * 🔴 **The model id is frozen at `deepseek-v4-flash-202605` — the `-202605` suffix is part of the
316
+ * id, not a date we append.** This is the sharpest illustration of why presets exist: the same
317
+ * model is `deepseek-v4.1-flash` on the relays, `deepseek-flash` on the official API, and
318
+ * `deepseek-v4-flash-202605` here. Getting it wrong does not fail locally — the upstream
319
+ * rejects it on the first request.
320
+ *
321
+ * Endpoint and model id verified against TWO independent records in this container (the
322
+ * 2026-10-15 model-aggregator survey and the 2026-10-19 research note), and against the live
323
+ * `/v1/models` route (401 = present, auth required).
324
+ */
325
+ "tencent-tokenhub": {
326
+ displayName: "Tencent TokenHub",
327
+ api: "openai-completions",
328
+ baseURL: "https://tokenhub.tencentmaas.com/v1/",
329
+ model: "deepseek-v4-flash-202605",
330
+ contextWindow: 1024e3,
331
+ maxTokens: 384e3,
332
+ input: ["text"],
333
+ reasoningEfforts: { ...V41_FLASH_CAPS.reasoningEfforts },
334
+ compat: { ...DEEPSEEK_THINKING_COMPAT },
335
+ /**
336
+ * 🔴 **No balance surface — measured by status-code discrimination, not assumed.**
337
+ *
338
+ * Without a key, the inference route answers **401** while a made-up path answers **404**,
339
+ * so 401 here means "route exists, auth required". Four balance candidates were then probed
340
+ * at the same prefix and ALL answered 404, i.e. they do not exist:
341
+ * /v1/dashboard/billing/subscription 404
342
+ * /v1/user/balance 404
343
+ * /v1/credits 404
344
+ *
345
+ * ⇒ No `balance` is declared. The panel therefore reports "no balance API" and falls back to
346
+ * ONE minimal live request (`http.ts` `balanceRow`), showing 「余额未尽」 with the serving
347
+ * model and round-trip time — exactly what the owner asked for (「余额查询是没有的做成测通
348
+ * 查询即可」). Declaring a speculative `openai-billing` shape would have produced a 404 on
349
+ * every check, which is the mistake this repo already made once and recorded.
350
+ */
351
+ note: "Tencent Cloud forwarding endpoint; model id is frozen with a -202605 suffix. Text-only, per the endpoint record. No balance API — the panel verifies liveness instead."
352
+ },
309
353
  ollama: {
310
354
  displayName: "Ollama Cloud",
311
355
  api: "openai-completions",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-any-deepseek",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "One DeepSeek V4.1 Flash provider for DeepSeek Harness: paste a key per channel, switch channels manually, token usage booked per real upstream.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,6 +30,7 @@
30
30
  "cordis.patch.yml",
31
31
  "README.md",
32
32
  "README.en.md",
33
+ "CONTRIBUTING.md",
33
34
  "docs/*.png",
34
35
  "LICENSE"
35
36
  ],