dsh-any-deepseek 0.0.14 → 0.0.16
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/README.en.md +277 -0
- package/README.md +185 -109
- package/docs/screenshot-channels.png +0 -0
- package/docs/screenshot-usage.png +0 -0
- package/lib/channels.d.ts +4 -1
- package/lib/client.js +341 -186
- package/lib/http.d.ts +66 -3
- package/lib/index.js +66 -13
- package/lib/presets.d.ts +3 -1
- package/package.json +5 -1
package/README.en.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# dsh-any-deepseek
|
|
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.
|
|
5
|
+
|
|
6
|
+
A small open-source tool (MIT). Fork it, add a relay, improve it — see
|
|
7
|
+
[Contributing](#contributing).
|
|
8
|
+
|
|
9
|
+
[中文](README.md) · English (this page)
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What problem this solves
|
|
16
|
+
|
|
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.
|
|
21
|
+
|
|
22
|
+
After installing this plugin:
|
|
23
|
+
|
|
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**.
|
|
29
|
+
|
|
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
|
+
```
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
dsh plugin --profile web add dsh-any-deepseek
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then **restart DSH** and open Settings → **Any DeepSeek**.
|
|
47
|
+
|
|
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).
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Add a channel
|
|
58
|
+
|
|
59
|
+
Click **添加渠道**. Only two things are required:
|
|
60
|
+
|
|
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. |
|
|
67
|
+
|
|
68
|
+
Bundled presets: **DeepSeek Official**, **OpenCode Go**, **Ollama Cloud**, **Open Design**,
|
|
69
|
+
**CommandCode**. For another relay, see [Adding a channel](#adding-a-channel).
|
|
70
|
+
|
|
71
|
+
### Switch channels
|
|
72
|
+
|
|
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`.
|
|
75
|
+
|
|
76
|
+
**Switching invalidates the prompt cache for the running conversation**, so switch at a natural
|
|
77
|
+
boundary or start a new session.
|
|
78
|
+
|
|
79
|
+
### Remaining balance
|
|
80
|
+
|
|
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:
|
|
83
|
+
|
|
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" |
|
|
90
|
+
|
|
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.
|
|
96
|
+
|
|
97
|
+
> ⚠️ **A missing number is never rendered as `0`.** "We could not read it" and "it is empty" lead
|
|
98
|
+
> to opposite actions.
|
|
99
|
+
|
|
100
|
+
### Usage
|
|
101
|
+
|
|
102
|
+

|
|
103
|
+
|
|
104
|
+
One row per channel, groupable by channel or by day.
|
|
105
|
+
|
|
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.
|
|
109
|
+
|
|
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**.
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
Everything lives under one settings namespace — the **profile entry id** from
|
|
117
|
+
`cordis.patch.yml`, which is what the panel binds to:
|
|
118
|
+
|
|
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
|
+
```
|
|
131
|
+
|
|
132
|
+
`channels.<id>` may be either:
|
|
133
|
+
|
|
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).
|
|
138
|
+
|
|
139
|
+
### Where the key lives (please read this part)
|
|
140
|
+
|
|
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.
|
|
143
|
+
|
|
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.
|
|
148
|
+
|
|
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.
|
|
152
|
+
|
|
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.
|
|
157
|
+
|
|
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.
|
|
160
|
+
|
|
161
|
+
### Protocols
|
|
162
|
+
|
|
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`).
|
|
167
|
+
|
|
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.
|
|
170
|
+
|
|
171
|
+
### Thinking level
|
|
172
|
+
|
|
173
|
+
Each channel declares its selectable thinking levels, and the level picked in the harness is
|
|
174
|
+
passed through to the relay:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
harness reasoningEffort → pi-ai ThinkingLevel → model.thinkingLevelMap → wire value
|
|
178
|
+
```
|
|
179
|
+
|
|
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.
|
|
183
|
+
|
|
184
|
+
### Image input
|
|
185
|
+
|
|
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.
|
|
189
|
+
|
|
190
|
+
## Why no failover
|
|
191
|
+
|
|
192
|
+
v1 is deliberately manual. A failing channel reports its own error; it does not silently fall over
|
|
193
|
+
to another:
|
|
194
|
+
|
|
195
|
+
- Silent failover makes "who answered this?" unknowable, while the usage table honestly records
|
|
196
|
+
the other relay — two parts of the UI would disagree.
|
|
197
|
+
- 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
|
+
```
|
|
223
|
+
|
|
224
|
+
That is enough — the preset picker gains the entry automatically.
|
|
225
|
+
|
|
226
|
+
⚠️ **Two rules** (both from real costs):
|
|
227
|
+
|
|
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.
|
|
235
|
+
|
|
236
|
+
### Add a balance shape (rarer)
|
|
237
|
+
|
|
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
|
|
249
|
+
```
|
|
250
|
+
|
|
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.
|
|
254
|
+
|
|
255
|
+
## Known boundaries
|
|
256
|
+
|
|
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.
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
Fork, file an issue, send a PR. Especially welcome:
|
|
266
|
+
|
|
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>
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,172 +1,248 @@
|
|
|
1
1
|
# dsh-any-deepseek
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
│
|
|
13
|
-
├─ channel selected in the panel (manual switch, v1)
|
|
14
|
-
├─ key resolved from ctx.credentials per request
|
|
15
|
-
└─ forwarded as openai-completions to the channel's endpoint
|
|
16
|
-
```
|
|
3
|
+
把多家 DeepSeek 中转站收成**一个** provider、**一个**模型,在设置面板里粘 key、点一下切换。
|
|
4
|
+
|
|
5
|
+
一个开源小工具(MIT)。欢迎改、欢迎加渠道 —— 见 [参与改进](#参与改进)。
|
|
6
|
+
|
|
7
|
+
[English](README.en.md) · 中文(本页)
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
---
|
|
17
12
|
|
|
18
|
-
##
|
|
13
|
+
## 它解决的是什么问题
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
你在好几个地方买了 DeepSeek 的额度:官方、某家中转、某个云厂商、某个同事推荐的便宜渠道。
|
|
16
|
+
在 DeepSeek Harness 里,它们本来会变成**模型选择器里的 N 个条目**,而每次换渠道都要改配置、
|
|
17
|
+
换模型名、换 key,还得自己记住这个月哪个渠道花了多少 token。
|
|
18
|
+
|
|
19
|
+
装了这个插件之后:
|
|
20
|
+
|
|
21
|
+
- 模型选择器里只有**一行**:`any-deepseek / deepseek-v4.1-flash`。
|
|
22
|
+
- 下面挂**任意多个渠道**,每个渠道 = 一个端点 + 一个 key + 一个显示名。
|
|
23
|
+
- 点哪个渠道的圆点,请求就发到哪个。**不会自动切换**(见 [为什么不做自动切换](#为什么不做自动切换))。
|
|
24
|
+
- 用量按**真实上游**分别记账。
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
┌── 渠道 A(官方) ← 当前选中这个
|
|
28
|
+
composer ──────►│
|
|
29
|
+
│ ├── 渠道 B(某中转)
|
|
30
|
+
▼ └── 渠道 C(另一家)
|
|
31
|
+
any-deepseek
|
|
32
|
+
/ deepseek-v4.1-flash
|
|
33
|
+
(唯一对外模型)
|
|
34
|
+
```
|
|
27
35
|
|
|
28
|
-
##
|
|
36
|
+
## 安装
|
|
29
37
|
|
|
30
38
|
```bash
|
|
31
39
|
dsh plugin --profile web add dsh-any-deepseek
|
|
32
40
|
```
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
然后**重启 DSH**,打开 设置 → **Any DeepSeek**。
|
|
35
43
|
|
|
36
|
-
>
|
|
37
|
-
>
|
|
38
|
-
> in the browser without a restart; its node half does not.
|
|
44
|
+
> **首次安装必须重启**:profile 的分层列表变了,而装配图是启动时构建的。
|
|
45
|
+
> 改已装插件的 client 半只需刷新浏览器;改它的 node 半则要重启。
|
|
39
46
|
>
|
|
40
|
-
> `dsh plugin`
|
|
41
|
-
|
|
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
|
+
### 切换渠道
|
|
66
|
+
|
|
67
|
+
点行左边的圆点。当前渠道有三重标识:左侧色条、更强边框、底色,另加 `aria-current`。
|
|
68
|
+
|
|
69
|
+
**切换会让当前对话的 prompt 缓存失效**,所以尽量在自然的断点切,或新开一个会话。
|
|
70
|
+
|
|
71
|
+
### 看余额
|
|
72
|
+
|
|
73
|
+
点 **查询余额**(按需触发,不在打开面板时自动扫)。四种结果被严格区分,因为它们的下一步动作不同:
|
|
42
74
|
|
|
43
|
-
|
|
75
|
+
| 显示 | 含义 |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `642.44 CNY` / `55.48 USD` | 读到了金额 |
|
|
78
|
+
| `rolling 100% · weekly 81% · monthly 75%` | 这家只给**订阅窗口**,不给钱 |
|
|
79
|
+
| **余额未尽**(无余额接口,实测可用:…) | 这家没有余额接口,但**实测能用** ⇒ 账户没欠费。**不代表余额数字**(见下) |
|
|
80
|
+
| 不支持查询 / 读取失败 / 未配置密钥 | 三种不同的"没有" |
|
|
44
81
|
|
|
45
|
-
|
|
46
|
-
|
|
82
|
+
> ⚠️ **余额未尽 ≠ 余额还有多少。** 它是一次最小真实请求(消耗一两个 token)成功的结果,
|
|
83
|
+
> 只证明三件事:端点在线、key 可用、账户**没有**因为欠费被拒(欠费会答 402/quota,不答 200)。
|
|
84
|
+
> 所以面板只写「未尽」,**绝不编一个金额**——写 `0.00` 会被读成"你没钱了",与事实正相反。
|
|
85
|
+
|
|
86
|
+
> ⚠️ **绝不用 0 代替"读不到"。** 缺数字就报缺。"读不到"与"是空的"导致相反的行动。
|
|
87
|
+
|
|
88
|
+
### 看用量
|
|
89
|
+
|
|
90
|
+

|
|
91
|
+
|
|
92
|
+
每个渠道一行,可以按渠道或按天分组。
|
|
93
|
+
|
|
94
|
+
**口径**(面板里也印着):harness 的 token 计数是**不相交**的 —— **输入**只算未命中缓存的输入,
|
|
95
|
+
**缓存读** / **缓存写**单独成列,三者相加才是**计费输入**。把「输入」直接当总量会得出
|
|
96
|
+
"输入被少报了"的错误结论。
|
|
97
|
+
|
|
98
|
+
**速度**只算生成阶段(总时长减去首字等待)。把排队 + 首字 + 生成混成一个数,会让
|
|
99
|
+
"想得久但吐得快"和"全程都慢"读数相同,而这两者的处理方式完全相反。
|
|
100
|
+
`—` 表示没测到,**不是 0**。
|
|
101
|
+
|
|
102
|
+
## 配置
|
|
103
|
+
|
|
104
|
+
设置都在这一个命名空间下 —— 它是 `cordis.patch.yml` 里的 **profile 条目 id**,面板绑定的就是它:
|
|
47
105
|
|
|
48
106
|
```yaml
|
|
49
107
|
dsh-any-deepseek:
|
|
50
|
-
current: opencode-go #
|
|
108
|
+
current: opencode-go # 当前渠道
|
|
51
109
|
channels:
|
|
52
110
|
opencode-go:
|
|
53
|
-
apiKey: sk-… #
|
|
54
|
-
|
|
55
|
-
|
|
111
|
+
apiKey: sk-… # key 本体
|
|
112
|
+
open-design:
|
|
113
|
+
preset: open-design
|
|
114
|
+
label: Open Design(主号) # 可选,显示名
|
|
56
115
|
usage:
|
|
57
116
|
enabled: true
|
|
58
117
|
```
|
|
59
118
|
|
|
60
|
-
`channels.<id>`
|
|
61
|
-
|
|
62
|
-
- **preset-backed** — `preset: opencode-go` plus optional overrides; or
|
|
63
|
-
- **fully declared** — `baseURL` + `model` are required; `api` defaults to
|
|
64
|
-
`openai-completions`, and `contextWindow` / `maxTokens` fall back to documented defaults
|
|
65
|
-
(the panel shows a warning next to the row when a default was applied rather than declared).
|
|
66
|
-
|
|
67
|
-
### Where the key lives
|
|
119
|
+
`channels.<id>` 有两种写法:
|
|
68
120
|
|
|
69
|
-
|
|
70
|
-
|
|
121
|
+
- **预设背书** — `preset: opencode-go`,可选覆盖若干字段;
|
|
122
|
+
- **完全手写** — `baseURL` + `model` 必填,`api` 默认 `openai-completions`,
|
|
123
|
+
`contextWindow` / `maxTokens` 有文档值兜底(走兜底时面板会在该行显示 ⚠)。
|
|
71
124
|
|
|
72
|
-
|
|
73
|
-
layers verbatim and does not redact, so a diagnostic dump, a `git diff` of the profile, or a
|
|
74
|
-
pasted config includes it. Do not share the profile document. Environment-variable injection
|
|
75
|
-
(`-e SOME_KEY=…`) is not consulted — nothing resolves a name.
|
|
125
|
+
### key 存在哪(请读完这段)
|
|
76
126
|
|
|
77
|
-
`
|
|
78
|
-
matches the credential-reference grammar (`^[A-Za-z_][A-Za-z0-9_]*$`) it is resolved as a name;
|
|
79
|
-
when it does not, it cannot be a name, so it is adopted as the key itself. `apiKey` always wins
|
|
80
|
-
when both are present, so re-keying through the panel is enough to migrate an entry.
|
|
127
|
+
`apiKey` 存的是 **key 本人**,就在渠道条目上。面板不会让你给它起名字 —— 你粘 key,它就写在这儿。
|
|
81
128
|
|
|
82
|
-
|
|
83
|
-
|
|
129
|
+
⚠️ **这意味着 key 在你的 profile 配置文件里。** `dsh --dump-config` 逐字打印 patch 层且
|
|
130
|
+
**不脱敏**,所以一次诊断输出、一次 `git diff` 都会带上它。**别把 profile 文件发给别人。**
|
|
131
|
+
环境变量注入(`-e SOME_KEY=…`)不会被查询 —— 没有任何地方去解析一个名字。
|
|
84
132
|
|
|
85
|
-
|
|
133
|
+
> 早期版本把 key 藏在"凭据引用名"后面,并提示你"到凭据中心设置" —— 而这个界面本身并不存在。
|
|
134
|
+
> 现在的做法是:一个字段、一个存储、一次写入。
|
|
86
135
|
|
|
87
|
-
`
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
(`prompt_tokens_details.cached_tokens`, `prompt_cache_hit_tokens`, top-level `cached_tokens`).
|
|
136
|
+
`apiKeyEnv` 仍会**读**,为了兼容旧配置:值符合引用名语法(`^[A-Za-z_][A-Za-z0-9_]*$`)时当名字解析;
|
|
137
|
+
不符合时不可能是名字,于是当 key 本体用。`apiKey` 存在时永远优先,所以在面板里重填一次 key
|
|
138
|
+
就把旧条目迁移完了。
|
|
91
139
|
|
|
92
|
-
|
|
93
|
-
|
|
140
|
+
没配 key 的渠道**照样出现在面板里** —— 那是你发现并修复它的方式。通过它发请求会失败
|
|
141
|
+
(`MISSING_CREDENTIAL`),该行的状态点是空心的。
|
|
94
142
|
|
|
95
|
-
###
|
|
143
|
+
### 协议
|
|
96
144
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
145
|
+
`openai-completions` 是默认也是推荐:几乎所有中转都说它,兼容位最多(约 20 个),
|
|
146
|
+
且 usage 的三种缓存字段拼写(`prompt_tokens_details.cached_tokens` /
|
|
147
|
+
`prompt_cache_hit_tokens` / 顶层 `cached_tokens`)已由传输层归一。
|
|
100
148
|
|
|
101
|
-
|
|
102
|
-
|
|
149
|
+
只有当某家真的说别的协议时才在**那一个渠道**上设 `api`(`openai-responses` / `anthropic-messages`)。
|
|
150
|
+
面板永远不会背着你在渠道间改协议。
|
|
103
151
|
|
|
104
|
-
###
|
|
152
|
+
### 思考强度
|
|
105
153
|
|
|
106
|
-
|
|
107
|
-
passed through to the relay:
|
|
154
|
+
每个渠道声明自己支持哪些档位,harness 里选的档位会一路传到上游:
|
|
108
155
|
|
|
109
156
|
```
|
|
110
157
|
harness reasoningEffort → pi-ai ThinkingLevel → model.thinkingLevelMap → wire value
|
|
111
158
|
```
|
|
112
159
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
and is not offered in the picker.
|
|
160
|
+
**本插件里没有第二层映射**:传输层用的 `thinkingLevelMap` **就是**映射层,再写一个必然与它漂移。
|
|
161
|
+
声明成 `null` 的档位表示不支持,不会出现在选择器里。
|
|
116
162
|
|
|
117
|
-
###
|
|
163
|
+
### 图片输入
|
|
118
164
|
|
|
119
|
-
`
|
|
120
|
-
|
|
121
|
-
|
|
165
|
+
支持。两个渠道都可以收发图片,前提是宿主把 `input` 声明为含 `image` —— 该声明在**注册模型时**给出
|
|
166
|
+
(`inputModalities`),不是写在请求处理里。这是个踩过的坑:能力是 provider 的属性,
|
|
167
|
+
不是某条渠道或某次请求的属性。
|
|
122
168
|
|
|
123
|
-
|
|
169
|
+
## 为什么不做自动切换
|
|
124
170
|
|
|
125
|
-
|
|
126
|
-
`attachmentId`; turning that into request bytes requires the durable attachment service, which
|
|
127
|
-
this adapter does not consult. Declaring image support anyway would make the harness stop
|
|
128
|
-
substituting its placeholder text and drop the image silently — worse than declaring text-only.
|
|
171
|
+
v1 刻意只做手动。一个渠道失败就**报它自己的错**,不会静默转到另一个:
|
|
129
172
|
|
|
130
|
-
|
|
173
|
+
- 静默切换会让"这次答案是谁给的"变得不可知,而用量表会照实记另一个渠道 —— 两处口径不一致。
|
|
174
|
+
- 半途换渠道会让 prompt 缓存失效,成本比你省下的那次失败更高。
|
|
175
|
+
- 失败是信息。把它藏起来,你就不知道某个渠道正在坏掉。
|
|
131
176
|
|
|
132
|
-
|
|
177
|
+
## 加一个新渠道要改什么
|
|
133
178
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
179
|
+
这是一个**实用小插件**,加渠道是最有价值的贡献 —— 大多数情况下只需要动一个文件。
|
|
180
|
+
|
|
181
|
+
### 只加一个预设(最常见)
|
|
137
182
|
|
|
138
|
-
|
|
183
|
+
改 [`src/presets.ts`](src/presets.ts),往 `CHANNEL_PRESETS` 里加一条:
|
|
139
184
|
|
|
140
|
-
```
|
|
141
|
-
|
|
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
|
+
},
|
|
142
198
|
```
|
|
143
199
|
|
|
144
|
-
|
|
145
|
-
|
|
200
|
+
就够了 —— 面板的规格下拉会自动多出这一项。
|
|
201
|
+
|
|
202
|
+
⚠️ **两条规矩**(都有实际代价):
|
|
203
|
+
|
|
204
|
+
1. **注释里不要写"我探过了",除非真的探过。** 这个仓里曾经有一条
|
|
205
|
+
`Probed live: the billing pair answers` 的注释,而它从没被调用过,第一次真调用就 404。
|
|
206
|
+
**假证据比没有注释更坏**,因为它让下一个读者不再去查。
|
|
207
|
+
2. **`model` 用上游的拼写,不是我们的 id。** 我们对外只暴露 `deepseek-v4.1-flash` 一个 id,
|
|
208
|
+
上游拼写由预设吸收 —— 这正是这个插件存在的理由。
|
|
209
|
+
|
|
210
|
+
### 加一种余额形状(少见)
|
|
146
211
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
212
|
+
如果这家有余额接口但形状是新的一种,改 [`src/balance.ts`](src/balance.ts):
|
|
213
|
+
加一个 `shape`、加一个 `parseXxx()`、在 `balanceUrls()` 里给路径、在 `readBalance()` 里分发。
|
|
214
|
+
`balanceUrls()` 是**唯一**知道路径的地方,所以预设无法与传输层意见相左。
|
|
150
215
|
|
|
151
|
-
|
|
216
|
+
### 门禁(提交前必跑)
|
|
152
217
|
|
|
153
218
|
```bash
|
|
154
|
-
pnpm
|
|
155
|
-
pnpm run
|
|
156
|
-
pnpm run test # pure layers only; no harness needed
|
|
219
|
+
pnpm run typecheck # 两个 tsconfig:node 半 + client 半
|
|
220
|
+
pnpm run test # 纯函数层,不需要 harness
|
|
157
221
|
pnpm run build # lib/index.js + lib/client.js
|
|
158
222
|
```
|
|
159
223
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
224
|
+
改动要带**能变红的用例**。这个仓的规矩是:**判据必须承重** —— 把修复回滚,那条用例必须红;
|
|
225
|
+
否则它只是装饰。仓里每条重要结论都附了它的负控制结果。
|
|
226
|
+
|
|
227
|
+
## 已知边界
|
|
228
|
+
|
|
229
|
+
- 不修改 harness,也不替代 `deepseek-official` / `llm-pi-ai` 路由。
|
|
230
|
+
- 只暴露一个模型 id。上游模型 id 各家不同,由预设映射。
|
|
231
|
+
- **不做费用估算**(没有价目表)。
|
|
232
|
+
- **不做自动故障转移**(v1,见上)。
|
|
233
|
+
- **不做多模型聚合** —— 这个插件就是"一个模型、多个来源",不是模型路由器。
|
|
234
|
+
|
|
235
|
+
## 参与改进
|
|
236
|
+
|
|
237
|
+
欢迎 fork、提 issue、发 PR。特别是:
|
|
163
238
|
|
|
164
|
-
|
|
239
|
+
- **加一家渠道** —— 见上,改一个文件。
|
|
240
|
+
- **修一处文案** —— 面板上的每一句都应该是一个可验证的事实;读到含糊或过时的,直接改。
|
|
241
|
+
- **报一个"装了但没反应"** —— 这类静默失效是本项目最花时间的一类缺陷,
|
|
242
|
+
复现步骤比任何猜测都值钱。
|
|
165
243
|
|
|
166
|
-
-
|
|
167
|
-
- Exposes exactly one model id. Upstream model ids differ per relay and are mapped in presets.
|
|
168
|
-
- No cost estimation (no price table), no automatic failover (v1), no multi-model aggregation.
|
|
244
|
+
源码:<https://github.com/tellmewhattodo/dsh-any-deepseek>
|
|
169
245
|
|
|
170
|
-
##
|
|
246
|
+
## 许可
|
|
171
247
|
|
|
172
248
|
MIT
|
|
Binary file
|
|
Binary file
|
package/lib/channels.d.ts
CHANGED
|
@@ -102,7 +102,10 @@ export interface ResolvedChannel {
|
|
|
102
102
|
* How this endpoint's remaining balance can be read, when it can be.
|
|
103
103
|
*
|
|
104
104
|
* Absent means "no balance API" — a real state, not a missing value. See `presets.ts` for the
|
|
105
|
-
*
|
|
105
|
+
* four verified shapes. Note the two different kinds of "no number": OpenCode Go reports usage
|
|
106
|
+
* WINDOWS rather than money (it does have a surface), while Open Design publishes no surface at
|
|
107
|
+
* all (measured: seven candidates, all 404). The panel distinguishes them, and checks liveness
|
|
108
|
+
* for the latter instead of inventing an amount.
|
|
106
109
|
*/
|
|
107
110
|
balance?: BalanceProbe;
|
|
108
111
|
/** Facts the user should see because a default was applied rather than declared. */
|