dsh-magpie-connector 0.1.0 → 0.1.2

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2
4
+
5
+ - 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.
6
+ - 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.
7
+ - An explicit `maxTokens` passes through unchanged; DSH still fills the default when a request omits it.
8
+ - 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.
9
+
10
+ ## 0.1.1
11
+
12
+ - Fix README images on npm: `repository` no longer declares a monorepo `directory`, so relative image links resolve against the standalone repository root.
13
+
3
14
  ## 0.1.0
4
15
 
5
16
  - First release.
package/README.md CHANGED
@@ -63,6 +63,7 @@ Need another method — custom headers, mTLS, another zero-trust gateway? **Issu
63
63
  ### Behavior worth knowing
64
64
 
65
65
  - **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.
66
+ - **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
67
  - **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
68
  - **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
69
  - **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.
package/README.zh.md CHANGED
@@ -63,6 +63,7 @@ dsh plugin --profile web add dsh-magpie-connector
63
63
  ### 需要知道的行为
64
64
 
65
65
  - **失败交给 DSH 重试,插件不重放请求。** 适配器声明与 `dsh-plugin-subscriptions` 相同的 `llm-retry` 策略(最多 10 次,1 秒起指数退避至 60 秒,±20% 抖动,429 时遵守 `Retry-After`)。`TIMEOUT`、`TRANSPORT`、`SERVER`、`RATE_LIMIT`、`EMPTY_RESPONSE` 会重试;401/403、额度用尽、上下文超限与其它 4xx 不重试。
66
+ - **默认输出预算为上下文压缩预留空间。** 网关的 `max_output_tokens` 是模型能力上限,不直接作为每次请求的默认预算。交给 DSH 的默认预算是 `min(能力上限, max(1, 窗口/4, 窗口/2 − 65536))`,给 DSH 的上下文压缩留出空间;显式设置的 `maxTokens` 不受影响。上下文窗口小于 104,026 token 的模型仍按同一公式计算,但不保证通过 DSH 的压缩检查。
66
67
  - **只有持续静默才会结束流。** 响应头、SSE 事件和心跳都算有数据;连续 300 秒没有任何数据才以 `TIMEOUT` 结束(消息含 `MAGPIE_STREAM_TIMEOUT`)。只要网关还在发心跳,长时间推理不会被截断。
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 侧配置。
@@ -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/lib/client.js CHANGED
@@ -5,7 +5,7 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- //#region src/client/api.ts
8
+ // src/client/api.ts
9
9
  const TIMEOUT_MS = 3e4;
