dsh-magpie-connector 0.1.1 → 0.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/CHANGELOG.md +21 -0
- package/README.md +11 -12
- package/README.zh.md +13 -12
- package/docs/SCREENSHOTS.md +2 -1
- package/docs/architecture.en.json +3 -3
- package/docs/architecture.en.svg +3 -3
- package/docs/architecture.zh.json +5 -5
- package/docs/architecture.zh.svg +6 -6
- package/docs/cloudflare-524-long-requests.md +172 -0
- package/docs/internal-api.md +32 -8
- package/lib/client/SettingsPage.js +80 -47
- package/lib/client/styles.d.ts +1 -1
- package/lib/client/styles.js +9 -0
- package/lib/client/view-model.d.ts +32 -2
- package/lib/client.js +211 -98
- package/lib/host/adapter.js +2 -2
- package/lib/host/cards.js +1 -1
- package/lib/host/catalog.d.ts +9 -0
- package/lib/host/catalog.js +12 -3
- package/lib/host/routes.js +9 -6
- package/lib/host/runtime.d.ts +51 -9
- package/lib/host/runtime.js +354 -87
- package/lib/host/selection.d.ts +48 -0
- package/lib/host/selection.js +148 -0
- package/lib/host/settings.d.ts +34 -2
- package/lib/host/settings.js +70 -1
- package/lib/host/transport.d.ts +7 -0
- package/lib/host/transport.js +14 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +3 -4
- package/package.json +1 -1
- package/screenshots.json +1 -2
- package/docs/settings.gif +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
**BREAKING:** the settings format changes from a single top-level `baseUrl` to an ordered `lines[]` list, and credentials move to per-line derived refs. Existing `settings.json` files with only `baseUrl` and the legacy fixed refs (`MAGPIE_API_KEY`, `MAGPIE_ACCESS_CLIENT_ID`, `MAGPIE_ACCESS_CLIENT_SECRET`) are **not read, migrated or deleted**; reconfigure the connection in Settings after upgrading. `visibleModels` / `defaultEfforts` are preserved. Back up `settings.json` and the old credential refs before upgrading if you need a rollback path.
|
|
6
|
+
|
|
7
|
+
- **Multiple connection lines.** Settings hold an ordered list of 0–5 lines (empty on first install; normally one), each with its own address, gateway key and access mode (`direct` or Cloudflare Access). Array order is the priority order used for automatic selection; moving lines does not change their identity or credentials.
|
|
8
|
+
- **Stable random line ids.** The host generates an 8-hex-character id when a line is created (unique among existing and pending lines); it is stored in settings, never shown in the UI (the UI shows the 1-based position) and never changes when the address, key, order, or another line changes.
|
|
9
|
+
- **Per-line credentials.** Every line uses `MAGPIE_LINE_<ID>_API_KEY` / `MAGPIE_LINE_<ID>_ACCESS_CLIENT_ID` / `MAGPIE_LINE_<ID>_ACCESS_CLIENT_SECRET` (uppercase id). Deleting a line clears its writable refs; environment-owned values are reported through `retainedCredentials`. New lines omit `id` in POST bodies and are assigned one by the host.
|
|
10
|
+
- **Automatic line selection.** Before each gateway operation the first reachable complete line (priority order) is chosen with a ≤600ms TCP probe (10s positive / 15s negative in-memory cache, no background polling). A single-line setup never probes. If all lines are unreachable the first line carries the real request and the error surfaces as-is.
|
|
11
|
+
- **Line switching keeps the catalog.** Switching lines carries the current model catalog over (no empty flash), clears usage cards and re-syncs the new line. Editing a non-active line does not clear the catalog or trigger a sync; editing the active line keeps the identity-change semantics. Generated requests are never replayed or switched: a connection-phase failure only invalidates that line's probe cache.
|
|
12
|
+
- **Per-line Cloudflare Access.** Access is enabled per line and sent only to HTTPS lines that enable it; plain-HTTP lines never carry the service token. Switching a line back to Direct clears its saved Access pair on save.
|
|
13
|
+
- **Load validation and write safety.** Malformed/duplicate persisted lines or pending records make the whole connection read as unconfigured (no requests, no credential resolution, file untouched). Credential writes are persisted as `(lineId, field)` pending entries before secrets are written; partial failures keep the pending quarantine across restarts and require an explicit complete retry. Removed/new line ids are cleaned through that quarantine.
|
|
14
|
+
- **Settings UI.** Line cards with the active line expanded by default, a 4×4 green active marker after its number, an info tooltip explaining priority, add/move/delete controls, an empty state with an add button for first install, and silent switching (no toast/alert).
|
|
15
|
+
- **Docs/tests.** README, architecture diagrams, internal API notes and the CHANGELOG document the breaking upgrade and reconfiguration steps; the old settings screenshot was removed pending re-recording.
|
|
16
|
+
|
|
17
|
+
## 0.1.2
|
|
18
|
+
|
|
19
|
+
- Default request output budget now reserves context space for DSH compaction: `min(max_output_tokens, max(1, context_window/4, context_window/2 − 65536))`. The gateway's capacity stays the pi-ai model output limit, so the model's real ceiling is unchanged.
|
|
20
|
+
- On a live gateway, `codex/gpt-6-astra` and `codex/gpt-6-luna` (272k window / 128k capacity) move their compaction threshold from 78,464 to 136,000 tokens; `opencode-go/minimax-m3.1-flash` (1M / 512k) from 422,464 to 500,000.
|
|
21
|
+
- An explicit `maxTokens` passes through unchanged; DSH still fills the default when a request omits it.
|
|
22
|
+
- Models with a window below 104,026 tokens are not guaranteed to pass DSH's two compaction checks; they are still mapped with the same formula rather than dropped.
|
|
23
|
+
|
|
3
24
|
## 0.1.1
|
|
4
25
|
|
|
5
26
|
- Fix README images on npm: `repository` no longer declares a monorepo `directory`, so relative image links resolve against the standalone repository root.
|
package/README.md
CHANGED
|
@@ -4,10 +4,9 @@ English · [简体中文](README.zh.md)
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<img src="docs/model-picker.gif" alt="Model picker grouped by upstream provider, with the GPT-only standard/fast control" width="49%">
|
|
7
|
-
<img src="docs/settings.gif" alt="Settings: connection, Cloudflare Access, provider groups and usage cards" width="49%">
|
|
8
7
|
</p>
|
|
9
8
|
|
|
10
|
-
*
|
|
9
|
+
*Model picker recorded from an isolated DSH host with a local mock Magpie gateway; models and accounts are synthetic. A fresh settings screenshot is pending re-recording.*
|
|
11
10
|
|
|
12
11
|
[Repository](https://github.com/prgrmrwy/dsh-magpie-connector) · [Releases](https://github.com/prgrmrwy/dsh-magpie-connector/releases) · [npm](https://www.npmjs.com/package/dsh-magpie-connector)
|
|
13
12
|
|
|
@@ -18,9 +17,10 @@ English · [简体中文](README.zh.md)
|
|
|
18
17
|
|
|
19
18
|
| Capability | What it does |
|
|
20
19
|
|---|---|
|
|
21
|
-
| **Gateway
|
|
20
|
+
| **Gateway lines** (basic) | Each line has its own URL and gateway key. The model catalog comes from the gateway's `GET /models`; Responses-capable models use Responses, everything else uses Chat Completions. Text, tool round trips, images, usage, cancellation and DSH-managed retries go through. |
|
|
21
|
+
| **Automatic multi-line failover** (signature) | Empty on first install; normally one line; add backup lines and let the order decide priority. Before every gateway operation the plugin picks the first reachable complete line with a ≤600ms TCP probe (10s positive / 15s negative in-memory cache, no background polling), so home uses the LAN while outside falls back to the public endpoint. Switching lines carries the catalog over (no flash), clears usage cards and re-syncs the new line; single-line users keep 0.1.x behavior. |
|
|
22
22
|
| **Grouped by upstream provider** (signature) | The model picker shows `Magpie (Claude)`, `Magpie (GPT)` and `Magpie (<other owner>)` headings while the real route stays one `dsh-magpie-connector`. Settings and usage cards use the same grouping, and a provider that only appears in usage is never dropped. |
|
|
23
|
-
| **Cloudflare Access
|
|
23
|
+
| **Per-line Cloudflare Access** | Each line independently chooses Direct or Cloudflare Access. Access-enabled line requests carry the service token pair plus the bearer key; the token is never sent to a plain-HTTP line. |
|
|
24
24
|
| **Read-only usage cards** | Each upstream account shows plan, expiry, window readings and Codex reset credits when the gateway provides them. Cards are read once when settings open and only re-read when you press **Refresh** on that card; bars turn orange above 70% and red at 90%. |
|
|
25
25
|
| **Model visibility and default effort** | Per provider, leave visibility untouched to follow the gateway catalog, or save an explicit allowlist so new models stay hidden. Per model, save a default reasoning effort; DSH applies it on the next request. |
|
|
26
26
|
| **Per-session fast mode** | Models advertising the `priority` service tier get a standard/fast switch next to the composer. The choice is per session and in host memory only; it adds `service_tier: "priority"` to requests. |
|
|
@@ -39,14 +39,12 @@ The model picker headings need the managed five-package Host extension that ohmy
|
|
|
39
39
|
|
|
40
40
|
Open **Settings → Magpie**.
|
|
41
41
|
|
|
42
|
-
1.
|
|
43
|
-
2.
|
|
44
|
-
3.
|
|
42
|
+
1. In **line 1**, enter the gateway API URL **including `/v1`**, for example `http://192.168.1.20:3425/v1`, and that line's gateway key. The line list, addresses, visibility and default efforts live at `$DSH_HOME/plugins/dsh-magpie-connector/settings.json` — keys and Access values are never written there or returned by the settings API.
|
|
43
|
+
2. For multiple ingresses press **Add backup line**; each line has its own address and key. Use the move-up/move-down buttons to change priority (number 1 wins). Saving keeps only the active line expanded; the 4×4 green marker after its number shows the current line, and the info icon explains priority and automatic fallback.
|
|
44
|
+
3. Inside each line's **Advanced connection** choose Direct or Cloudflare Access. LAN and Tailscale are Direct; Cloudflare Access requires an HTTPS line and switching back to Direct clears that line's two Access credentials on save. Cloudflare Free/Pro returns 524 when the origin sends nothing for 100 seconds.
|
|
45
|
+
4. Saving synchronizes the catalog; **Refresh model list** synchronizes again, and there is no catalog polling.
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- **Direct** (default) — the gateway URL is reachable as-is. LAN and Tailscale both count: after joining the tailnet use `http://<host>.<tailnet>.ts.net:3425/v1` or the subnet-routed address; nothing else is configured in the plugin.
|
|
49
|
-
- **Cloudflare Access** — for Cloudflare Tunnel + Access when the client cannot run a VPN. Enter the Access service token pair; every request carries both headers plus the bearer key. Saving after switching back to Direct clears the saved Access credentials. Cloudflare Free/Pro returns 524 when the origin sends nothing for 100 seconds.
|
|
47
|
+
> **0.2.0 BREAKING upgrade**: settings are now a single `lines[]` format. A legacy single-address `settings.json` (top-level `baseUrl`) and the legacy fixed credential refs (`MAGPIE_API_KEY` / `MAGPIE_ACCESS_*`) are **not read or migrated**; re-add your lines and enter each line's key/Access in Settings. Model visibility and default efforts are preserved; old credential records can be deleted manually in DSH credentials. Back up `settings.json` and the old refs first if you want to keep a rollback path.
|
|
50
48
|
|
|
51
49
|
Need another method — custom headers, mTLS, another zero-trust gateway? **Issues and pull requests are welcome**; describe your deployment and the gateway version.
|
|
52
50
|
|
|
@@ -63,6 +61,7 @@ Need another method — custom headers, mTLS, another zero-trust gateway? **Issu
|
|
|
63
61
|
### Behavior worth knowing
|
|
64
62
|
|
|
65
63
|
- **Failed requests are retried by DSH, not replayed by the plugin.** The adapter declares the same `llm-retry` policy as `dsh-plugin-subscriptions` (10 attempts, exponential backoff from 1s to 60s, ±20% jitter, honoring `Retry-After`). `TIMEOUT`, `TRANSPORT`, `SERVER`, `RATE_LIMIT` and `EMPTY_RESPONSE` are retryable; 401/403, exhausted quota, context overflow and other 4xx are not.
|
|
64
|
+
- **The default output budget reserves room for compaction.** The gateway's `max_output_tokens` is the model's capacity, not the per-request default. The default budget handed to DSH is `min(capacity, max(1, window/4, window/2 − 65536))`, leaving context space for DSH compaction; an explicit `maxTokens` is unaffected. Models with a window below 104,026 tokens follow the same formula but are not guaranteed to pass DSH's compaction checks.
|
|
66
65
|
- **Only silence ends a stream.** Headers, SSE events and heartbeats all count as liveness; 300 seconds without any data ends the request with `TIMEOUT` (message contains `MAGPIE_STREAM_TIMEOUT`). Long reasoning survives as long as the gateway keeps the stream alive.
|
|
67
66
|
- **Connection changes do not abort work.** A new address or key immediately hides the old catalog and cards, but generations already prepared or running finish with their captured address and key. User cancellation and plugin disposal still stop them.
|
|
68
67
|
- **The transport is deliberately narrow.** Host requests go through a private `undici` dispatcher straight to the configured gateway, never through a global proxy dispatcher. Redirects are rejected; only `GET /models`, `POST /responses`, `POST /chat/completions`, `GET /magpie/quotas/cards` and `POST /magpie/quotas/refresh` are allowed; Magpie GUI `/api/*` management endpoints are never called. Provider egress proxies belong in Magpie.
|
|
@@ -93,7 +92,7 @@ Tests use local mock gateways and the real wire library; no real key is required
|
|
|
93
92
|
dsh plugin --profile web remove dsh-magpie-connector
|
|
94
93
|
```
|
|
95
94
|
|
|
96
|
-
Restart DSH and refresh the page. Disposal releases registrations, timers and connections; it does not remove credentials or settings. To erase data, delete
|
|
95
|
+
Restart DSH and refresh the page. Disposal releases registrations, timers and connections; it does not remove credentials or settings. To erase data, delete each line's `MAGPIE_LINE_<ID>_*` refs (plus any pre-0.2.0 `MAGPIE_API_KEY` / `MAGPIE_ACCESS_*` leftovers) through DSH credentials management and remove the plugin settings file after locating it from the installed plugin's data directory. Do not remove a credential if another integration still uses it.
|
|
97
96
|
|
|
98
97
|
Inside [ohmydsh](https://github.com/prgrmrwy/ohmydsh), set the manifest entry to `enabled: false` and rebuild instead.
|
|
99
98
|
|
package/README.zh.md
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<img src="docs/model-picker.gif" alt="模型选择器:按上游 provider 分组,仅 GPT 显示标准/快速" width="49%">
|
|
7
|
-
<img src="docs/settings.gif" alt="设置页:连接、Cloudflare Access、分组配置与用量卡片" width="49%">
|
|
8
7
|
</p>
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
*模型选择器录屏来自隔离 DSH 宿主与本地 mock Magpie 网关,模型和账号均为演示数据;设置页的线路列表截图待重录。*
|
|
11
10
|
|
|
12
11
|
[插件仓库](https://github.com/prgrmrwy/dsh-magpie-connector) · [版本发布](https://github.com/prgrmrwy/dsh-magpie-connector/releases) · [npm](https://www.npmjs.com/package/dsh-magpie-connector)
|
|
13
12
|
|
|
@@ -18,9 +17,10 @@
|
|
|
18
17
|
|
|
19
18
|
| 能力 | 具体行为 |
|
|
20
19
|
|---|---|
|
|
21
|
-
| **Magpie
|
|
20
|
+
| **Magpie 网关接入**(基础) | 每条线路只需地址和该线路的 key。模型目录来自网关 `GET /models`;支持 Responses 的模型走 Responses,其余走 Chat Completions。文本、工具往返、图片、usage、取消以及 DSH 统一重试都能正常工作。 |
|
|
21
|
+
| **多线路自动切换**(特色) | 通常一条线路(首次安装为空,最多 5 条),可添加备用线路并按序号自动选择:每次操作前对候选地址做 ≤600ms 的 TCP 可达探测(正缓存 10s / 负缓存 15s,不后台轮询),在家走局域网、在外自动落到公网。切换线路时模型目录继承不闪断、用量卡片清空并按新线路重读;单线路(通常一条)行为与旧版一致。 |
|
|
22
22
|
| **按上游 provider 分组**(特色) | 模型选择器显示 `Magpie (Claude)`、`Magpie (GPT)`、`Magpie (<其它归属>)` 等标题,真实 route 始终只有一个 `dsh-magpie-connector`。设置页和用量卡片使用同一套分组,只出现在用量里的 provider 也不会被丢弃。 |
|
|
23
|
-
|
|
|
23
|
+
| **按线路的 Cloudflare Access** | 每条线路可单独选择「直连」或「Cloudflare Access」。启用后该线路的请求带上服务令牌两项并保留 bearer key;服务令牌永远不会发往明文 HTTP 线路。 |
|
|
24
24
|
| **只读用量卡片** | 每个上游账号展示计划、到期时间、各窗口读数和 Codex 重置次数(网关提供时)。打开设置页只读一次,之后只有点单张卡片的「刷新」才会重新读取;用量条超过 70% 变橙、达到 90% 变红。 |
|
|
25
25
|
| **模型可见性与默认 effort** | 每个 provider 不设置可见性时跟随网关目录;保存过选择后只显示勾选项,新上架模型默认隐藏。每个模型可保存一个默认推理 effort,DSH 在下一次请求应用。 |
|
|
26
26
|
| **会话级快速档** | 网关声明 `priority` 服务档的模型,输入框旁会出现标准/快速切换。选择按会话保存在 host 内存,快速档会给请求带上 `service_tier: "priority"`。 |
|
|
@@ -39,14 +39,12 @@ dsh plugin --profile web add dsh-magpie-connector
|
|
|
39
39
|
|
|
40
40
|
打开 **设置 → Magpie**。
|
|
41
41
|
|
|
42
|
-
1.
|
|
43
|
-
2.
|
|
44
|
-
3.
|
|
42
|
+
1. 在**线路 1** 里填写**包含 `/v1`** 的网关 API 地址,例如 `http://192.168.1.20:3425/v1`,以及这条线路的 gateway key。地址、线路列表、可见性与默认 effort 保存在 `$DSH_HOME/plugins/dsh-magpie-connector/settings.json` —— key/Access 不会写入该文件,也不会被设置接口返回。
|
|
43
|
+
2. 需要多条线路时点「添加备用线路」,每条线路填写自己的地址与 key;用「上移/下移」调整优先级(序号 1 优先)。保存后打开设置页默认只展开当前生效线路,序号后的 4×4 绿色方块表示当前线路;点击 info 图标可查看优先级与自动回退说明。
|
|
44
|
+
3. 每条线路的**高级连接**里选择「直连」或「Cloudflare Access」。局域网与 Tailscale 都属于直连;Cloudflare Access 需要 HTTPS 线路,从 Access 切回直连并保存会清除该线路的两项 Access 凭据。Cloudflare 免费/Pro 套餐在源站 100 秒内没有响应时会返回 524。
|
|
45
|
+
4. 保存会同步模型目录;「刷新模型列表」可再次同步,插件不会定时轮询目录。
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- **直连**(默认)—— 网关地址可直接访问。局域网和 Tailscale 都属于这种:客户端加入 tailnet 后填 `http://<主机>.<tailnet>.ts.net:3425/v1` 或子网路由地址即可,插件里没有额外配置。
|
|
49
|
-
- **Cloudflare Access** —— 网关在 Cloudflare Tunnel + Access 之后、客户端不方便装 VPN 时使用。填写服务令牌两项;每个请求同时携带两项头和 bearer key。从 Cloudflare Access 切回直连并保存会清除已保存的 Access 凭据。Cloudflare 免费/Pro 套餐在源站 100 秒内没有响应时会返回 524。
|
|
47
|
+
> **0.2.0 BREAKING 升级**:设置格式改为单一 `lines[]`,旧版单地址 `settings.json`(只有 `baseUrl`)与旧固定凭据引用(`MAGPIE_API_KEY` / `MAGPIE_ACCESS_*`)**不会被读取或迁移**,升级后需在设置页重新添加线路并填写每线路 key/Access。模型可见性与默认 effort 会保留;旧凭据记录可由你在 DSH credentials 中手动删除。升级前若要保留回滚能力,请备份 `settings.json` 与旧凭据引用。
|
|
50
48
|
|
|
51
49
|
还需要别的接入方式(自定义请求头、mTLS、其它零信任网关)?**欢迎提 issue 或 PR**,说明你的部署方式和网关版本。
|
|
52
50
|
|
|
@@ -63,7 +61,10 @@ dsh plugin --profile web add dsh-magpie-connector
|
|
|
63
61
|
### 需要知道的行为
|
|
64
62
|
|
|
65
63
|
- **失败交给 DSH 重试,插件不重放请求。** 适配器声明与 `dsh-plugin-subscriptions` 相同的 `llm-retry` 策略(最多 10 次,1 秒起指数退避至 60 秒,±20% 抖动,429 时遵守 `Retry-After`)。`TIMEOUT`、`TRANSPORT`、`SERVER`、`RATE_LIMIT`、`EMPTY_RESPONSE` 会重试;401/403、额度用尽、上下文超限与其它 4xx 不重试。
|
|
64
|
+
- **默认输出预算为上下文压缩预留空间。** 网关的 `max_output_tokens` 是模型能力上限,不直接作为每次请求的默认预算。交给 DSH 的默认预算是 `min(能力上限, max(1, 窗口/4, 窗口/2 − 65536))`,给 DSH 的上下文压缩留出空间;显式设置的 `maxTokens` 不受影响。上下文窗口小于 104,026 token 的模型仍按同一公式计算,但不保证通过 DSH 的压缩检查。
|
|
66
65
|
- **只有持续静默才会结束流。** 响应头、SSE 事件和心跳都算有数据;连续 300 秒没有任何数据才以 `TIMEOUT` 结束(消息含 `MAGPIE_STREAM_TIMEOUT`)。只要网关还在发心跳,长时间推理不会被截断。
|
|
66
|
+
- **自动选线不会重放请求。** 每次操作开始前按序号做 TCP 可达探测并选线路;连接阶段失败只使该线路缓存失效、本次操作失败,下一次操作重新选择。生成请求从不被插件换线或重放,其它失败交给 DSH 重试策略。
|
|
67
|
+
- **切换线路与编辑线路语义不同。** 自动切换保留模型目录(不闪断)、清空用量卡片并以新线路重同步;编辑生效线路的地址/key/接入方式才会按换代清空目录;编辑非生效线路不影响当前目录。
|
|
67
68
|
- **更改连接不会中止正在进行的生成。** 新地址或新 key 会立即隐藏旧目录与旧卡片,但已 prepare 或正在运行的生成会用捕获到的旧地址与 key 跑完。用户取消与插件卸载仍会中止它们。
|
|
68
69
|
- **传输边界刻意收窄。** host 请求经私有 `undici` dispatcher 直连配置的网关,不经过全局代理 dispatcher;拒绝重定向;只允许 `GET /models`、`POST /responses`、`POST /chat/completions`、`GET /magpie/quotas/cards`、`POST /magpie/quotas/refresh`;从不调用 Magpie GUI 的 `/api/*` 管理接口。厂商出站代理在 Magpie 侧配置。
|
|
69
70
|
- **不支持:** 非空 `stop` 序列会在发请求前失败(`MAGPIE_STOP_UNSUPPORTED`),因为 Magpie 无法在所有上游协议转换中保留 stop。用量卡片与 Codex 重置次数只读展示,插件不会调用账号管理或重置端点。
|
|
@@ -93,7 +94,7 @@ npm run build --workspace=dsh-magpie-connector
|
|
|
93
94
|
dsh plugin --profile web remove dsh-magpie-connector
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
重启 DSH 并刷新页面。dispose 只释放注册、计时器与连接,不删除凭据和设置。需要彻底清理时,通过 DSH credentials
|
|
97
|
+
重启 DSH 并刷新页面。dispose 只释放注册、计时器与连接,不删除凭据和设置。需要彻底清理时,通过 DSH credentials 管理删除各线路的 `MAGPIE_LINE_<ID>_*` 凭据(以及升级前遗留的 `MAGPIE_API_KEY` / `MAGPIE_ACCESS_*`),并在确认安装版本的数据目录后删除插件设置文件。如果其它集成仍在使用某个凭据,不要删除。
|
|
97
98
|
|
|
98
99
|
在 [ohmydsh](https://github.com/prgrmrwy/ohmydsh) 中则把 manifest 条目设为 `enabled: false` 后重新构建。
|
|
99
100
|
|
package/docs/SCREENSHOTS.md
CHANGED
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
| file | kind | source | date | reviewer | verdict | note |
|
|
4
4
|
|---|---|---|---|---|---|---|
|
|
5
5
|
| model-picker.gif | screenshot | isolated DSH_HOME instance (temp HOME/XDG/DSH_HOME under /tmp, port 3199), synthetic data: local mock Magpie gateway on loopback with demo models (only Codex models advertise the priority tier, as on Magpie); lossless deviceScaleFactor-2 screenshots of the real Web UI, 1500x880 at 1:1 | 2026-10-28 | lead | pass (viewed frames: demo workspace, mock models and mock account only; no hostname, user, path, token or real session) | Model picker grouped by upstream provider, search, switch to GPT shows the standard/fast control, switch back to Claude hides it immediately |
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
> Settings screenshot removed pending re-recording after the 0.2.0 line-list settings rewrite; the old recording showed the pre-multi-line connection form.
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"id": "credentials",
|
|
62
62
|
"type": "security",
|
|
63
63
|
"label": "DSH credentials",
|
|
64
|
-
"sublabel": "
|
|
64
|
+
"sublabel": "per-line key · Access (bound to stable id)",
|
|
65
65
|
"pos": [
|
|
66
66
|
180,
|
|
67
67
|
150
|
|
@@ -259,14 +259,14 @@
|
|
|
259
259
|
"title": "Connection methods",
|
|
260
260
|
"items": [
|
|
261
261
|
"Direct: LAN and Tailscale subnet routing use the same gateway address",
|
|
262
|
-
"
|
|
262
|
+
"Automatic line selection: ≤600ms TCP probe (10s positive / 15s negative cache); Access headers only on the HTTPS line that enables them"
|
|
263
263
|
]
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
"dot": "rose",
|
|
267
267
|
"title": "Boundaries and data",
|
|
268
268
|
"items": [
|
|
269
|
-
"
|
|
269
|
+
"Each line's key/Access values live in DSH credentials and are never returned by the settings API",
|
|
270
270
|
"A private undici dispatcher talks only to the configured gateway and rejects redirects",
|
|
271
271
|
"No Magpie GUI management API; changing the connection does not abort running generations"
|
|
272
272
|
]
|
package/docs/architecture.en.svg
CHANGED
|
@@ -493,8 +493,8 @@ rect.c-bg-rect { fill: var(--bg); }
|
|
|
493
493
|
<text x="535" y="371" class="t-muted" font-size="9" text-anchor="middle">LLM adapter + settings/usage API</text>
|
|
494
494
|
</g>
|
|
495
495
|
|
|
496
|
-
<g id="node-credentials" data-node-id="credentials" data-node-label="DSH credentials" tabindex="0" role="button" aria-label="Focus DSH credentials,
|
|
497
|
-
<title>DSH credentials ·
|
|
496
|
+
<g id="node-credentials" data-node-id="credentials" data-node-label="DSH credentials" tabindex="0" role="button" aria-label="Focus DSH credentials, per-line key · Access, Local DSH Host" aria-pressed="false" data-node-kind="security" data-node-sublabel="per-line key · Access" data-node-context="Local DSH Host">
|
|
497
|
+
<title>DSH credentials · per-line key · Access · Local DSH Host</title>
|
|
498
498
|
<rect x="180" y="150" width="170" height="66" rx="6" class="c-mask"/>
|
|
499
499
|
<rect x="180" y="150" width="170" height="66" rx="6" class="c-security" stroke-width="1.5"/>
|
|
500
500
|
<g aria-hidden="true" data-semantic-sigil="security" class="semantic-sigil s-security" transform="translate(186 156) scale(0.6875)">
|
|
@@ -502,7 +502,7 @@ rect.c-bg-rect { fill: var(--bg); }
|
|
|
502
502
|
<path d="m5.8 8 1.5 1.5 3-3"/>
|
|
503
503
|
</g>
|
|
504
504
|
<text data-node-label="" x="265" y="181" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">DSH credentials</text>
|
|
505
|
-
<text x="265" y="197" class="t-muted" font-size="9" text-anchor="middle">
|
|
505
|
+
<text x="265" y="197" class="t-muted" font-size="9" text-anchor="middle">per-line key · Access</text>
|
|
506
506
|
</g>
|
|
507
507
|
|
|
508
508
|
<g id="node-settings" data-node-id="settings" data-node-label="settings.json" tabindex="0" role="button" aria-label="Focus settings.json, address · visibility · default effort, Local DSH Host" aria-pressed="false" data-node-kind="database" data-node-sublabel="address · visibility · default effort" data-node-context="Local DSH Host">
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"id": "credentials",
|
|
62
62
|
"type": "security",
|
|
63
63
|
"label": "DSH credentials",
|
|
64
|
-
"sublabel": "
|
|
64
|
+
"sublabel": "按线路 key · Access(稳定 id 绑定)",
|
|
65
65
|
"pos": [
|
|
66
66
|
180,
|
|
67
67
|
150
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"id": "settings",
|
|
84
84
|
"type": "database",
|
|
85
85
|
"label": "settings.json",
|
|
86
|
-
"sublabel": "
|
|
86
|
+
"sublabel": "线路列表 · 可见性 · 默认 effort",
|
|
87
87
|
"pos": [
|
|
88
88
|
470,
|
|
89
89
|
500
|
|
@@ -258,15 +258,15 @@
|
|
|
258
258
|
"dot": "emerald",
|
|
259
259
|
"title": "接入方式",
|
|
260
260
|
"items": [
|
|
261
|
-
"
|
|
262
|
-
"
|
|
261
|
+
"多线路:首次安装为空、通常 1 条,可加备用线路,序号即优先级",
|
|
262
|
+
"自动选线:≤600ms TCP 探测(正 10s/负 15s 缓存),Access 仅随启用它的 HTTPS 线路发送"
|
|
263
263
|
]
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
"dot": "rose",
|
|
267
267
|
"title": "边界与数据",
|
|
268
268
|
"items": [
|
|
269
|
-
"key 存 DSH credentials,设置接口不回显",
|
|
269
|
+
"每线路 key/Access 存 DSH credentials,设置接口不回显",
|
|
270
270
|
"私有 undici dispatcher 只直连网关,拒绝重定向",
|
|
271
271
|
"不调用 Magpie GUI 管理接口,切换连接不中止在途生成"
|
|
272
272
|
]
|
package/docs/architecture.zh.svg
CHANGED
|
@@ -493,8 +493,8 @@ rect.c-bg-rect { fill: var(--bg); }
|
|
|
493
493
|
<text x="535" y="371" class="t-muted" font-size="9" text-anchor="middle">LLM adapter + 设置/用量 API</text>
|
|
494
494
|
</g>
|
|
495
495
|
|
|
496
|
-
<g id="node-credentials" data-node-id="credentials" data-node-label="DSH credentials" tabindex="0" role="button" aria-label="聚焦DSH credentials
|
|
497
|
-
<title>DSH credentials ·
|
|
496
|
+
<g id="node-credentials" data-node-id="credentials" data-node-label="DSH credentials" tabindex="0" role="button" aria-label="聚焦DSH credentials,按线路 key · Access, 本机 DSH Host" aria-pressed="false" data-node-kind="security" data-node-sublabel="按线路 key · Access" data-node-context="本机 DSH Host">
|
|
497
|
+
<title>DSH credentials · 按线路 key · Access · 本机 DSH Host</title>
|
|
498
498
|
<rect x="180" y="150" width="170" height="66" rx="6" class="c-mask"/>
|
|
499
499
|
<rect x="180" y="150" width="170" height="66" rx="6" class="c-security" stroke-width="1.5"/>
|
|
500
500
|
<g aria-hidden="true" data-semantic-sigil="security" class="semantic-sigil s-security" transform="translate(186 156) scale(0.6875)">
|
|
@@ -502,11 +502,11 @@ rect.c-bg-rect { fill: var(--bg); }
|
|
|
502
502
|
<path d="m5.8 8 1.5 1.5 3-3"/>
|
|
503
503
|
</g>
|
|
504
504
|
<text data-node-label="" x="265" y="181" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">DSH credentials</text>
|
|
505
|
-
<text x="265" y="197" class="t-muted" font-size="9" text-anchor="middle"
|
|
505
|
+
<text x="265" y="197" class="t-muted" font-size="9" text-anchor="middle">按线路 key · Access</text>
|
|
506
506
|
</g>
|
|
507
507
|
|
|
508
|
-
<g id="node-settings" data-node-id="settings" data-node-label="settings.json" tabindex="0" role="button" aria-label="聚焦settings.json
|
|
509
|
-
<title>settings.json ·
|
|
508
|
+
<g id="node-settings" data-node-id="settings" data-node-label="settings.json" tabindex="0" role="button" aria-label="聚焦settings.json,线路列表 · 可见性 · 默认 effort, 本机 DSH Host" aria-pressed="false" data-node-kind="database" data-node-sublabel="线路列表 · 可见性 · 默认 effort" data-node-context="本机 DSH Host">
|
|
509
|
+
<title>settings.json · 线路列表 · 可见性 · 默认 effort · 本机 DSH Host</title>
|
|
510
510
|
<rect x="470" y="500" width="170" height="66" rx="6" class="c-mask"/>
|
|
511
511
|
<rect x="470" y="500" width="170" height="66" rx="6" class="c-database" stroke-width="1.5"/>
|
|
512
512
|
<g aria-hidden="true" data-semantic-sigil="database" class="semantic-sigil s-database" transform="translate(476 506) scale(0.6875)">
|
|
@@ -514,7 +514,7 @@ rect.c-bg-rect { fill: var(--bg); }
|
|
|
514
514
|
<path d="M3 4v8c0 1.1 2.2 2 5 2s5-.9 5-2V4M3 8c0 1.1 2.2 2 5 2s5-.9 5-2"/>
|
|
515
515
|
</g>
|
|
516
516
|
<text data-node-label="" x="555" y="531" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">settings.json</text>
|
|
517
|
-
<text x="555" y="547" class="t-muted" font-size="9" text-anchor="middle"
|
|
517
|
+
<text x="555" y="547" class="t-muted" font-size="9" text-anchor="middle">线路列表 · 可见性 · 默认 effort</text>
|
|
518
518
|
</g>
|
|
519
519
|
|
|
520
520
|
<g id="node-cloudflare" data-node-id="cloudflare" data-node-label="Cloudflare Access" tabindex="0" role="button" aria-label="聚焦Cloudflare Access,可选 · 服务令牌, 架构组件" aria-pressed="false" data-node-kind="security" data-node-sublabel="可选 · 服务令牌" data-node-context="架构组件">
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Cloudflare 524:长会话的 Magpie 请求在 125 秒边缘超时
|
|
2
|
+
|
|
3
|
+
> 状态:**已定位,未修复**。跟踪条目见 [BACKLOG D014](../../../BACKLOG.md)。
|
|
4
|
+
> 记录时间:2026-10-28。本文是排查记录,不是当前实现规范。
|
|
5
|
+
|
|
6
|
+
## 1. 结论摘要
|
|
7
|
+
|
|
8
|
+
经 Cloudflare 访问 Magpie 网关时,长会话(约 40 万 token)的生成请求会被 Cloudflare 以 **524** 中断。根因不是 Magpie 报错,也不是 DSH 插件超时:
|
|
9
|
+
|
|
10
|
+
- Cloudflare 边缘等**源站返回 HTTP 响应头**的上限是 **125 秒**(Proxy Read Timeout);
|
|
11
|
+
- Magpie 要**先拿到上游响应头**才写自己的响应头;
|
|
12
|
+
- 所以这 125 秒计的是上游完整的「首个响应字节」时间,长 prompt 的 prefill 超过它,边缘就回 524。
|
|
13
|
+
|
|
14
|
+
突破口:这个计时在源站发出**第一个字节**时停止,不需要整个响应在 125 秒内完成。任何「让源站立刻发头 + 心跳」的中间层都能绕开。
|
|
15
|
+
|
|
16
|
+
## 2. 现象
|
|
17
|
+
|
|
18
|
+
- 报错文案:`本轮运行失败 524 status code (no body)`。
|
|
19
|
+
- 只发生在长会话;新会话正常。同一天内第一次出现时上下文已到约 40 万 token。
|
|
20
|
+
- 影响所有经该 Cloudflare 入口的模型(Claude 与 OpenCode Go 都出现)。
|
|
21
|
+
|
|
22
|
+
## 3. 证据
|
|
23
|
+
|
|
24
|
+
### 3.1 会话日志里的原始错误
|
|
25
|
+
|
|
26
|
+
从会话日志(`~/.dsh/sessions/<workspace>/session-*/session.v4.jsonl.zstd`,多帧 zstd,需逐帧解压)中解出:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{"failure":{"code":"SERVER","message":"524 status code (no body)","status":524,"providerRetryAfterMs":120000}}
|
|
30
|
+
{"failure":{"code":"SERVER","message":"502 status code (no body)","status":502,"providerRetryAfterMs":60000}}
|
|
31
|
+
{"failure":{"code":"SERVER","message":"dsh-magpie-connector API error (524): 524 status code (no body)","status":524,"providerRetryAfterMs":120000}}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`providerRetryAfterMs: 120000` 说明响应带了 `Retry-After: 120`——这是 Cloudflare 的 524 响应特征,不是 Magpie 的。
|
|
35
|
+
|
|
36
|
+
### 3.2 失败耗时正好卡在 125 秒
|
|
37
|
+
|
|
38
|
+
用 `step/start` 与 `assistant/attempt` 的时间差计算三次失败(2026-10-28):
|
|
39
|
+
|
|
40
|
+
| 时间 | 轮次 | 耗时 | 结果 |
|
|
41
|
+
|---|---|---:|---|
|
|
42
|
+
| 15:26:29 → 15:28:36 | turn 8 step 17 | **127.2s** | 524 |
|
|
43
|
+
| 15:34:07 → 15:36:15 | turn 9 step 1 | **128.1s** | 524 |
|
|
44
|
+
| 15:41:24 → 15:43:34 | turn 10 step 3 | **129.7s** | 524 |
|
|
45
|
+
|
|
46
|
+
对照:同一会话里一次 502 只用了 16.2 秒。**127–130 秒**与 Cloudflare 的 125 秒 Proxy Read Timeout 完全吻合,可以排除「上游正常但偶发」。
|
|
47
|
+
|
|
48
|
+
### 3.3 会话规模与请求配置
|
|
49
|
+
|
|
50
|
+
- 该会话峰值 usage:`inputTokens + cacheReadTokens = 396,348`(约 40 万 token)。
|
|
51
|
+
- `request/header` 记录:`provider: dsh-magpie-connector`,`maxTokens: 128000`(Claude)或 `384000`(OpenCode Go DeepSeek),`adapterDefaults.maxTokens: true`。
|
|
52
|
+
|
|
53
|
+
### 3.4 取证方法
|
|
54
|
+
|
|
55
|
+
会话日志是多帧 zstd,`zstd -dc` 只能解第一帧且本机未必装了 zstd。可用 Node `zlib.zstdDecompressSync` 按帧魔数 `28 B5 2F FD` 切分后逐帧解压,再按 `type` 过滤 `assistant/attempt`、`step/start`、`request/header`。
|
|
56
|
+
|
|
57
|
+
## 4. 根因机制
|
|
58
|
+
|
|
59
|
+
### 4.1 Cloudflare 的连接限制
|
|
60
|
+
|
|
61
|
+
[Connection limits](https://developers.cloudflare.com/fundamentals/reference/connection-limits/) 中与本次相关的三行:
|
|
62
|
+
|
|
63
|
+
| 限制 | 数值 | 触发错误码 | 可配置 |
|
|
64
|
+
|---|---:|---|---|
|
|
65
|
+
| Proxy Read Timeout(等源站响应头) | **125s** | 524 | 仅 Enterprise 可调 |
|
|
66
|
+
| Proxy Write Timeout(向源站写请求) | 30s | 524 | 否 |
|
|
67
|
+
| Proxy Idle Timeout | 900s | 520 | 否 |
|
|
68
|
+
|
|
69
|
+
[Error 524 官方说明](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/):Cloudflare 已连上源站,但源站在 125 秒内没有返回 HTTP 响应。Enterprise 可经 Cache Rule 或 zone setting API 提到 6000 秒;其它计划只有两个官方建议——**换不代理的子域**,或**改成后台任务 + 轮询**。
|
|
70
|
+
|
|
71
|
+
### 4.2 Magpie 写响应头的时机
|
|
72
|
+
|
|
73
|
+
Magpie 的流式转发路径(`internal/gateway/gateway.go`,固定 commit `98e23350`):
|
|
74
|
+
|
|
75
|
+
```go
|
|
76
|
+
// 约 4990 行:先拿到上游响应 res
|
|
77
|
+
rd, sse := eventStream(res)
|
|
78
|
+
...
|
|
79
|
+
w.WriteHeader(res.StatusCode) // 约 5004 行,此后才写自己的响应头
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
也就是说 `WriteHeader` 在 `res` 之后。上游(Anthropic / OpenAI / Codex 后端)在长 prompt 的 prefill 完成前不返回响应头,Magpie 也就一直不发头,Cloudflare 的 125 秒从收到请求起算,直接命中。
|
|
83
|
+
|
|
84
|
+
### 4.3 为什么长会话必炸
|
|
85
|
+
|
|
86
|
+
以 Claude 为例(`context_window: 1,000,000`、`max_output_tokens: 128,000`)。当前(未修复的 0.1.1)把最大输出能力直接作为默认输出预算,DSH 的压缩阈值按 compaction-basic 公式计算:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
压缩阈值 = floor(min(C × 0.8, C − defaultMaxTokens − headroom))
|
|
90
|
+
= floor(min(800,000, 1,000,000 − 128,000 − 65,536))
|
|
91
|
+
= 800,000 tokens
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
所以 40 万 token 远未达到压缩条件,会话可以继续增长,每轮都重传全部上下文。上游 prefill 时间随 prompt 线性增长,超过 125 秒只是时间问题。
|
|
95
|
+
|
|
96
|
+
> 注意:这**不是** `magpie-default-output-budget` change 能解决的问题。该 change 修的是「最大输出相对窗口过大」的模型(如 272k/128k 的 GPT-6 Astra/Luna、1M/512k 的 MiniMax);Claude 的 128k/1M 按公式预算不变、阈值仍是 800k。
|
|
97
|
+
|
|
98
|
+
### 4.4 重试放大了观感
|
|
99
|
+
|
|
100
|
+
适配器把 524 归入 `SERVER`(可重试),并把 Cloudflare 的 `Retry-After: 120` 作为 `providerRetryAfterMs` 传下去;DSH 的重试策略是 normal 模式最多 10 次、指数退避上限 60 秒。于是每次 524 后要等约 120 秒再重试,长请求场景下表现为**连续十几分钟一直在报 524**。
|
|
101
|
+
|
|
102
|
+
## 5. 已排除的方案
|
|
103
|
+
|
|
104
|
+
| 方案 | 为什么不可行 |
|
|
105
|
+
|---|---|
|
|
106
|
+
| 直连 LAN / 不走代理 | 用户从公网访问家庭网络,只能经 Cloudflare 打通 |
|
|
107
|
+
| 缩小上下文窗口预算 | 长会话是主要使用方式,不能牺牲 |
|
|
108
|
+
| 提高 Cloudflare 超时 | 仅 Enterprise 可调 125 → 6000 秒 |
|
|
109
|
+
| 不等代理的子域 | 家庭网络没有公网入口,Tunnel 是唯一通道 |
|
|
110
|
+
| 后台任务 + 轮询 | OpenAI 兼容协议与 DSH 生成流程不支持改协议 |
|
|
111
|
+
|
|
112
|
+
## 6. 社区同类问题与解法
|
|
113
|
+
|
|
114
|
+
### 6.1 local-cli-bridge:最贴近的先例
|
|
115
|
+
|
|
116
|
+
[REMOTE_BRIDGE.md](https://github.com/JediConcepts/local-cli-bridge/blob/HEAD/docs/REMOTE_BRIDGE.md) 描述的场景与本仓几乎同构:Claude/Codex 订阅跑在家用工作站,经 Cloudflare Tunnel + Access 暴露成 OpenAI 兼容端点。它对 524 的做法(原文要点):
|
|
117
|
+
|
|
118
|
+
- 请求带 Cloudflare 边缘头(`cf-ray` / `cf-connecting-ip`)时,**立即提交 200 并送出第一个字节**,停掉边缘计时;
|
|
119
|
+
- 然后每 **20 秒**发一次心跳(换行),直到后端完成;
|
|
120
|
+
- 最后再写真正的响应体。
|
|
121
|
+
|
|
122
|
+
它同时写明三点代价:
|
|
123
|
+
|
|
124
|
+
1. 心跳开始后失败无法再改状态码,只能作为 `{"error":{...}}` 放进 200 的 body,客户端必须识别;
|
|
125
|
+
2. 可用 `BRIDGE_KEEPALIVE_MS` 强制数值间隔(`0` 关闭);
|
|
126
|
+
3. 如果隧道链路剥掉了 Cloudflare 头,`auto` 会误判成本地请求,长请求重新 524。
|
|
127
|
+
|
|
128
|
+
### 6.2 Cloudflare 侧的官方选项
|
|
129
|
+
|
|
130
|
+
见 4.1:Enterprise 提额,或改不代理子域 / 轮询协议。都不适用。
|
|
131
|
+
|
|
132
|
+
### 6.3 Workers 能否代理
|
|
133
|
+
|
|
134
|
+
[Cloudflare Agents 的 SSE 文档](https://developers.cloudflare.com/agents/agent-class/http-sse/)称 Workers 上的 SSE 响应没有有效时长限制。但 Worker 到源站(经 Tunnel)的 fetch 是否仍受 125 秒约束未经验证,作为备选而非首选。
|
|
135
|
+
|
|
136
|
+
### 6.4 SSE 缓冲
|
|
137
|
+
|
|
138
|
+
社区已知 SSE 经 Cloudflare 需要关掉缓冲([SSE with Cloudflare Proxy 讨论](https://community.cloudflare.com/t/using-server-sent-events-sse-with-cloudflare-proxy/656279)),靠 `X-Accel-Buffering: no`。Magpie 已经设置(`gateway.go` 约 5001–5003 行),这一层不是本次原因。
|
|
139
|
+
|
|
140
|
+
## 7. 候选方案对比
|
|
141
|
+
|
|
142
|
+
| 方案 | 做法 | 优点 | 代价 |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| **A. 家庭侧保活中继**(推荐) | 在 nginx 与 Magpie 之间加一层小中继:带 `cf-ray` 的请求先等 20–30 秒拿 Magpie 响应头;快请求原样透传状态码;超时未响应则提交 `200 text/event-stream` + SSE 注释心跳(15–20 秒一次),直到 Magpie 开始返回再透传字节 | 不动上游、不动客户端、不需要企业版;错误语义只对慢请求退化 | 多一个要维护的服务;慢请求的错误变成流内事件 |
|
|
145
|
+
| B. 推动 Magpie 上游 | 让 Magpie 收到请求就 flush SSE 头并周期心跳 | 根治,所有用户受益 | 上游是否接受未知;周期长;且需要等发版 |
|
|
146
|
+
| C. Cloudflare Worker 代理 | Worker 立即回 200 流,把源站 SSE 管道接进来 | 不用家里加服务 | Worker→源站的 125 秒约束未验证;多一处故障点 |
|
|
147
|
+
| D. 换入口 | Tailscale / frp + VPS 等 | 彻底绕开 125 秒 | 与「必须走 Cloudflare」的约束冲突 |
|
|
148
|
+
|
|
149
|
+
## 8. 推荐方案要点(A)
|
|
150
|
+
|
|
151
|
+
1. 只拦截生成路径(`/v1/responses`、`/v1/chat/completions`),其它接口(`/v1/models`、额度卡片)保持原样直连。
|
|
152
|
+
2. **延迟提交**:先给 Magpie 20–30 秒拿响应头。4xx/5xx(鉴权、限流)通常远快于这个窗口,因此状态码语义保留。
|
|
153
|
+
3. 超时后才提交 `200` + `text/event-stream`,立刻发一条 SSE 注释心跳,之后每 15–20 秒一条,直到 Magpie 的首个字节到达。
|
|
154
|
+
4. Magpie 开始返回后,字节原样透传,不再插入任何内容。
|
|
155
|
+
5. 已提交 200 之后 Magpie 若失败,把失败翻译成对应协议的**流内错误**(Chat 的 `error` 事件 / Responses 的 `response.failed`),DSH 适配器已能识别并正确归类。
|
|
156
|
+
6. 判据用 `cf-ray` 头;中继只监听回环,不额外暴露端口。
|
|
157
|
+
|
|
158
|
+
## 9. 待验证 / 未决
|
|
159
|
+
|
|
160
|
+
- **Proxy Write Timeout(30 秒)**:40 万 token 的请求体约 1.5–2MB,若家庭上行带宽不足,上传超过 30 秒同样会 524,症状一样但根因不同。需要实测一次长请求的请求体大小与上行速率,确认是否叠加了第二个问题。
|
|
161
|
+
- 中继落地位置:建议放在 home-substore 的 `ops/magpie-cloudflare`(与 Cloudflare 入口配置同处),不属于本仓。
|
|
162
|
+
- 是否同时向 yetone/magpie 提 issue(该仓库不收 PR,功能请求走 issue)。
|
|
163
|
+
- 修复后如何回归:用一次 40 万 token 级别的长会话请求,确认耗时超过 125 秒仍正常出流,且 401/429 仍是原始状态码。
|
|
164
|
+
|
|
165
|
+
## 10. 参考资料
|
|
166
|
+
|
|
167
|
+
- [Cloudflare Error 524](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/)
|
|
168
|
+
- [Cloudflare Connection limits](https://developers.cloudflare.com/fundamentals/reference/connection-limits/)
|
|
169
|
+
- [local-cli-bridge: Remote model bridge over Cloudflare](https://github.com/JediConcepts/local-cli-bridge/blob/HEAD/docs/REMOTE_BRIDGE.md)
|
|
170
|
+
- [Cloudflare Agents: HTTP and Server-Sent Events](https://developers.cloudflare.com/agents/agent-class/http-sse/)
|
|
171
|
+
- [Cloudflare Community: Using SSE with Cloudflare Proxy](https://community.cloudflare.com/t/using-server-sent-events-sse-with-cloudflare-proxy/656279)
|
|
172
|
+
- 上游代码:`yetone/magpie` commit `98e23350f14bfa63adc43c54271fb8fecb4fce5c` 的 `internal/gateway/gateway.go`
|
package/docs/internal-api.md
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
## 0.2.0 single-schema line settings API (current)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
BREAKING: the host settings surface keeps only the `lines[]` schema. Legacy top-level `baseUrl`, `nextLineId`, notices and fixed `MAGPIE_API_KEY` / `MAGPIE_ACCESS_*` refs no longer exist; the 0.1.x notes below are historical.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```ts
|
|
6
|
+
// GET projection (SettingsController)
|
|
7
|
+
{ lines: [{ id, baseUrl, accessMode, complete,
|
|
8
|
+
key: { configured, writable },
|
|
9
|
+
access: { clientId: { configured, writable }, clientSecret: { configured, writable } },
|
|
10
|
+
refs: { key, accessClientId, accessClientSecret } }],
|
|
11
|
+
activeLineId, pending, retainedCredentials,
|
|
12
|
+
lastSyncAt?, lastSyncError?, lastSyncErrorKind?, groups, visibleModels, defaultEfforts }
|
|
13
|
+
|
|
14
|
+
// POST (SettingsController passes the connection subset to runtime.settings)
|
|
15
|
+
{ lines: [{ id?, baseUrl: string, accessMode: 'direct' | 'cloudflare',
|
|
16
|
+
key?: string | null, accessClientId?: string | null, accessClientSecret?: string | null }],
|
|
17
|
+
resync?: true }
|
|
18
|
+
// `resync: true` alone is valid (force one catalog sync). A line with zero lines in the
|
|
19
|
+
// committed list is only reachable through load/first install; POST requires 1–5 lines.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- A line is complete when it has an address, a resolvable key and (for `cloudflare`) an HTTPS address plus both Access values. Only complete lines take part in automatic selection.
|
|
23
|
+
- Line ids are host-generated 8-hex-character random ids (unique among existing and pending lines), stored in settings, stable across reorder/delete/restart and never derived from the address or key. Creation must omit `id`; existing ids must reference a current line and may not repeat (400 zero-write otherwise).
|
|
24
|
+
- Every line uses `MAGPIE_LINE_<ID>_API_KEY` / `..._ACCESS_CLIENT_ID` / `..._ACCESS_CLIENT_SECRET` (uppercase id). Sorting/deleting never rebinds references; deleting a line clears writable refs and reports environment-owned values through `retainedCredentials`.
|
|
25
|
+
- Credential fields are tri-state: omitted = keep, `null` = clear (writable refs only), string = write. Whole-table saves/reorders must omit credentials they do not want to change.
|
|
26
|
+
- Loading validates the persisted lines and pending entries strictly: invalid format, duplicate ids/addresses, >5 lines or malformed pending make the whole connection read as empty (unconfigured) without touching the file or resolving credentials.
|
|
27
|
+
- Subscribe to `credentials/reference-updated` for **any** ref and map it with `lineIdOfCredentialRef(ref)`: a change on the active line invalidates the generation, a change on a non-active line does not.
|
|
28
|
+
- Credential writes are not transactional. Attempted `(lineId, field)` pairs are persisted as `pending` before any secret write; on successful whole-table commit, pending entries for lines absent from the commit are cleaned automatically (unset writable refs, keep env-owned ones in `retainedCredentials`). Deletes/access-mode changes commit only after every writable credential was cleared.
|
|
29
|
+
- `Operation` captures `lineId`, `accessMode`, `baseUrl` and the credentials; use `op` (not `gen.baseUrl`) for requests. `Generation` still owns catalog/cards; a line switch replaces the generation but carries the catalog, so `isCurrent(gen)` remains the commit guard.
|
|
6
30
|
|
|
7
31
|
## Runtime and connection identity
|
|
8
32
|
|
|
@@ -13,22 +37,22 @@ await runtime.syncCatalog()
|
|
|
13
37
|
const op = await runtime.beginOperation()
|
|
14
38
|
try {
|
|
15
39
|
const response = await runtime.transport.request(
|
|
16
|
-
{ baseUrl: op.
|
|
40
|
+
{ baseUrl: op.baseUrl, key: op.key }, 'GET', '/magpie/quotas/cards',
|
|
17
41
|
{ signal: op.gen.bgAbort.signal },
|
|
18
42
|
)
|
|
19
43
|
// Read/project/redact first; commit only if runtime.isCurrent(op.gen).
|
|
20
44
|
} finally { op.release() }
|
|
21
45
|
```
|
|
22
46
|
|
|
23
|
-
- `credentials` is `Pick<CredentialProvider, 'describe' | 'resolve' | 'set' | 'unset'>`.
|
|
24
|
-
- `SettingsStore(path?)` loads/saves `{
|
|
47
|
+
- `credentials` is `Pick<CredentialProvider, 'describe' | 'resolve' | 'set' | 'unset'>`. Line references are derived with `lineCredentialRefs(lineId)`; `lineIdOfCredentialRef(ref)` maps a ref back to its line id. Legacy fixed refs are not read. No credential files or process-environment fallback are read.
|
|
48
|
+
- `SettingsStore(path?)` loads/saves the single-schema `{lines, visibleModels, defaultEfforts, credentialUpdatePending?}` (see above). With no path it is an in-memory test store. `save(value)` atomically replaces a 0600 settings file. Never put key material in this object.
|
|
25
49
|
- `beginOperation(): Promise<Operation>` resolves credentials once, changes generation before dispatch if necessary, starts new-identity catalog sync, and retains that key for redaction until `release()`.
|
|
26
50
|
- `Operation = { gen, key, release }`. Release is idempotent. Keep the operation alive through response/error projection, not merely through request dispatch.
|
|
27
51
|
- `Generation` exposes `id`, `baseUrl`, `keyFingerprint`, `catalog`, `bgAbort`, `syncPromise?`, `lastSyncAt?`, `lastSyncError?`, `lastSyncErrorKind?`. The string-indexed extension slot lets features attach generation-local state, e.g. `gen.magpieCards`. Do not put feature state on a global singleton.
|
|
28
52
|
- `runtime.isCurrent(gen)` must guard every result commit. Dispose also makes it false. A stale request must not put old cards into the new generation.
|
|
29
|
-
- `runtime.invalidateCredential()` synchronously drops visible old state and aborts background operations before asynchronously resolving/syncing.
|
|
53
|
+
- `runtime.invalidateCredential(ref?)` maps a `credentials/reference-updated` ref to its line id (`lineIdOfCredentialRef`) and rotates only when that line is active; without a ref it rotates the current generation. It synchronously drops visible old state and aborts background operations before asynchronously resolving/syncing. Own credential writes suppress that listener until the combined settings mutation commits.
|
|
30
54
|
- `runtime.redact(text)` replaces current and active-operation keys literally with `[redacted]`. All gateway strings going into HTTP responses or logs must use this method. Catalog metadata is redacted before it is cached.
|
|
31
|
-
- `runtime.settings(body?)`
|
|
55
|
+
- `runtime.settings(body?)` handles the single-schema settings update `{lines?, resync?}` (plus GET projection) and returns a `Response`; the generic SettingsController adds preference fields (`visibleModels`, `defaultEffort`) around it. Credential fields use the tri-state (omit/null/write) contract above; unknown keys are rejected with 400 and nothing is written.
|
|
32
56
|
- `publish()` must call the adapter registration handle's `replace(['dsh-magpie-connector'])`; no payload-free imitation event is needed. Same-generation sync is single-flight and times out after 15 seconds. Last successful catalog/time survive same-generation failure; identity changes never retain old data.
|
|
33
57
|
|
|
34
58
|
## Catalog and adapter
|