deepinfra-fusion-proxy 0.0.0-stage → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 deepinfra-fusion-proxy contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,235 @@
1
- # Temporary Holding Version
1
+ # deepinfra-fusion-proxy
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [English](README.md) · [简体中文](README.zh-CN.md)
4
+
5
+ [![Release](https://img.shields.io/github/v/tag/citydirector/deepinfra-fusion-proxy?sort=semver&label=release)](https://github.com/citydirector/deepinfra-fusion-proxy/releases) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![DSH plugin](https://img.shields.io/badge/DSH%20plugin-%E2%89%A50.2.2--alpha.2-blue)](https://github.com/topics/dsh-plugin)
6
+
7
+ A dependency-free local reverse proxy for the [DeepInfra](https://deepinfra.com) API that turns the service-tier knobs into first-class switches and adds automatic `Standard ↔ Flex` fallback with per-session cooldown locks. It also ships as a **DSH (DeepSeek Harness) profile bundle** (DSH ≥ 0.2.2-alpha.2), so a DSH install can add the proxy, its per-session toggle card *and* the bundle row's configuration page from npm.
8
+
9
+ Requires **DSH 0.2.2-alpha.2 or newer 0.2.x**.
10
+
11
+ ## npm installation and updates
12
+
13
+ Requires DSH 0.2.2-alpha.2 or a newer 0.2.x and Node.js 24.9+. Enter `deepinfra-fusion-proxy` in **Plugins → Add plugin**, or run:
14
+
15
+ ```sh
16
+ dsh plugin --profile web add deepinfra-fusion-proxy
17
+ ```
18
+
19
+ For an existing GitHub installation, back up the profile's `cordis.patch.yml`, remove the old package in Plugins, and add the npm package above in the same profile. Keep the configuration row IDs and verify the saved settings after reinstalling. Subsequent updates use DSH's version list and update operation. The plugin no longer polls GitHub or provides its own install/rollback buttons; an old `updateCheck` setting has no effect. DeepInfra runtime data stays at its existing path.
20
+
21
+ ## Quick start
22
+
23
+ 1. **Install**: sidebar **Plugins** → Add plugin, and give it
24
+ ```
25
+ deepinfra-fusion-proxy
26
+ ```
27
+ Switch it on. The proxy starts with DSH on `127.0.0.1:8790`.
28
+ 2. **Point a route at it**: an `openai-completions` provider with `baseURL: http://127.0.0.1:8790/v1/openai` and `cacheRetention: long` (that is what gives each session its own `prompt_cache_key`). Keep your DeepInfra API key on the provider as usual.
29
+ 3. **Pick a tier per session**: the chip in the composer tool row switches Standard / Flex, the wait policy and the fallback behaviour.
30
+
31
+ From the command line:
32
+
33
+ ```bash
34
+ dsh plugin --profile web add deepinfra-fusion-proxy
35
+ ```
36
+
37
+ Without DSH, run the proxy on its own (no install step, the core has no dependencies):
38
+
39
+ ```bash
40
+ git clone https://github.com/citydirector/deepinfra-fusion-proxy.git
41
+ cd deepinfra-fusion-proxy
42
+ node server.js # or: npm start
43
+ ```
44
+
45
+ ## Why
46
+
47
+ DeepInfra labels certain models with service tiers ([Chat Completions overview](https://docs.deepinfra.com/chat/overview), [Priority Service Tier](https://deepinfra.com/blog/priority-service-tier)):
48
+
49
+ | Parameter | Values | Semantics |
50
+ |---|---|---|
51
+ | `service_tier` | `"flex"` \| `"priority"` | `flex` = cheap, best-effort (queues when at capacity); `priority` = guaranteed capacity, pricier. The response echoes the effective tier back in `response.service_tier`. |
52
+ | `fail_fast` | `true` \| `false` | Default `false` (queue). When `true`, capacity problems return **HTTP 429** immediately instead of queuing. |
53
+
54
+ `fail_fast: true` turns a capacity problem into a detectable `429`, and `service_tier` echoes let you know which tier you actually got. This proxy exploits exactly that to implement a fallback engine.
55
+
56
+ Most clients have no way to send these top-level fields, and switching between tiers on a per-request or per-session basis would require modifying client code. This proxy centralizes both.
57
+
58
+ ## Features
59
+
60
+ - **Body injection** — adds `service_tier` / `fail_fast` to `POST /v1/openai/chat/completions` requests; never touches the model-visible message text.
61
+ - **Per-session routing** — reads `prompt_cache_key` from the request body (clients that request long cache retention send it automatically) and treats it as the session id, so routing and cooldown locks are naturally per-session with zero client changes.
62
+ - **Automatic fallback** — a `429` on a Flex attempt retries once as Standard, then either stays on Standard (`fallbackAction: "stay"`) or arms a per-session cooldown lock (`fallbackAction: "cooldown"`) that auto-returns to Flex after N rounds.
63
+ - **Wait policy** — per session, choose **fail-fast** (`fail_fast: true`, busy → immediate 429 → fallback) or **wait** (`fail_fast: false`, busy → queue up to the provider cap, ~10 minutes on DeepInfra).
64
+ - **Control API** — JSON endpoints for state, config, and unlocking cooldown locks, so a GUI or script can drive it live without a restart.
65
+ - **DSH 0.1.7 configuration** — the host plugin declares its connection fields in its own Cordis `Config` (every field `.volatile()`), so on DSH 0.1.7+ the values live in the active profile's `cordis.patch.yml` and an edit restarts the supervised child in place instead of remounting the plugin. The bundle ships the row's configuration page (Plugins → `deepinfra-fusion-proxy` → `deepinfra-proxy` → 配置).
66
+ - **Zero dependencies in the core** — pure Node stdlib (`http`/`https`/`fs`/`path`), so `server.js` runs straight from a checkout with nothing to install. (The DSH host plugin beside it imports the harness' own `@deepseek-ai/schemastery`, declared as a dependency so a bundle install resolves it.)
67
+
68
+ ## How it works
69
+
70
+ ```
71
+ OpenAI-compatible client (baseURL → http://127.0.0.1:8790/v1/openai)
72
+ │
73
+ ▼
74
+ [ deepinfra-fusion-proxy (127.0.0.1:8790) ]
75
+ ├─ reads config.json: defaultMode, sessions{}, defaultWait, waitModes{}…
76
+ ├─ injects service_tier / fail_fast per (mode × wait policy)
77
+ ├─ forwards to https://api.deepinfra.com/v1/openai (keeps Authorization)
78
+ └─ on 429 (non-Standard): retry once as Standard, then
79
+ stay / arm cooldown lock per fallbackAction
80
+ ```
81
+
82
+ - `chat/completions` POSTs get injection + fallback.
83
+ - Everything else (`GET /models`, embeddings, …) passes through untouched.
84
+ - SSE responses are relayed chunked with correct headers; upstream errors become `502`.
85
+
86
+ ## The DSH bundle
87
+
88
+ This repository **is** a bundle package (`dsh.bundle.patch`), so a DSH profile can install it from the GitHub URL — the Plugins panel resolves the patch, adds the bundle to the profile's layer stack, and pnpm lands `server.js`, the host plugin and the toggle card together.
89
+
90
+ What the bundle inserts (`cordis.patch.yml`, one id-addressed row — a user-layer row with the same id overrides it):
91
+
92
+ ```yaml
93
+ - insert:
94
+ - id: deepinfra-proxy
95
+ name: deepinfra-fusion-proxy # the package: "." = host plugin, "./client" = chip + configuration page
96
+ ```
97
+
98
+ The row names the **package**, not a `./` path: DSH turns a relative name into an absolute file URL,
99
+ and the configuration editor copies that URL into the user patch on its first save, which breaks as
100
+ soon as a portable DSH folder moves. Releases before 2.1 did exactly that (and shipped a second
101
+ `deepinfra-proxy-ui` row); 2.1 rewrites such a stale user-patch row on its first start.
102
+
103
+ Installing the package is not the same as activating the bundle: the profile loads the
104
+ row above only when its `package.json` lists `deepinfra-fusion-proxy` in `dsh.profile.bundles`.
105
+ A `dependencies` entry alone (a manual `pnpm add`, for example) leaves the profile with no row
106
+ and no proxy — the Plugins panel writes the bundle entry for you, a manual install must add it too.
107
+
108
+ Two profiles that share one `$DSH_HOME` (the portable and desktop apps pointed at the same `data`
109
+ directory) both default to `$DSH_HOME/deepinfra-proxy` and share that state directory. Set a
110
+ distinct `stateDir` and `port` per profile to keep the instances independent.
111
+
112
+ - **Host plugin** — starts/stops the proxy core with DSH, restarts it with backoff, releases the port on unload, and registers the same-origin `/__dfusion/*` → `/__proxy/*` bridge the card talks through.
113
+ - **Toggle card** — a chip in the composer tool row (`conversation.input.right`) with a popover for Standard/Flex, wait policy, cooldown rounds and fallback behaviour. It drives the proxy's own `config.json`, so it survives a DSH restart and needs no shipped-code change.
114
+ - **Configuration page** — the same browser half registers the bundle row's page on the Plugins page (`plugins.row.config`, keyed `deepinfra-fusion-proxy#deepinfra-proxy`). It opens with a status card read from the authenticated `api/deepinfra-proxy/status` route: whether the core runs and, if not, why (a busy port, a missing core, a crash, with its last output), the baseURL to give llm-pi-ai, and the version. Below it, the host Config over the shared `configForms` service: `enabled`, `port`, `upstream`, `stateDir`, `serverPath`. Those are connection fields and are deliberately *not* the same thing as the runtime mode above.
115
+ - **Lifecycle** — the core leaves with DSH even when DSH dies without stopping it (it watches a stdin pipe), so no orphan holds the port at the next start; a replacement instance (a hot swap, an update) waits for the old core to release the port; a core that keeps failing is retried five times with backoff and then stopped, with the reason on the page, until the next save.
116
+ - **Layout** — everything is bundle-relative: the core is `<bundle>/server.js`, the state dir defaults to `$DSH_HOME/deepinfra-proxy` (`config.json` / `locks.json`), and both are overridable through this plugin's own Config. `config/cordis.patch.example.yml` shows the profile-layer form; you normally edit the page instead.
117
+ - **Row id is load-bearing** — on DSH 0.1.7+ a plugin's settings namespace **is** its profile entry id, so the host row stays `deepinfra-proxy` (equal to the plugin name and to the legacy `settings.yaml` section, which the one-shot migration still imports).
118
+ - **Compatibility gate** — `peerDependencies: { "@deepseek-ai/dsh": ">=0.2.2-alpha.2 <0.3.0" }` (plus a declarative `engines.dsh`). Version **2.2+** requires DSH 0.2.2-alpha.2 or later for native npm updates: `.volatile()` needs `@deepseek-ai/schemastery` ≥ 3.18.4 and the old client `settingsScope` service is gone, so there is no 0.1.6 branch.
119
+ - **Point a route at it** — e.g. an `openai-completions` provider with `baseURL: http://127.0.0.1:8790/v1/openai` and `cacheRetention: long` (that is what supplies the per-session `prompt_cache_key`).
120
+
121
+ ## Configuration
122
+
123
+ State lives in `config.json` (auto-created with defaults on first boot):
124
+
125
+ ```jsonc
126
+ {
127
+ "defaultMode": "standard", // "standard" | "flex"
128
+ "coolLockRounds": 4, // 0-99: cooldown length after a 429 fallback
129
+ "sessions": { "<sessionId>": "flex" }, // per-session mode override
130
+ "fallbackAction": "stay", // "stay" | "cooldown"
131
+ "defaultWait": "failfast", // "failfast" | "wait"
132
+ "waitModes": { "<sessionId>": "wait" } // per-session wait override
133
+ }
134
+ ```
135
+
136
+ - **Mode resolution**: a cooldown lock for the session (→ `standard`) > `sessions[id]` > `defaultMode`.
137
+ - **Wait resolution**: `waitModes[id]` > `defaultWait`.
138
+ - `standard` never injects anything. `flex` injects `service_tier: "flex"` and `fail_fast` per the wait policy (`failfast` → `true`, `wait` → `false`).
139
+ - Unknown modes normalize to `standard` (forward-compatible).
140
+
141
+ ## Control API
142
+
143
+ All under `/__proxy/` (change the prefix with `DF_PROXY_CONTROL_PREFIX`). When `DF_PROXY_CONTROL_TOKEN` is set — the DSH host plugin always sets a fresh random one — every control call must carry it in `X-DFusion-Control` (the `/__dfusion/` bridge adds it); otherwise any web page could change the routing with a cross-site "simple" POST to 127.0.0.1. A standalone `npm start` sets none, so the examples below work as written.
144
+
145
+ | Endpoint | Description |
146
+ |---|---|
147
+ | `GET /__proxy/state` | Full current state: `defaultMode`, `coolLockRounds`, `sessions`, `locks`, `fallbackAction`, `defaultWait`, `waitModes`. |
148
+ | `POST /__proxy/config` | Update any config field. `sessions` / `waitModes` **merge by key** (`null` deletes a key), so toggling one session never wipes the others. |
149
+ | `POST /__proxy/unlock` | `{ "key": "…" }` or `{ "sessionId": "…" }` — clear a cooldown lock immediately. |
150
+
151
+ Example:
152
+
153
+ ```bash
154
+ # put a session on Flex with queueing
155
+ curl -X POST http://127.0.0.1:8790/__proxy/config \
156
+ -H 'content-type: application/json' \
157
+ -d '{"sessions":{"abc123":"flex"},"waitModes":{"abc123":"wait"}}'
158
+
159
+ # read it back
160
+ curl http://127.0.0.1:8790/__proxy/state
161
+ ```
162
+
163
+ If the proxy runs behind another HTTP server (e.g. same-origin for a web UI), forward `/__proxy/*` to it — the control endpoints are plain JSON.
164
+
165
+ ## Session key
166
+
167
+ The per-session key is the request body's `prompt_cache_key`. OpenAI-compatible clients that enable long cache retention (e.g. set `cacheRetention: "long"` with the matching capability flag) include it automatically as the raw session id (clamped to 64 chars). No session plumbing is needed in the client. Requests without a key fall back to `default` / `defaultMode`.
168
+
169
+ ## Running standalone
170
+
171
+ Every knob of the standalone core is an optional environment variable.
172
+
173
+ | Env | Default | Meaning |
174
+ |---|---|---|
175
+ | `DF_PROXY_PORT` | `8790` | Listen port. |
176
+ | `DF_PROXY_UPSTREAM` | `https://api.deepinfra.com/v1/openai` | Upstream base URL. |
177
+ | `DF_PROXY_BASE_PATH` | `/v1/openai` | Path prefix to strip before mapping onto the upstream. |
178
+ | `DF_PROXY_STATE_DIR` | script directory | Where `config.json` / `locks.json` live. |
179
+ | `DF_PROXY_CONTROL_PREFIX` | `/__proxy` | Prefix the control endpoints answer on (the DSH bundle's host plugin points its `/__dfusion/` browser bridge here). |
180
+
181
+ Point your client at `http://127.0.0.1:8790/v1/openai` and keep sending the normal DeepInfra `Authorization` header — the proxy relays it verbatim.
182
+
183
+ ## Updating
184
+
185
+ Use the DSH Plugins page to list npm versions and apply a compatible update. The plugin no longer checks GitHub or performs its own installation. See [PUBLISHING.md](PUBLISHING.md) for automatic publishing.
186
+
187
+ ## Upgrading from older versions
188
+
189
+ Follow the npm migration instructions above. Keep the same profile and row IDs; DeepInfra heals old absolute module paths at startup.
190
+
191
+ ## Security
192
+
193
+ - The proxy relays the `Authorization` header verbatim; it stores no credentials — read the API key from an env var in your client/host.
194
+ - The core binds to `127.0.0.1` only. Under DSH its control API needs the per-start token; the `/__dfusion/` bridge runs DSH's own Host/Origin and login check first (or, in a deployment without that service, answers loopback callers only).
195
+ - These are transport-level fields: injected request-body keys are never part of the model-visible text, so the "what the model sees is what gets logged" invariant is preserved.
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ npm test # or: node test/deepinfra-proxy-bundle-test.mjs && node test/run-wait-test.js
201
+ ```
202
+
203
+ `test/deepinfra-proxy-bundle-test.mjs` covers the bundle contract (no DSH, no network — except the localhost bridge it drives itself):
204
+
205
+ - **package layout** — `dsh.bundle.patch` declared, the one package-named row, the dual-face root package (`"."` host plugin, `"./client"` browser half), the icon + `locale/{en,zh}.json` page metadata, and the host plugin anchoring on bundle-relative paths with no machine-specific absolute path baked in;
206
+ - **the 0.1.7 settings contract** — the real schemastery `Config` parses into live `.get()` references with schema defaults, every field is `.volatile()`, the schema serializes for the form projection, and every field is editable in the browser half; plus static guards that the removed `ctx.settings.register` / `ctx.settings.get` / `settings/updated` APIs are gone;
207
+ - **the live host contract** — a mocked DSH context (`webServer` route registration, `loader/volatile-update`, the optional page policy) around the real plugin, which spawns the real `server.js`; the harness drives the `/__dfusion/` bridge over HTTP and asserts that a volatile `enabled: false` commit stops the supervised child, `enabled: true` restarts it, and disposal stops it;
208
+ - **2.1 fixes** — the core refuses control calls without the token, a client hang-up cancels the upstream request, the chat query string reaches upstream, the core exits when its parent's pipe closes, a busy port shows on the status route, a replacement instance waits for the old core's port, a stale pre-2.1 user-patch row is healed, and the release-tag logic picks and pins versions correctly.
209
+
210
+ `test/run-wait-test.js` spins up the bundled mock upstream (`test-mock-upstream.js`, records the received body, can force 429) and a proxy instance on throwaway ports with a throwaway state dir, then asserts:
211
+
212
+ - flex + fail-fast injects `service_tier: "flex"`, `fail_fast: true`
213
+ - flex + wait injects `fail_fast: false` (queueing)
214
+ - standard strips both fields
215
+ - 429 → retry as Standard + cooldown/stay fallback
216
+ - per-session maps merge (setting one session doesn't wipe others; `null` deletes)
217
+ - `defaultWait` / `waitModes` round-trip through the control API
218
+
219
+ The mock writes its debug scratch to the OS temp dir, and both suites clean their throwaway state (the host-contract harness kills the child it spawned) — the checkout stays pristine.
220
+
221
+ Running the harness needs the real schemastery, so install once:
222
+
223
+ ```bash
224
+ pnpm install # reads pnpm-workspace.yaml: autoInstallPeers=false keeps @deepseek-ai/dsh a peer only
225
+ ```
226
+
227
+ ## Related projects
228
+
229
+ - [dsh-remote-access-proxy](https://github.com/citydirector/dsh-remote-access-proxy): Use DSH from your phone, tablet, or another computer
230
+ - [dsh-loop-rescue](https://github.com/citydirector/dsh-loop-rescue): Cut model repetition loops mid-stream, prune them from the log, and resume
231
+ - [dsh-session-manager](https://github.com/citydirector/dsh-session-manager): Offline session manager: upgrade, archive, trash, loop cleanup, broken-tail repair
232
+
233
+ ## License
234
+
235
+ [MIT](LICENSE)
@@ -0,0 +1,232 @@
1
+ # deepinfra-fusion-proxy
2
+
3
+ [English](README.md) · [简体中文](README.zh-CN.md)
4
+
5
+ [![Release](https://img.shields.io/github/v/tag/citydirector/deepinfra-fusion-proxy?sort=semver&label=release)](https://github.com/citydirector/deepinfra-fusion-proxy/releases) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![DSH plugin](https://img.shields.io/badge/DSH%20plugin-%E2%89%A50.2.2--alpha.2-blue)](https://github.com/topics/dsh-plugin)
6
+
7
+ 一个零依赖的 [DeepInfra](https://deepinfra.com) API 本地反向代理:把服务档位(service tier)参数变成开箱即用的开关,并实现自动 `Standard ↔ Flex` 回退与按会话的冷却锁。同时以 **DSH(DeepSeek Harness)profile bundle** 形式发布(需 DSH ≥ 0.2.2-alpha.2),DSH 可从 npm 一次装好代理、按会话切换卡片与 bundle 行的配置页。
8
+
9
+ 插件需要 **DSH 0.1.7-rc.1 或更新**(2.x 不再兼容 0.1.6);单独运行代理只需要 **Node.js 18+**。
10
+
11
+ ## npm 安装与更新
12
+
13
+ 需要 DSH 0.2.2-alpha.2 或更新的 0.2.x,以及 Node.js 24.9+。在 DSH「插件 → 添加插件」输入 `deepinfra-fusion-proxy`,或运行:
14
+
15
+ ```sh
16
+ dsh plugin --profile web add deepinfra-fusion-proxy
17
+ ```
18
+
19
+ 旧版从 GitHub 安装的用户:先备份 profile 的 `cordis.patch.yml`,在插件页移除旧包并用上述 npm 包名重新添加,保持原 profile 与配置行 ID。配置仍由 profile 保存;重新添加后检查已保存的值。此后统一使用 DSH 插件页的版本列表与更新功能。插件不再轮询 GitHub、不再提供独立安装或回退按钮,旧 `updateCheck` 设置不再生效。DeepInfra 的运行数据目录保持原路径。
20
+
21
+ ## 快速开始
22
+
23
+ 1. **安装**:侧边栏 **插件** → 添加插件,填入
24
+ ```
25
+ deepinfra-fusion-proxy
26
+ ```
27
+ 装完打开开关。代理随 DSH 启动,监听 `127.0.0.1:8790`。
28
+ 2. **把路由指过来**:一个 `openai-completions` provider,`baseURL: http://127.0.0.1:8790/v1/openai`,并设 `cacheRetention: long`(每个会话的 `prompt_cache_key` 由此而来)。DeepInfra 的 API Key 照常填在 provider 上。
29
+ 3. **按会话选档位**:会话输入区工具行里的 chip 可以切 Standard / Flex、等待策略和回退方式。
30
+
31
+ 命令行安装:
32
+
33
+ ```bash
34
+ dsh plugin --profile web add deepinfra-fusion-proxy
35
+ ```
36
+
37
+ 不用 DSH 时也可以单独运行(内核零依赖,不需要安装步骤):
38
+
39
+ ```bash
40
+ git clone https://github.com/citydirector/deepinfra-fusion-proxy.git
41
+ cd deepinfra-fusion-proxy
42
+ node server.js # 或:npm start
43
+ ```
44
+
45
+ ## 为什么需要它
46
+
47
+ DeepInfra 会给部分模型标注服务档位(参考 [Chat Completions 概览](https://docs.deepinfra.com/chat/overview)、[Priority Service Tier](https://deepinfra.com/blog/priority-service-tier)):
48
+
49
+ | 参数 | 取值 | 语义 |
50
+ |---|---|---|
51
+ | `service_tier` | `"flex"` \| `"priority"` | `flex` = 便宜、尽力而为(容量满时入队);`priority` = 保证容量、更贵。响应会把实际生效的档位回显在 `response.service_tier`。 |
52
+ | `fail_fast` | `true` \| `false` | 默认 `false`(排队)。为 `true` 时,容量不足直接返回 **HTTP 429**,而不是入队等待。 |
53
+
54
+ `fail_fast: true` 把"容量问题"变成可检测的 `429`,`service_tier` 的回显让你知道实际走了哪个档位——本代理正是利用这两点来实现回退引擎。
55
+
56
+ 大多数客户端无法发送这些顶层字段,想在"按请求/按会话"粒度切换档位通常得改客户端代码。本代理把这两件事集中到一处。
57
+
58
+ ## 特性
59
+
60
+ - **请求体注入** — 向 `POST /v1/openai/chat/completions` 请求注入 `service_tier` / `fail_fast`;绝不触碰模型可见的对话文本。
61
+ - **按会话路由** — 读取请求体里的 `prompt_cache_key`(开启长缓存保留的客户端会自动携带)作为会话 id,因此路由与冷却锁天然按会话生效,客户端零改动。
62
+ - **自动回退** — Flex 尝试遇到 `429` 后,以 Standard 重试一次,然后要么停在 Standard(`fallbackAction: "stay"`),要么挂上按会话的冷却锁(`fallbackAction: "cooldown"`),N 轮后自动回到 Flex。
63
+ - **等待策略** — 按会话选择 **立即失败**(`fail_fast: true`,忙时立刻 429 → 回退)或 **排队等待**(`fail_fast: false`,忙时入队,DeepInfra 上限约 10 分钟)。
64
+ - **控制 API** — 提供状态 / 配置 / 解锁冷却锁的 JSON 端点,GUI 或脚本可免重启实时驱动。
65
+ - **DSH 0.1.7 配置模型** — 宿主插件用自己的 Cordis `Config` 声明连接字段(全部 `.volatile()`),因此在 0.1.7+ 上这些值落在活动 profile 的 `cordis.patch.yml` 里,改动是**原地重启**被监管的子进程,而不是重挂插件。bundle 同时提供该行的配置页(插件页 → `deepinfra-fusion-proxy` → `deepinfra-proxy` → 配置)。
66
+ - **内核零依赖** — 仅用 Node 标准库(`http`/`https`/`fs`/`path`),`server.js` 从检出目录直接就能跑、无需安装。(它旁边的 DSH 宿主插件会 import 宿主自带的 `@deepseek-ai/schemastery`,已声明为依赖,bundle 安装时由 pnpm 解析。)
67
+
68
+ ## 工作原理
69
+
70
+ ```
71
+ OpenAI 兼容客户端(baseURL → http://127.0.0.1:8790/v1/openai)
72
+ │
73
+ ▼
74
+ [ deepinfra-fusion-proxy (127.0.0.1:8790) ]
75
+ ├─ 读取 config.json:defaultMode、sessions{}、defaultWait、waitModes{}…
76
+ ├─ 按(模式 × 等待策略)注入 service_tier / fail_fast
77
+ ├─ 转发到 https://api.deepinfra.com/v1/openai(保留 Authorization)
78
+ └─ 遇到 429(非 Standard):以 Standard 重试一次,然后按 fallbackAction
79
+ 停留 / 挂冷却锁
80
+ ```
81
+
82
+ - `chat/completions` POST 请求会得到注入与回退处理。
83
+ - 其它请求(`GET /models`、embeddings 等)原样透传。
84
+ - SSE 响应以正确的分块与响应头转发;上游出错返回 `502`。
85
+
86
+ ## DSH bundle 说明
87
+
88
+ 本仓库**本身就是**一个 bundle 包(含 `dsh.bundle.patch`),所以 DSH profile 可以直接从 npm 安装:插件页解析补丁、把该 bundle 加入 profile 的层栈,pnpm 一次性落地 `server.js`、宿主插件与切换卡片。
89
+
90
+ bundle 插入的内容(`cordis.patch.yml`,一行,按 id 定位——用户层写同一 id 即可覆盖):
91
+
92
+ ```yaml
93
+ - insert:
94
+ - id: deepinfra-proxy
95
+ name: deepinfra-fusion-proxy # 包本身:"." = 宿主插件,"./client" = 切换卡片 + 配置页
96
+ ```
97
+
98
+ 这一行写的是**包名**而不是 `./` 路径:DSH 会把相对路径解析成绝对 file URL,配置编辑器第一次保存时
99
+ 还会把它抄进用户 patch,绿色版 DSH 目录一搬家就失效。2.1 之前的版本正是这样(另外还多一行
100
+ `deepinfra-proxy-ui`);2.1 首次启动时会把用户 patch 里这种旧行改回包名。
101
+
102
+ 安装依赖并不等于启用 bundle:只有当 profile 的 `package.json` 把 `deepinfra-fusion-proxy` 列进
103
+ `dsh.profile.bundles` 时,上面那一行才会被加载。只加 `dependencies`(例如手工 `pnpm add`)会得到一个
104
+ 没有任何行、也没有代理的 profile —— 插件页会替你写 bundle 条目,手工安装则需要自己补上。
105
+
106
+ 共用同一个 `$DSH_HOME` 的两个 profile(便携版与桌面端指向同一个 `data` 目录)都会默认使用
107
+ `$DSH_HOME/deepinfra-proxy` 作为状态目录。给每个 profile 设不同的 `stateDir` 与 `port` 即可互不影响。
108
+
109
+ - **宿主插件** — 随 DSH 启停代理内核、异常退出按退避重启、卸载时释放端口,并注册卡片所用的同源 `/__dfusion/*` → `/__proxy/*` 桥接。
110
+ - **切换卡片** — 会话输入区工具行(`conversation.input.right`)的 chip,弹出面板可切 Standard/Flex、等待策略、冷却轮数与 429 回退方式。它驱动代理自身的 `config.json`,重启 DSH 仍保留,且不需要改任何随包发布的代码。
111
+ - **配置页** — 同一个浏览器半边还在插件页注册了该 bundle 行的配置页(`plugins.row.config`,key = `deepinfra-fusion-proxy#deepinfra-proxy`)。最上面是状态卡,读自带鉴权的 `api/deepinfra-proxy/status` 路由:内核是否在运行、没运行的原因(端口被占、找不到内核、意外退出,附最近输出)、给 llm-pi-ai 填的 baseURL、当前版本。下面是走共享 `configForms` 服务的宿主 Config:`enabled`、`port`、`upstream`、`stateDir`、`serverPath`。这些是**连接字段**,与上面那个运行时档位刻意分开。
112
+ - **生命周期** — DSH 没来得及停它就挂了,内核也会跟着退出(它盯着一条 stdin 管道),下次启动不会被孤儿进程占住端口;替换实例(热替换、更新)会等旧内核放开端口;内核反复失败时按退避重试 5 次后停下,原因显示在配置页,下次保存再重新启动。
113
+ - **目录约定** — 全部相对 bundle:内核是 `<bundle>/server.js`,状态目录默认 `$DSH_HOME/deepinfra-proxy`(`config.json` / `locks.json`),二者都可在插件自己的 Config 里覆盖。`config/cordis.patch.example.yml` 给出 profile 层的写法;通常你直接在配置页改就行。
114
+ - **行 id 是承重的** — 0.1.7+ 里插件的设置命名空间**就是**它的 profile entry id,所以宿主行保持 `deepinfra-proxy`(与插件名、与旧 `settings.yaml` 段名三者同名,一次性迁移仍能导入)。
115
+ - **兼容门禁** — `peerDependencies: { "@deepseek-ai/dsh": ">=0.2.2-alpha.2 <0.3.0" }`(另有声明性的 `engines.dsh`)。**2.2+** 需要 DSH 0.2.2-alpha.2 或更新版本以使用原生 npm 更新:`.volatile()` 需要 `@deepseek-ai/schemastery` ≥ 3.18.4,且旧的客户端 `settingsScope` 服务已移除,因此不做 0.1.6 双兼容。
116
+ - **把路由指过来** — 例如一个 `openai-completions` provider,`baseURL: http://127.0.0.1:8790/v1/openai` 且 `cacheRetention: long`(按会话的 `prompt_cache_key` 正是由此而来)。
117
+
118
+ ## 配置
119
+
120
+ 状态保存在 `config.json`(首次启动自动创建默认值):
121
+
122
+ ```jsonc
123
+ {
124
+ "defaultMode": "standard", // "standard" | "flex"
125
+ "coolLockRounds": 4, // 0-99:429 回退后的冷却轮数
126
+ "sessions": { "<sessionId>": "flex" }, // 按会话的模式覆盖
127
+ "fallbackAction": "stay", // "stay" | "cooldown"
128
+ "defaultWait": "failfast", // "failfast" | "wait"
129
+ "waitModes": { "<sessionId>": "wait" } // 按会话的等待策略覆盖
130
+ }
131
+ ```
132
+
133
+ - **模式解析顺序**:该会话的冷却锁(→ `standard`)> `sessions[id]` > `defaultMode`。
134
+ - **等待策略解析顺序**:`waitModes[id]` > `defaultWait`。
135
+ - `standard` 不注入任何字段;`flex` 注入 `service_tier: "flex"`,并按等待策略注入 `fail_fast`(`failfast` → `true`,`wait` → `false`)。
136
+ - 未知模式归一为 `standard`(向前兼容)。
137
+
138
+ ## 控制 API
139
+
140
+ 全部位于 `/__proxy/` 下(可用 `DF_PROXY_CONTROL_PREFIX` 改前缀)。设置了 `DF_PROXY_CONTROL_TOKEN` 时——DSH 宿主插件每次启动都会设一个新的随机值——所有控制请求都必须在 `X-DFusion-Control` 头里带上它(`/__dfusion/` 桥接会自动加);否则任何网页都能用跨站的「简单请求」POST 到 127.0.0.1 改路由。单独 `npm start` 不设,下面的示例照常可用。
141
+
142
+ | 端点 | 说明 |
143
+ |---|---|
144
+ | `GET /__proxy/state` | 当前完整状态:`defaultMode`、`coolLockRounds`、`sessions`、`locks`、`fallbackAction`、`defaultWait`、`waitModes`。 |
145
+ | `POST /__proxy/config` | 更新任意配置字段。`sessions` / `waitModes` 为**按键合并**(传 `null` 删除某个 key),切换一个会话不会清掉其它会话。 |
146
+ | `POST /__proxy/unlock` | `{ "key": "…" }` 或 `{ "sessionId": "…" }` — 立即清除某个冷却锁。 |
147
+
148
+ 示例:
149
+
150
+ ```bash
151
+ # 将会话设为 Flex 并排队等待
152
+ curl -X POST http://127.0.0.1:8790/__proxy/config \
153
+ -H 'content-type: application/json' \
154
+ -d '{"sessions":{"abc123":"flex"},"waitModes":{"abc123":"wait"}}'
155
+
156
+ # 读回状态
157
+ curl http://127.0.0.1:8790/__proxy/state
158
+ ```
159
+
160
+ 如果代理跑在另一个 HTTP 服务后面(例如给 Web UI 提供同源访问),把 `/__proxy/*` 转发给它即可——控制端点就是普通 JSON。
161
+
162
+ ## 会话键
163
+
164
+ 按会话的 key 取自请求体的 `prompt_cache_key`。开启长缓存保留的 OpenAI 兼容客户端(例如设置 `cacheRetention: "long"` 并带对应能力标记)会自动把它带上,值即原始会话 id(截断到 64 字符)。客户端无需额外会话管道。没有 key 的请求回落到 `default` / `defaultMode`。
165
+
166
+ ## 单独运行
167
+
168
+ 单独运行时,每个参数都是可选的环境变量。
169
+
170
+ | 环境变量 | 默认值 | 含义 |
171
+ |---|---|---|
172
+ | `DF_PROXY_PORT` | `8790` | 监听端口。 |
173
+ | `DF_PROXY_UPSTREAM` | `https://api.deepinfra.com/v1/openai` | 上游基地址。 |
174
+ | `DF_PROXY_BASE_PATH` | `/v1/openai` | 映射到上游前要剥掉的路径前缀。 |
175
+ | `DF_PROXY_STATE_DIR` | 脚本所在目录 | `config.json` / `locks.json` 存放位置。 |
176
+ | `DF_PROXY_CONTROL_PREFIX` | `/__proxy` | 控制端点响应的前缀(DSH bundle 的宿主插件把 `/__dfusion/` 浏览器桥接到这里)。 |
177
+
178
+ 把你的客户端指向 `http://127.0.0.1:8790/v1/openai`,继续照常发送 DeepInfra 的 `Authorization` 头——代理会原样透传。
179
+
180
+ ## 更新
181
+
182
+ 更新由 DSH「插件」页统一管理:查看 npm 版本列表、选择兼容版本并更新。插件自身不再检查 GitHub 或执行安装。发布配置见 [PUBLISHING.md](PUBLISHING.md)。
183
+
184
+ ## 从旧版本升级
185
+
186
+ 从 GitHub 来源迁移的步骤见本页开头的 npm 安装说明。保留原 profile 与配置行 ID;DeepInfra 的旧绝对路径配置会在启动时修正。
187
+
188
+ ## 安全
189
+
190
+ - 代理原样透传 `Authorization` 头,不保存任何凭据——请在客户端/宿主里用环境变量读取 API key。
191
+ - 内核只监听 `127.0.0.1`。在 DSH 里,控制 API 需要每次启动生成的 token;`/__dfusion/` 桥接先跑 DSH 自己的 Host/Origin 与登录校验(没有这个服务的部署里,只回应本机来源)。
192
+ - 这些是传输层字段:注入的请求体 key 永远不会出现在模型可见文本里,因此"模型所见即所记"的不变式保持不变。
193
+
194
+ ## 开发与测试
195
+
196
+ ```bash
197
+ npm test # 或:node test/deepinfra-proxy-bundle-test.mjs && node test/run-wait-test.js
198
+ ```
199
+
200
+ `test/deepinfra-proxy-bundle-test.mjs` 覆盖 bundle 契约(不依赖 DSH、不联外网——只驱动它自己起的 localhost 桥接):
201
+
202
+ - **打包形态** — 声明 `dsh.bundle.patch`、唯一一行写的是包名、根包是双面模块(`"."` 宿主插件,`"./client"` 浏览器半边)、图标与 `locale/{en,zh}.json` 页面元数据齐备、宿主插件只用 bundle 相对路径且没有写死机器绝对路径;
203
+ - **0.1.7 设置契约** — 用**真** schemastery 断言 `Config({})` 返回带默认值的活引用、每个字段都是 `.volatile()`、schema 能序列化给表单投影、且每个字段在浏览器半边都可编辑;另有静态守卫确认已移除的 `ctx.settings.register` / `ctx.settings.get` / `settings/updated` 旧 API 不再出现;
204
+ - **实机宿主契约** — 用 mock 的 DSH 上下文(`webServer` 路由注册、`loader/volatile-update`、可选的页面策略)包住真插件,由它拉起真的 `server.js`;测试台再通过 HTTP 驱动 `/__dfusion/` 桥接,断言 volatile 提交 `enabled: false` 会停掉被监管的子进程、`enabled: true` 会重启它、dispose 会停掉它;
205
+ - **2.1 修复** — 内核拒绝不带 token 的控制请求、客户端断开会取消上游请求、chat 请求的查询串会带到上游、父进程管道关闭时内核退出、端口被占会出现在状态路由里、替换实例会等旧内核放开端口、2.1 之前的旧用户 patch 行会被修复、发版 tag 的选取与锁定正确。
206
+
207
+ `test/run-wait-test.js` 会拉起仓库自带的 mock 上游(`test-mock-upstream.js`,记录收到的请求体、可强制 429)和一个使用一次性端口/状态目录的代理实例,然后断言:
208
+
209
+ - flex + 立即失败注入 `service_tier: "flex"`、`fail_fast: true`
210
+ - flex + 等待注入 `fail_fast: false`(排队)
211
+ - standard 剔除这两个字段
212
+ - 429 → 以 Standard 重试 + 冷却/停留回退
213
+ - 按会话映射按键合并(设置一个会话不会清掉其它会话;`null` 删除)
214
+ - `defaultWait` / `waitModes` 经控制 API 往返一致
215
+
216
+ mock 把调试残留写到系统临时目录,两套测试都会清理自己的一次性状态(宿主契约测试台会杀掉它拉起的子进程)——检出目录始终保持整洁。
217
+
218
+ 测试台要用真的 schemastery,先装一次:
219
+
220
+ ```bash
221
+ pnpm install # 读 pnpm-workspace.yaml:autoInstallPeers=false,@deepseek-ai/dsh 只作 peer 声明
222
+ ```
223
+
224
+ ## 相关项目
225
+
226
+ - [dsh-remote-access-proxy](https://github.com/citydirector/dsh-remote-access-proxy):在手机、平板或另一台电脑上使用 DSH
227
+ - [dsh-loop-rescue](https://github.com/citydirector/dsh-loop-rescue):模型复读时从流内切断、剪掉循环并自动续跑
228
+ - [dsh-session-manager](https://github.com/citydirector/dsh-session-manager):离线会话管理器,可升级、归档、清洗循环、修复烂尾会话
229
+
230
+ ## 许可证
231
+
232
+ [MIT](LICENSE)