10
10
  function createFetchApi(base = "/api/dsh-magpie-connector/") {
11
11
  return async ({ method, path, query, body }) => {
@@ -45,8 +45,7 @@ window.__ModuleLoader__.load({
45
45
  return typeof reason === "string" && reason ? `HTTP ${response.status}:${reason}` : `HTTP ${response.status}`;
46
46
  }
47
47
  const errorText = (error) => error instanceof Error ? error.message : String(error);
48
- //#endregion
49
- //#region src/client/styles.ts
48
+ // src/client/styles.ts
50
49
  /**
51
50
  * One scoped stylesheet for the settings section and the speed control.
52
51
  * Colours come only from `--dsw-alias-*` theme tokens (they flip under the
@@ -102,8 +101,7 @@ window.__ModuleLoader__.load({
102
101
  .dmg-speed-item>span:first-child{width:14px;flex:none;font-size:12px}
103
102
  .dmg-speed-item small{display:block;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
104
103
  `;
105
- //#endregion
106
- //#region src/client/view-model.ts
104
+ // src/client/view-model.ts
107
105
  const str = (value) => typeof value === "string" && value !== "";
108
106
  /** Defensive parse: unknown or malformed entries never reach the renderer. */
109
107
  function parseCards(body, httpStatus) {
@@ -142,8 +140,7 @@ window.__ModuleLoader__.load({
142
140
  for (const card of cards) if (!order.includes(card.provider)) order.push(card.provider);
143
141
  return order;
144
142
  }
145
- //#endregion
146
- //#region src/client/UsageCard.ts
143
+ // src/client/UsageCard.ts
147
144
  /**
148
145
  * One read-only usage card. Every gateway string is a React text child (never
149
146
  * HTML), absent fields are omitted, and the only control is "刷新" — there is
@@ -205,8 +202,7 @@ window.__ModuleLoader__.load({
205
202
  role: "alert"
206
203
  }, card.error) : null);
207
204
  }
208
- //#endregion
209
- //#region src/client/SettingsPage.ts
205
+ // src/client/SettingsPage.ts
210
206
  /**
211
207
  * Magpie settings section: connection (with an advanced access mode), then
212
208
  * per-provider groups (model visibility, DSH-side default effort, read-only
@@ -929,8 +925,7 @@ window.__ModuleLoader__.load({
929
925
  }
930
926
  }, label));
931
927
  }
932
- //#endregion
933
- //#region src/client/bird-svg.ts
928
+ // src/client/bird-svg.ts
934
929
  const birdSvg = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" fill=\"#000\"><path fill-rule=\"evenodd\" d=\"M1.5 5.5C1.3 5.7 1.2 6.5 1.2 6.9C1.2 6.9 1.2 7 1.2 7.1C1.1 7.4 1.3 8 1.4 8.3C1.4 8.4 1.5 8.5 1.5 8.7C1.5 8.9 1.7 9 1.8 9.3C2.2 9.9 2.2 10 2.3 10.1C2.4 10.2 2.5 10.3 2.6 10.5C2.8 10.8 3 11.1 3.2 11.3C3.3 11.3 3.4 11.6 3.6 11.8C3.8 12 4.1 12.4 4.2 12.5C4.7 13 5.2 13.7 5.4 13.9C6 14.5 6.5 15.1 6.7 15.3C6.7 15.4 6.9 15.5 6.9 15.7C7.3 15.9 7.7 16.5 7.9 16.8C7.9 16.8 8 16.9 8 16.9C8.1 16.9 8.2 17.2 8.2 17.3C8.2 17.3 8.1 17.2 8.1 17.1C7.9 16.9 7.9 16.9 7.6 16.6C7.5 16.5 7.4 16.5 7.4 16.3C7.2 16.2 6.7 15.7 6.3 15.2C6.1 15.1 6 14.9 6 14.9C5.9 14.8 5.7 14.7 5.7 14.6C5.6 14.5 5.4 14.3 5.3 14.2C5.1 14.1 4.9 13.8 4.7 13.5C4.5 13.3 4.2 13.1 4.1 12.9C4.1 12.9 3.8 12.7 3.8 12.5C3.5 12.3 3 11.8 2.8 11.4C1.8 10.5 0.9 9.6 0.7 9.6C0.6 9.7 0.6 9.8 0.5 10C0.5 10.1 0.6 10.2 0.5 10.3C0.5 10.4 0.6 10.5 0.6 10.6C0.5 10.7 0.5 10.9 0.6 11C0.5 11.1 0.6 11.4 0.6 11.5C0.9 12.9 1.3 13.8 2.8 14.9C2.9 15.1 3.3 15.3 3.4 15.4C3.4 15.5 3.6 15.6 3.7 15.7C4.2 16 6.4 17.8 7 18.4C7.4 18.8 7.4 18.8 7.5 18.9C7.6 19 7.7 19 7.7 19.1C7.7 19.2 7.8 19.2 7.9 19.2C7.9 19.3 8.1 19.4 8.1 19.5C8.2 19.6 8.2 19.7 8.3 19.7C8.3 19.7 8.3 19.7 8.2 19.7C8.2 19.6 8.1 19.6 8 19.6C7.9 19.5 7.9 19.4 7.8 19.4C7.7 19.3 7.6 19.2 7.6 19.2C7.6 19.1 7.5 19 7.3 18.9C7.2 18.9 6.9 18.6 6.7 18.4C6.1 17.8 4.5 16.6 3.5 16C3.4 15.8 3 15.6 2.9 15.5C2.6 15.3 1.5 14.6 0.9 14.3C0.7 14.1 0.6 14.1 0.6 14.2C0.5 14.5 0.8 15.9 0.9 16.1C0.9 16.2 1 16.2 0.9 16.3C1.2 16.8 1.9 17.6 2.7 18.3C2.8 18.5 4 19.2 4.7 19.5C6.2 20.5 6.6 20.7 7.4 21.5C8.2 22.2 8.7 22.7 9.3 23.6C9.5 24.1 9.9 24.4 10.3 25.3C10.4 25.5 10.5 25.8 10.5 25.8C10.7 25.8 10.8 25.7 11 25.4C11.6 25 11.9 24.6 12.3 24.3C12.8 24 13 23.8 13 23.7C13.1 23.7 13.1 23.8 13 24C12 24.8 11 26 10.2 26.9C10.1 27.1 9.9 27.5 9.7 27.7C9.5 27.8 9.4 28 9.4 28C9.4 28 9.3 28.3 9.1 28.5C8.6 29.2 7.7 30.8 7.7 30.9C7.7 31 7.8 31 8.3 30.8C9 30.4 9.5 30.2 10.4 30C11.7 29.6 12.5 29.6 11.6 30C10.5 30.2 9.7 30.6 9.2 31.2C8.8 31.7 8.8 31.7 9.3 31.7C10.1 31.8 10.9 31.9 11.6 32.2C12.8 32.7 13.5 33.1 15.5 34.8C16.4 35.5 17.3 36.2 17.8 36.4C17.9 36.5 18.1 36.7 18.2 36.8C19.1 37.3 21.2 38.1 22.3 38.3C22.4 38.3 22.7 38.3 22.8 38.3C22.8 38.5 23 38.5 23 38.5C23.1 38.5 23.2 38.5 23.3 38.5C23.6 38.6 25.9 38.6 26.1 38.5C26.2 38.5 26.3 38.5 26.4 38.5C26.5 38.5 26.6 38.5 26.7 38.3C26.7 38.3 27 38.3 27.1 38.3C28.3 38.1 30.2 37.3 31.4 36.7C31.5 36.7 31.7 36.4 31.8 36.4C32.2 36.2 33.2 35.4 33.4 35.2C34.2 34.5 35.5 33 35.8 32.6C35.8 32.5 36 32.2 36.1 32C36.4 31.8 36.6 31.4 36.6 31.2C36.7 31.1 36.8 30.9 36.9 30.8C37.2 30.4 37.5 29.6 37.7 29.2C37.7 29.1 37.8 28.7 37.9 28.7C38.1 28.5 38.4 27.1 38.5 26.8C39 25.2 39.1 23.7 39 22.4C39 22.2 38.9 21.9 38.9 21.8C38.9 21.7 38.9 21.6 38.9 21.4C38.6 20.5 38.5 19.9 38.2 19C37.9 18.5 37.5 17.5 37.3 17.2C37 16.8 36.8 15.8 36.8 15.2C36.8 13.8 37.7 12.7 39.6 12.2C40.1 12.1 41.3 11.8 42.1 11.7C43.5 11.5 43.5 11.5 43.4 11.3C42.9 10.7 40.9 9.9 39.2 9.7C37.9 9.5 38.1 9.5 37.6 9C37 8.6 36.4 8 36 7.9C36 7.8 35.8 7.8 35.7 7.7C35.1 7.3 34.2 7 33.1 6.9C32.6 6.7 31.6 6.9 31 7C29.6 7.3 28.3 8 27.3 9.2C26.7 9.8 26.2 10.8 25.7 11.7C25.5 12.1 24.8 13.4 24.7 13.7C24.6 13.9 24.5 14.1 24.1 14.7C23.5 15.7 22.1 16.9 20.6 17.8C20.5 18 20.3 18.2 20.1 18.3C19.7 18.5 19.4 18.8 19.1 19C18.9 19.1 18.7 19.3 18.5 19.4C18 19.8 17.2 20.3 17 20.5C16.9 20.5 16.8 20.6 16.7 20.7C16.3 20.8 16.1 20.9 16 20.9C15.5 20.8 14.9 20.5 14.2 20.2C13.7 19.8 12.7 18.9 12.4 18.6C12.3 18.5 12.2 18.4 12.1 18.2C11.9 18 11.2 17.1 11 16.6C10.9 16.5 10.8 16.4 10.7 16.3C10.7 16.2 10.6 16.1 10.6 15.9C10.5 15.9 10.4 15.7 10.4 15.6C10.3 15.5 10.2 15.2 10.1 15C10 14.8 9.8 14.4 9.7 14.2C9.6 14 9.5 13.7 9.4 13.6C9.4 13.4 9.3 13.2 9.2 13C9.2 12.8 9 12.6 9 12.5C8.9 12.3 8.7 12.1 8.7 12C8.7 12 8.5 11.7 8.5 11.7C8.3 11.2 8.2 10.9 7.5 10C6.4 8.3 4.2 6.4 2.7 5.8C2 5.5 1.6 5.4 1.5 5.5ZM28.4 16.7C29.2 16.9 29.7 17.2 29.9 17.7C30.1 18.2 30 18.9 29.6 19.4C29.2 19.9 28.2 20.9 27.6 21.2C26.8 21.7 25.3 22.4 24.2 22.7C23.9 22.8 23.6 22.9 23.5 22.9C22.8 23.2 20.7 23.7 19.6 24C19 24.2 18.4 24.3 18.1 24.4C17.9 24.4 16.1 24.9 15.3 25.1C14.6 25.2 14.7 25.1 15.7 23.6C16.4 22.7 18.5 20.6 19.4 19.9C20.8 18.8 21.4 18.4 23.2 17.6C23.8 17.4 25.6 16.8 25.9 16.8C25.9 16.8 26.3 16.8 26.6 16.7C26.8 16.7 28.2 16.7 28.4 16.7ZM30.4 20.2C30.5 20.3 30.6 20.6 30.6 20.7C31.7 23.1 33.4 24.6 35.6 25.3C36.1 25.5 36.7 25.7 37.2 25.8C37.7 25.8 37.8 25.9 37.8 26.1C37.7 26.2 37.7 26.3 37.7 26.5C37.7 26.6 37.7 26.8 37.6 27C37.4 28.5 36.8 29.9 36.1 31C36.1 31.2 35.9 31.4 35.9 31.6C35.7 31.9 35.2 32.6 34.7 33.3C32.6 35.6 29.7 37.3 26.6 37.8C26.5 37.8 26.3 37.9 26.2 37.9C25.8 37.9 23.7 37.9 23.3 37.9C23.2 37.9 23 37.8 22.9 37.8C21.1 37.6 19.3 36.8 18 35.7C17.7 35.4 17 34.7 16.8 34.5C16.3 33.8 16.1 33 16.3 32.5C16.5 31.8 16.8 31.6 17.9 31.1C19.1 30.5 19.4 30.4 20.2 30C21.4 29.4 23.2 28.4 23.9 27.9C23.9 27.8 24.2 27.7 24.5 27.6C26.3 26.3 28 24.6 28.9 22.9C29.5 22.2 29.7 21.6 29.9 20.7C30.1 20 30.2 19.9 30.4 20.2Z\"/></svg>";
935
930
  const marker = "data-dsh-magpie-connector-settings-nav";
936
931
  const MAGPIE_SETTINGS_NAV_CSS = `
@@ -970,8 +965,7 @@ window.__ModuleLoader__.load({
970
965
  style.remove();
971
966
  };
972
967
  }
973
- //#endregion
974
- //#region src/client/index.ts
968
+ // src/client/index.ts
975
969
  const name = "dsh-magpie-connector/client";
976
970
  const inject = ["slots"];
977
971
  function apply(ctx, api = createFetchApi()) {
@@ -996,7 +990,6 @@ window.__ModuleLoader__.load({
996
990
  })
997
991
  }, SpeedSelect));
998
992
  }
999
- //#endregion
1000
993
  exports.apply = apply;
1001
994
  exports.inject = inject;
1002
995
  exports.name = name;
@@ -230,7 +230,7 @@ export class MagpieAdapter extends LlmAdapter {
230
230
  if (options.stop?.length)
231
231
  throw new Error('MAGPIE_STOP_UNSUPPORTED');
232
232
  const api = entry.nativeEndpoints.includes('/v1/responses') ? 'openai-responses' : 'openai-completions';
233
- const model = { id: entry.info.id, name: entry.info.name, provider: 'dsh-magpie-connector', api, baseUrl: op.gen.baseUrl, reasoning: entry.efforts.length > 0, thinkingLevelMap: { off: null, ...Object.fromEntries(entry.efforts.map(e => [e, e])) }, input: entry.info.inputModalities?.includes('image') ? ['text', 'image'] : ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: entry.info.context?.contextWindow ?? 128000, maxTokens: entry.info.defaultMaxTokens ?? 16384,
233
+ const model = { id: entry.info.id, name: entry.info.name, provider: 'dsh-magpie-connector', api, baseUrl: op.gen.baseUrl, reasoning: entry.efforts.length > 0, thinkingLevelMap: { off: null, ...Object.fromEntries(entry.efforts.map(e => [e, e])) }, input: entry.info.inputModalities?.includes('image') ? ['text', 'image'] : ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: entry.info.context?.contextWindow ?? 128000, maxTokens: entry.outputLimit ?? 16384,
234
234
  compat: { supportsFinishReason: true, thinkingFormat: 'openai', maxTokensField: 'max_completion_tokens', ...(api === 'openai-responses' ? { supportsStrictMode: true } : {}) } };
235
235
  const context = await this.context(options, model);
236
236
  const fast = !!options.sessionId && this.isFast(options.sessionId) && entry.serviceTiers.includes('priority');
@@ -5,13 +5,22 @@ type DisplayModelInfo = LlmResolvedModelInfo & {
5
5
  label: string;
6
6
  };
7
7
  };
8
+ /** The gateway's `max_output_tokens`, the model's reply capacity; `info.defaultMaxTokens` is the smaller per-request budget. */
8
9
  export interface CatalogModel {
9
10
  info: DisplayModelInfo;
10
11
  provider: string;
11
12
  efforts: string[];
12
13
  nativeEndpoints: string[];
13
14
  serviceTiers: string[];
15
+ outputLimit?: number;
14
16
  }
17
+ /**
18
+ * DSH compaction-basic's default `headroomTokens` (DSH 0.2.0-rc.2, lib/index.js) and Magpie's `dshHeadroom`.
19
+ * Re-check on every DSH upgrade: DSH reserves `defaultMaxTokens` plus this headroom out of the window before compacting.
20
+ */
21
+ export declare const COMPACTION_HEADROOM_TOKENS = 65536;
22
+ /** Magpie's `dshMaxTokens` (without its user-set Max output exception, which /models does not expose). */
23
+ export declare const defaultOutputBudget: (contextWindow: number | undefined, outputLimit: number) => number;
15
24
  export declare const strings: (value: unknown) => string[];
16
25
  export declare function mapCatalog(body: unknown): CatalogModel[];
17
26
  export {};
@@ -1,3 +1,11 @@
1
+ /**
2
+ * DSH compaction-basic's default `headroomTokens` (DSH 0.2.0-rc.2, lib/index.js) and Magpie's `dshHeadroom`.
3
+ * Re-check on every DSH upgrade: DSH reserves `defaultMaxTokens` plus this headroom out of the window before compacting.
4
+ */
5
+ export const COMPACTION_HEADROOM_TOKENS = 65536;
6
+ /** Magpie's `dshMaxTokens` (without its user-set Max output exception, which /models does not expose). */
7
+ export const defaultOutputBudget = (contextWindow, outputLimit) => contextWindow === undefined ? outputLimit : Math.min(outputLimit, Math.max(1, Math.floor(contextWindow / 4), Math.floor(contextWindow / 2) - COMPACTION_HEADROOM_TOKENS));
8
+ const positive = (value) => Number.isSafeInteger(value) && value > 0 ? value : undefined;
1
9
  export const strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === 'string' && !!v) : [];
2
10
  export function mapCatalog(body) {
3
11
  const data = body?.data;
@@ -8,17 +16,18 @@ export function mapCatalog(body) {
8
16
  return [];
9
17
  const efforts = [...new Set(strings(raw.reasoning_levels))];
10
18
  const ownedBy = typeof raw.owned_by === 'string' && raw.owned_by ? raw.owned_by : undefined;
19
+ const contextWindow = positive(raw.context_window), outputLimit = positive(raw.max_output_tokens);
11
20
  const info = {
12
21
  provider: 'dsh-magpie-connector', id: raw.id, name: typeof raw.display_name === 'string' && raw.display_name ? raw.display_name : raw.id,
13
22
  // Namespace raw owners so a real 'other' cannot collide with missing ownership.
14
23
  displayGroup: ownedBy === undefined ? { id: 'other', label: 'Magpie (Other)' }
15
24
  : { id: ownedBy === 'claude' || ownedBy === 'codex' ? ownedBy : `owned-by:${ownedBy}`, label: `Magpie (${ownedBy === 'claude' ? 'Claude' : ownedBy === 'codex' ? 'GPT' : ownedBy})` },
16
25
  inputModalities: strings(raw.modalities?.input).includes('image') ? ['text', 'image'] : ['text'],
17
- ...(Number.isSafeInteger(raw.context_window) && raw.context_window > 0 ? { context: { contextWindow: raw.context_window } } : {}),
18
- ...(Number.isSafeInteger(raw.max_output_tokens) && raw.max_output_tokens > 0 ? { defaultMaxTokens: raw.max_output_tokens } : {}),
26
+ ...(contextWindow !== undefined ? { context: { contextWindow } } : {}),
27
+ ...(outputLimit !== undefined ? { defaultMaxTokens: defaultOutputBudget(contextWindow, outputLimit) } : {}),
19
28
  ...(efforts.length ? { reasoning: { efforts: efforts.map(id => ({ id: id, name: id })) } } : {}),
20
29
  };
21
- return [{ info, provider: ownedBy ?? raw.id.split('/')[0], efforts,
30
+ return [{ info, provider: ownedBy ?? raw.id.split('/')[0], efforts, ...(outputLimit !== undefined ? { outputLimit } : {}),
22
31
  nativeEndpoints: strings(raw.native_endpoints), serviceTiers: Array.isArray(raw.service_tiers) ? raw.service_tiers.flatMap((t) => typeof t?.id === 'string' ? [t.id] : []) : [] }];
23
32
  });
24
33
  }
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "dsh-magpie-connector",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "DeepSeek Harness plugin: connect a self-hosted Magpie gateway as one LLM provider with models grouped by upstream provider, read-only usage cards, model visibility, default effort and per-session fast mode",
5
5
  "license": "MIT",
6
6
  "author": "prgrmrwy",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/prgrmrwy/dsh-magpie-connector.git",
10
- "directory": "packages/dsh-magpie-connector"
9
+ "url": "git+https://github.com/prgrmrwy/dsh-magpie-connector.git"
11
10
  },
12
11
  "homepage": "https://github.com/prgrmrwy/dsh-magpie-connector#readme",
13
12
  "bugs": {