@dan-ai-studio/dshopencodego 0.1.84 → 0.1.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -201,7 +201,7 @@ The gateway's `/v1/models` answers only *which* models exist, never *how* to cal
201
201
  - the bundled catalog is **frozen at the plugin's version** and affects only the quality of the "how to call it" decision, never *which* models exist.
202
202
  - **fresh installs do not wait**: the bundle carries a directory snapshot from publish time (`src/catalog/seed.snapshot.json`), which answers at once when the disk cache is empty and refreshes behind it; past 24 hours without a refresh counts as stale and refreshes in the background too, whatever the TTL says. A timer re-checks on a live host, so long-running processes stay current without a request to trigger it.
203
203
 
204
- When the gateway is **temporarily unavailable**, the plugin keeps the last listing that succeeded, marks it stale on the page, and retries in the background. The retry rate is governed by a **five-minute back-off** rather than by the TTL, so a gateway that comes back is picked up within five minutes; while it stays down it is asked once, not once per model request. The settings page's Recheck button revalidates at any time and is not bound by that back-off.
204
+ When the gateway is **temporarily unavailable**, the plugin keeps the last listing that succeeded, marks it stale on the page, and retries in the background. The retry rate is governed by a **five-minute back-off** rather than by the TTL, so a gateway that comes back is picked up within five minutes; while it stays down it is asked once, not once per model request. When the settings page opens on a stale reading, it follows up on its own (about every 5 seconds) until the reading turns current — the follow-ups are cheap local reads, and the host's back-off still decides when the gateway is really asked — so there is no need to press Recheck by hand. The settings page's Recheck button revalidates at any time and is not bound by that back-off.
205
205
 
206
206
  The only thing that waits on a plugin release is a missing wire quirk in the pi-ai catalog (DeepSeek's `thinkingFormat`, say) — the model itself is already usable.
207
207
 
@@ -291,7 +291,7 @@ npm run seed # refresh the bundled catalog seed before a release (see "Re
291
291
 
292
292
  **Order matters**: `npm run build` must come before `npm test` — `tests/pack-contents.spec.ts` walks every `exports` target, and whether `lib/` exists decides whether that check runs or is silently skipped (it does not fail, it just stops protecting you). `package.json` no longer carries `prepare`, so `npm ci` will not build for you; all three CI workflows run `npm run build` explicitly.
293
293
 
294
- The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **60 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
294
+ The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **61 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
295
295
 
296
296
  ## Releasing (maintainers)
297
297
 
package/README.md CHANGED
@@ -200,7 +200,7 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
200
200
  - 内置目录**冻结**在插件版本里,仅影响"怎么调"的推断质量,不影响"有哪些"。
201
201
  - **首次安装不等待**:包里带了一份发布时生成的目录快照(`src/catalog/seed.snapshot.json`,28 个模型上下),磁盘缓存为空时直接用它回答并在后台刷新;过期(超 24 小时未刷新)同样后台刷新,绝不为等网络拦住请求。插件挂载后还会定时触发一次,保证长期运行的进程目录常新。
202
202
 
203
- 网关**临时不可用**时,插件保留上次成功获取的列表并在页面上标注陈旧,同时在后台重试:重试由**5 分钟的退避**控制,而不是等满一个 TTL,所以网关恢复后 5 分钟内列表就会更新;网关持续不可用时,它只被问一次,不会每次模型请求都去问一遍。设置页的「重新检查」随时可以强制重验,不受这个退避限制。
203
+ 网关**临时不可用**时,插件保留上次成功获取的列表并在页面上标注陈旧,同时在后台重试:重试由**5 分钟的退避**控制,而不是等满一个 TTL,所以网关恢复后 5 分钟内列表就会更新;网关持续不可用时,它只被问一次,不会每次模型请求都去问一遍。设置页打开时若读到的是旧目录,会自动跟进重读直到拿到最新的(约每 5 秒一次读本地快照;何时真问网关由 Host 的 5 分钟退避决定,不会因此多发请求),不用手动点「重新检查」;「重新检查」随时可以强制重验,不受这个退避限制。
204
204
 
205
205
  只有当 pi-ai 内置目录**缺了某个模型的线怪癖**(如 DeepSeek 的 `thinkingFormat`)时,才需要等插件同步 pi-ai 的新版本——而模型本身早已可用。
206
206
 
@@ -290,7 +290,7 @@ npm run seed # 发版前刷新捆绑的目录种子(见「发布」)
290
290
 
291
291
  **顺序有讲究**:`npm run build` 要排在 `npm test` 之前——`tests/pack-contents.spec.ts` 会遍历 `exports` 的每个目标,`lib/` 存在与否决定这条检查是执行还是被跳过(不报错,但会少一层保护)。`package.json` 已移除 `prepare`,因此 `npm ci` 不会再替你隐式构建;三个 CI workflow 都显式跑了 `npm run build`。
292
292
 
293
- 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **60 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
293
+ 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **61 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
294
294
 
295
295
  ## 发布(维护者)
296
296
 
package/lib/client.js CHANGED
@@ -208,6 +208,10 @@ var networkRemote = {
208
208
  ]
209
209
  };
210
210
 
211
+ // src/request-log/store.ts
212
+ var MAX_STREAM_MARKS = 200;
213
+ var MIN_STREAM_MS_FOR_RATE = 100;
214
+
211
215
  // src/request-log/contract.ts
212
216
  function count(key, row, optional) {
213
217
  const entry = row[key];
@@ -254,6 +258,8 @@ function parseEntry(value) {
254
258
  ...row["costUsd"] === void 0 ? {} : { costUsd: rate("costUsd", row) },
255
259
  ...row["requestBytes"] === void 0 ? {} : { requestBytes: count("requestBytes", row) },
256
260
  ...row["prepMs"] === void 0 ? {} : { prepMs: count("prepMs", row) },
261
+ ...row["responseMs"] === void 0 ? {} : { responseMs: count("responseMs", row) },
262
+ ...row["streamMarksMs"] === void 0 ? {} : { streamMarksMs: parseMarks(row["streamMarksMs"]) },
257
263
  ...row["imageBytes"] === void 0 ? {} : { imageBytes: count("imageBytes", row) },
258
264
  ...row["durationMs"] === void 0 ? {} : { durationMs: count("durationMs", row) },
259
265
  ...row["firstTokenMs"] === void 0 ? {} : { firstTokenMs: count("firstTokenMs", row) },
@@ -281,6 +287,15 @@ function parseEntry(value) {
281
287
  } : {}
282
288
  };
283
289
  }
290
+ function parseMarks(value) {
291
+ if (!Array.isArray(value)) throw new Error("invalid request-log stream marks");
292
+ const kept = [];
293
+ for (const mark of value) {
294
+ if (typeof mark !== "number" || !Number.isFinite(mark) || mark < 0 || kept.length >= MAX_STREAM_MARKS) continue;
295
+ kept.push(mark);
296
+ }
297
+ return kept;
298
+ }
284
299
  function rate(key, row) {
285
300
  const entry = row[key];
286
301
  if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 0) {
@@ -569,6 +584,7 @@ var en = {
569
584
  requestLogColPrep: "Prep",
570
585
  requestLogColWait: "Wait",
571
586
  requestLogTimelineWait: "Waiting for first token",
587
+ requestLogTimelineFirstByte: "First byte",
572
588
  requestLogTimelineStream: "Streaming",
573
589
  requestLogTimelineBurst: "Delivered in one burst",
574
590
  requestLogColStream: "Content span",
@@ -787,6 +803,7 @@ var zh = {
787
803
  requestLogDetailUrl: "\u5730\u5740",
788
804
  requestLogDetailRequestBytes: "\u8BF7\u6C42\u4F53",
789
805
  requestLogDetailPrep: "\u51C6\u5907\u8017\u65F6",
806
+ requestLogTimelineFirstByte: "\u9996\u5B57\u8282",
790
807
  requestLogDetailContentSplit: "\u5185\u5BB9\u62C6\u5206",
791
808
  requestLogDetailText: "\u6587\u672C",
792
809
  requestLogDetailImages: "\u56FE\u7247",
@@ -1049,6 +1066,7 @@ var section_default = { "badge": "d7lz0W_badge", "block": "d7lz0W_block", "butto
1049
1066
  var import_jsx_runtime = require("react/jsx-runtime");
1050
1067
  var API_KEY_REF = "OPENCODE_GO_API_KEY";
1051
1068
  var VISIBILITY_WRITE_DELAY_MS = 250;
1069
+ var FOLLOW_UP_MS = 5e3;
1052
1070
  var INITIAL = {
1053
1071
  loading: true,
1054
1072
  reading: void 0,
@@ -1099,6 +1117,8 @@ var SectionController = class _SectionController {
1099
1117
  pendingVisibility;
1100
1118
  /** Timer of the coalescing window, when one is armed. */
1101
1119
  visibilityTimer;
1120
+ /** The armed stale-reading follow-up, if one is waiting. */
1121
+ followTimer;
1102
1122
  constructor(services) {
1103
1123
  this.services = services;
1104
1124
  const scope = services.scope;
@@ -1126,6 +1146,10 @@ var SectionController = class _SectionController {
1126
1146
  dispose() {
1127
1147
  this.listeners.clear();
1128
1148
  for (const dispose of this.disposers.splice(0)) dispose();
1149
+ if (this.followTimer !== void 0) {
1150
+ clearTimeout(this.followTimer);
1151
+ this.followTimer = void 0;
1152
+ }
1129
1153
  const scope = this.services.scope;
1130
1154
  if (this.visibilityTimer !== void 0 && scope !== void 0) void this.flushVisibility(scope);
1131
1155
  }
@@ -1140,6 +1164,7 @@ var SectionController = class _SectionController {
1140
1164
  const result = refresh ? await this.services.remote.opencodeGoCatalog.refresh() : await this.services.remote.opencodeGoCatalog.read();
1141
1165
  if (!result.ok) throw result.error;
1142
1166
  this.set({ loading: false, reading: result.value, failure: void 0 });
1167
+ if (result.value.cached === true) this.followReading();
1143
1168
  } catch (error) {
1144
1169
  this.set({
1145
1170
  loading: false,
@@ -1147,6 +1172,35 @@ var SectionController = class _SectionController {
1147
1172
  });
1148
1173
  }
1149
1174
  }
1175
+ /**
1176
+ * Re-read a stale reading until it turns live, then stop.
1177
+ *
1178
+ * Only one follow-up is ever armed. The Host decides the real refresh
1179
+ * rhythm, so this never hammers the gateway — a follow-up is a plain
1180
+ * `read`, and the Host's own back-off decides how often the gateway is
1181
+ * really asked. A failed or still-stale read re-arms; only a live reading
1182
+ * or a disposal lets it lapse.
1183
+ */
1184
+ followReading() {
1185
+ if (this.followTimer !== void 0) return;
1186
+ const timer = setTimeout(() => {
1187
+ this.followTimer = void 0;
1188
+ void (async () => {
1189
+ try {
1190
+ const result = await this.services.remote.opencodeGoCatalog.read();
1191
+ if (result.ok) {
1192
+ this.set({ reading: result.value, failure: void 0 });
1193
+ if (result.value.cached !== true) return;
1194
+ }
1195
+ } catch {
1196
+ }
1197
+ this.followReading();
1198
+ })();
1199
+ }, FOLLOW_UP_MS);
1200
+ const releasable = timer;
1201
+ if (typeof releasable.unref === "function") releasable.unref();
1202
+ this.followTimer = timer;
1203
+ }
1150
1204
  async loadKey() {
1151
1205
  try {
1152
1206
  const described = await this.services.remote.credentials.describe([API_KEY_REF]);
@@ -2153,19 +2207,16 @@ function Section({ controller, t, getLocale, openRequestLog }) {
2153
2207
  var import_react2 = require("react");
2154
2208
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
2155
2209
 
2156
- // src/request-log/store.ts
2157
- var MIN_STREAM_MS_FOR_RATE = 100;
2158
-
2159
2210
  // plugin-css:/home/runner/work/dshopencodego/dshopencodego/src/client/request-log.module.css
2160
2211
  var id2 = "@dan-ai-studio/dshopencodego/request-log.module.css";
2161
2212
  if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id2) + "]")) {
2162
2213
  const style = document.createElement("style");
2163
2214
  style.dataset.plugin = "@dan-ai-studio/dshopencodego";
2164
2215
  style.dataset.pluginCss = id2;
2165
- style.textContent = ".Lx7MOq_page{box-sizing:border-box;background:var(--dsw-alias-bg-base);height:100%;min-height:0;color:var(--dsw-alias-label-primary);flex-direction:column;display:flex}.Lx7MOq_pageLoose{height:auto}.Lx7MOq_pageLoose .Lx7MOq_tableWrap{overflow:visible}.Lx7MOq_pageLoose .Lx7MOq_stuck{z-index:3;background:var(--dsw-alias-bg-base);position:sticky;top:0}.Lx7MOq_pageLoose .Lx7MOq_table th{top:var(--request-log-stuck,0px)}.Lx7MOq_pageLoose .Lx7MOq_foot{z-index:2;background:var(--dsw-alias-bg-base);position:sticky;bottom:0}.Lx7MOq_head{flex-wrap:wrap;flex:none;align-items:center;gap:12px;padding:20px clamp(16px,3vw,32px) 12px;display:flex}.Lx7MOq_headText{min-width:0}.Lx7MOq_title{margin:0;font-size:20px;font-weight:500;line-height:28px}.Lx7MOq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px;line-height:20px}.Lx7MOq_headActions{flex:none;align-items:center;gap:8px;margin-left:auto;display:flex}.Lx7MOq_button{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);height:28px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;align-items:center;gap:6px;padding:0 12px;font-size:13px;display:inline-flex}.Lx7MOq_button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.Lx7MOq_button:disabled{cursor:default;opacity:.5}.Lx7MOq_summary{flex-wrap:wrap;flex:none;gap:8px;padding:0 clamp(16px,3vw,32px) 12px;display:flex}.Lx7MOq_card{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);flex-direction:column;gap:2px;min-width:104px;padding:8px 12px;display:flex}.Lx7MOq_cardLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.Lx7MOq_cardValue{font-variant-numeric:tabular-nums;font-size:16px;font-weight:500;line-height:22px}.Lx7MOq_cardError .Lx7MOq_cardValue{color:var(--dsw-alias-state-error-primary)}.Lx7MOq_cardWarn .Lx7MOq_cardValue{color:var(--dsw-alias-state-warn-primary)}.Lx7MOq_toolbar{flex-wrap:wrap;flex:none;align-items:center;gap:8px 12px;padding:0 clamp(16px,3vw,32px) 12px;display:flex}.Lx7MOq_field{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:13px;line-height:20px;display:inline-flex}.Lx7MOq_select,.Lx7MOq_input{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);height:28px;color:var(--dsw-alias-label-primary);font:inherit;padding:0 8px;font-size:13px}.Lx7MOq_input{width:160px}.Lx7MOq_inputNumber{width:72px}.Lx7MOq_checkbox{accent-color:var(--dsw-alias-brand-primary)}.Lx7MOq_sessionLink{color:var(--dsw-alias-link);font:inherit;text-align:left;cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0}.Lx7MOq_sessionLink:hover{text-decoration:underline}.Lx7MOq_sessionText{text-overflow:ellipsis;vertical-align:bottom;max-width:340px;display:inline-block;overflow:hidden}.Lx7MOq_modelText{text-overflow:ellipsis;vertical-align:bottom;max-width:220px;display:inline-block;overflow:hidden}.Lx7MOq_tableWrap{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);flex:1;min-height:0;margin:0 clamp(16px,3vw,32px);overflow:auto}.Lx7MOq_table{border-collapse:separate;border-spacing:0;width:100%;min-width:940px;font-size:13px;line-height:20px}.Lx7MOq_table th{z-index:1;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);text-align:left;white-space:nowrap;padding:8px 10px;font-size:12px;font-weight:500;position:sticky;top:0}.Lx7MOq_thSortable{cursor:pointer;user-select:none}.Lx7MOq_thSortable:hover{color:var(--dsw-alias-label-primary)}.Lx7MOq_table td{border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;font-variant-numeric:tabular-nums;padding:6px 10px}.Lx7MOq_table tbody tr:last-child td{border-bottom:none}.Lx7MOq_row{cursor:pointer}.Lx7MOq_row:hover td{background:var(--dsw-alias-interactive-bg-hover)}.Lx7MOq_rowOk td:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-border-l3)}.Lx7MOq_rowSlow td:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-warn-primary)}.Lx7MOq_rowFail td:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-error-primary)}.Lx7MOq_rowRunning td:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-brand-primary)}.Lx7MOq_rowFail td:last-child,.Lx7MOq_rowSlow td:last-child{color:var(--dsw-alias-state-error-primary)}.Lx7MOq_group td{border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);padding-top:12px;font-weight:500}.Lx7MOq_detail td{border-bottom:1px solid var(--dsw-alias-border-l1);white-space:normal;background:var(--dsw-alias-bg-layer-2);padding:12px 10px 16px}.Lx7MOq_detailGrid{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:6px 24px;display:grid}.Lx7MOq_timeline{flex-direction:column;grid-column:1/-1;gap:6px;padding:4px 0 8px;display:flex}.Lx7MOq_timelineTrackWrap{padding-bottom:18px;position:relative}.Lx7MOq_timelineTrack{background:var(--dsw-alias-bg-layer-1);border-radius:999px;height:10px;position:relative;overflow:hidden}.Lx7MOq_timelineMidTick{background:var(--dsw-alias-label-tertiary);opacity:.7;width:1px;position:absolute;top:0;bottom:0;left:50%}.Lx7MOq_timelineMid{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-variant-numeric:tabular-nums;font-size:11px;line-height:16px;position:absolute;top:100%;transform:translate(-50%)}.Lx7MOq_timelineSeg{min-width:2px;position:absolute;top:0;bottom:0}.Lx7MOq_timelinePrep{background:var(--dsw-alias-brand-primary)}.Lx7MOq_timelineWait{background:var(--dsw-alias-state-warn-primary)}.Lx7MOq_timelineStream{background:var(--dsw-alias-brand-primary);opacity:.45}.Lx7MOq_timelineTick{background:var(--dsw-alias-label-primary);width:2px;margin-left:-2px;position:absolute;top:-2px;bottom:-2px;left:100%}.Lx7MOq_timelineLegend{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:4px 16px;font-size:12px;display:flex}.Lx7MOq_timelineItem{white-space:nowrap;align-items:center;gap:6px;display:inline-flex}.Lx7MOq_timelineSwatch{border-radius:2px;width:8px;height:8px}.Lx7MOq_timelineTickSwatch{background:var(--dsw-alias-label-primary)}.Lx7MOq_detailItem{gap:8px;min-width:0;font-size:13px;line-height:20px;display:flex}.Lx7MOq_detailLabel{color:var(--dsw-alias-label-tertiary);flex:none}.Lx7MOq_detailValue{overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-primary)}.Lx7MOq_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}.Lx7MOq_empty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:48px 16px}.Lx7MOq_foot{color:var(--dsw-alias-label-tertiary);flex-wrap:wrap;flex:none;align-items:center;gap:12px;padding:12px clamp(16px,3vw,32px) 20px;font-size:12px;display:flex}.Lx7MOq_pager{align-items:center;gap:8px;margin-left:auto;display:flex}.Lx7MOq_notice{color:var(--dsw-alias-label-secondary);padding:0 clamp(16px,3vw,32px) 12px;font-size:13px;line-height:20px}.Lx7MOq_noticeError{color:var(--dsw-alias-state-error-primary)}";
2216
+ style.textContent = ".Lx7MOq_page{box-sizing:border-box;background:var(--dsw-alias-bg-base);height:100%;min-height:0;color:var(--dsw-alias-label-primary);flex-direction:column;display:flex}.Lx7MOq_pageLoose{height:auto}.Lx7MOq_pageLoose .Lx7MOq_tableWrap{overflow:visible}.Lx7MOq_pageLoose .Lx7MOq_stuck{z-index:3;background:var(--dsw-alias-bg-base);position:sticky;top:0}.Lx7MOq_pageLoose .Lx7MOq_table th{top:var(--request-log-stuck,0px)}.Lx7MOq_pageLoose .Lx7MOq_foot{z-index:2;background:var(--dsw-alias-bg-base);position:sticky;bottom:0}.Lx7MOq_head{flex-wrap:wrap;flex:none;align-items:center;gap:12px;padding:20px clamp(16px,3vw,32px) 12px;display:flex}.Lx7MOq_headText{min-width:0}.Lx7MOq_title{margin:0;font-size:20px;font-weight:500;line-height:28px}.Lx7MOq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px;line-height:20px}.Lx7MOq_headActions{flex:none;align-items:center;gap:8px;margin-left:auto;display:flex}.Lx7MOq_button{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);height:28px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;align-items:center;gap:6px;padding:0 12px;font-size:13px;display:inline-flex}.Lx7MOq_button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.Lx7MOq_button:disabled{cursor:default;opacity:.5}.Lx7MOq_summary{flex-wrap:wrap;flex:none;gap:8px;padding:0 clamp(16px,3vw,32px) 12px;display:flex}.Lx7MOq_card{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);flex-direction:column;gap:2px;min-width:104px;padding:8px 12px;display:flex}.Lx7MOq_cardLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.Lx7MOq_cardValue{font-variant-numeric:tabular-nums;font-size:16px;font-weight:500;line-height:22px}.Lx7MOq_cardError .Lx7MOq_cardValue{color:var(--dsw-alias-state-error-primary)}.Lx7MOq_cardWarn .Lx7MOq_cardValue{color:var(--dsw-alias-state-warn-primary)}.Lx7MOq_toolbar{flex-wrap:wrap;flex:none;align-items:center;gap:8px 12px;padding:0 clamp(16px,3vw,32px) 12px;display:flex}.Lx7MOq_field{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:13px;line-height:20px;display:inline-flex}.Lx7MOq_select,.Lx7MOq_input{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);height:28px;color:var(--dsw-alias-label-primary);font:inherit;padding:0 8px;font-size:13px}.Lx7MOq_input{width:160px}.Lx7MOq_inputNumber{width:72px}.Lx7MOq_checkbox{accent-color:var(--dsw-alias-brand-primary)}.Lx7MOq_sessionLink{color:var(--dsw-alias-link);font:inherit;text-align:left;cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0}.Lx7MOq_sessionLink:hover{text-decoration:underline}.Lx7MOq_sessionText{text-overflow:ellipsis;vertical-align:bottom;max-width:340px;display:inline-block;overflow:hidden}.Lx7MOq_modelText{text-overflow:ellipsis;vertical-align:bottom;max-width:220px;display:inline-block;overflow:hidden}.Lx7MOq_tableWrap{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);flex:1;min-height:0;margin:0 clamp(16px,3vw,32px);overflow:auto}.Lx7MOq_table{border-collapse:separate;border-spacing:0;width:100%;min-width:940px;font-size:13px;line-height:20px}.Lx7MOq_table th{z-index:1;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);text-align:left;white-space:nowrap;padding:8px 10px;font-size:12px;font-weight:500;position:sticky;top:0}.Lx7MOq_thSortable{cursor:pointer;user-select:none}.Lx7MOq_thSortable:hover{color:var(--dsw-alias-label-primary)}.Lx7MOq_table td{border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;font-variant-numeric:tabular-nums;padding:6px 10px}.Lx7MOq_table tbody tr:last-child td{border-bottom:none}.Lx7MOq_row{cursor:pointer}.Lx7MOq_row:hover td{background:var(--dsw-alias-interactive-bg-hover)}.Lx7MOq_rowOk td:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-border-l3)}.Lx7MOq_rowSlow td:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-warn-primary)}.Lx7MOq_rowFail td:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-error-primary)}.Lx7MOq_rowRunning td:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-brand-primary)}.Lx7MOq_rowFail td:last-child,.Lx7MOq_rowSlow td:last-child{color:var(--dsw-alias-state-error-primary)}.Lx7MOq_group td{border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);padding-top:12px;font-weight:500}.Lx7MOq_detail td{border-bottom:1px solid var(--dsw-alias-border-l1);white-space:normal;background:var(--dsw-alias-bg-layer-2);padding:12px 10px 16px}.Lx7MOq_detailGrid{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:6px 24px;display:grid}.Lx7MOq_timeline{flex-direction:column;grid-column:1/-1;gap:6px;padding:4px 0 8px;display:flex}.Lx7MOq_timelineTrackWrap{padding-bottom:18px;position:relative}.Lx7MOq_timelineTrack{background:var(--dsw-alias-bg-layer-1);border-radius:999px;height:10px;position:relative;overflow:hidden}.Lx7MOq_timelineMidTick{background:var(--dsw-alias-label-tertiary);opacity:.7;width:1px;position:absolute;top:0;bottom:0;left:50%}.Lx7MOq_timelineMid{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-variant-numeric:tabular-nums;font-size:11px;line-height:16px;position:absolute;top:100%;transform:translate(-50%)}.Lx7MOq_timelineSeg{min-width:2px;position:absolute;top:0;bottom:0}.Lx7MOq_timelinePrep{background:var(--dsw-alias-brand-primary)}.Lx7MOq_timelineWait{background:var(--dsw-alias-state-warn-primary)}.Lx7MOq_timelineStream{background:var(--dsw-alias-brand-primary);opacity:.45}.Lx7MOq_timelineStreamAlt{background:var(--dsw-alias-brand-primary);opacity:.9}.Lx7MOq_timelineRespTick{background:var(--dsw-alias-label-primary);opacity:.7;width:1px;position:absolute;top:-2px;bottom:-2px}.Lx7MOq_timelineTick{background:var(--dsw-alias-label-primary);width:2px;margin-left:-2px;position:absolute;top:-2px;bottom:-2px;left:100%}.Lx7MOq_timelineLegend{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:4px 16px;font-size:12px;display:flex}.Lx7MOq_timelineItem{white-space:nowrap;align-items:center;gap:6px;display:inline-flex}.Lx7MOq_timelineSwatch{border-radius:2px;width:8px;height:8px}.Lx7MOq_timelineTickSwatch{background:var(--dsw-alias-label-primary)}.Lx7MOq_detailItem{gap:8px;min-width:0;font-size:13px;line-height:20px;display:flex}.Lx7MOq_detailLabel{width:88px;color:var(--dsw-alias-label-tertiary);flex:none}.Lx7MOq_detailValue{overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-primary)}.Lx7MOq_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}.Lx7MOq_empty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:48px 16px}.Lx7MOq_foot{color:var(--dsw-alias-label-tertiary);flex-wrap:wrap;flex:none;align-items:center;gap:12px;padding:12px clamp(16px,3vw,32px) 20px;font-size:12px;display:flex}.Lx7MOq_pager{align-items:center;gap:8px;margin-left:auto;display:flex}.Lx7MOq_notice{color:var(--dsw-alias-label-secondary);padding:0 clamp(16px,3vw,32px) 12px;font-size:13px;line-height:20px}.Lx7MOq_noticeError{color:var(--dsw-alias-state-error-primary)}";
2166
2217
  document.head.appendChild(style);
2167
2218
  }
2168
- var request_log_default = { "button": "Lx7MOq_button", "card": "Lx7MOq_card", "cardError": "Lx7MOq_cardError", "cardLabel": "Lx7MOq_cardLabel", "cardValue": "Lx7MOq_cardValue", "cardWarn": "Lx7MOq_cardWarn", "checkbox": "Lx7MOq_checkbox", "detail": "Lx7MOq_detail", "detailGrid": "Lx7MOq_detailGrid", "detailItem": "Lx7MOq_detailItem", "detailLabel": "Lx7MOq_detailLabel", "detailValue": "Lx7MOq_detailValue", "empty": "Lx7MOq_empty", "field": "Lx7MOq_field", "foot": "Lx7MOq_foot", "group": "Lx7MOq_group", "head": "Lx7MOq_head", "headActions": "Lx7MOq_headActions", "headText": "Lx7MOq_headText", "input": "Lx7MOq_input", "inputNumber": "Lx7MOq_inputNumber", "modelText": "Lx7MOq_modelText", "mono": "Lx7MOq_mono", "notice": "Lx7MOq_notice", "noticeError": "Lx7MOq_noticeError", "page": "Lx7MOq_page", "pageLoose": "Lx7MOq_pageLoose", "pager": "Lx7MOq_pager", "row": "Lx7MOq_row", "rowFail": "Lx7MOq_rowFail", "rowOk": "Lx7MOq_rowOk", "rowRunning": "Lx7MOq_rowRunning", "rowSlow": "Lx7MOq_rowSlow", "select": "Lx7MOq_select", "sessionLink": "Lx7MOq_sessionLink", "sessionText": "Lx7MOq_sessionText", "stuck": "Lx7MOq_stuck", "subtitle": "Lx7MOq_subtitle", "summary": "Lx7MOq_summary", "table": "Lx7MOq_table", "tableWrap": "Lx7MOq_tableWrap", "thSortable": "Lx7MOq_thSortable", "timeline": "Lx7MOq_timeline", "timelineItem": "Lx7MOq_timelineItem", "timelineLegend": "Lx7MOq_timelineLegend", "timelineMid": "Lx7MOq_timelineMid", "timelineMidTick": "Lx7MOq_timelineMidTick", "timelinePrep": "Lx7MOq_timelinePrep", "timelineSeg": "Lx7MOq_timelineSeg", "timelineStream": "Lx7MOq_timelineStream", "timelineSwatch": "Lx7MOq_timelineSwatch", "timelineTick": "Lx7MOq_timelineTick", "timelineTickSwatch": "Lx7MOq_timelineTickSwatch", "timelineTrack": "Lx7MOq_timelineTrack", "timelineTrackWrap": "Lx7MOq_timelineTrackWrap", "timelineWait": "Lx7MOq_timelineWait", "title": "Lx7MOq_title", "toolbar": "Lx7MOq_toolbar" };
2219
+ var request_log_default = { "button": "Lx7MOq_button", "card": "Lx7MOq_card", "cardError": "Lx7MOq_cardError", "cardLabel": "Lx7MOq_cardLabel", "cardValue": "Lx7MOq_cardValue", "cardWarn": "Lx7MOq_cardWarn", "checkbox": "Lx7MOq_checkbox", "detail": "Lx7MOq_detail", "detailGrid": "Lx7MOq_detailGrid", "detailItem": "Lx7MOq_detailItem", "detailLabel": "Lx7MOq_detailLabel", "detailValue": "Lx7MOq_detailValue", "empty": "Lx7MOq_empty", "field": "Lx7MOq_field", "foot": "Lx7MOq_foot", "group": "Lx7MOq_group", "head": "Lx7MOq_head", "headActions": "Lx7MOq_headActions", "headText": "Lx7MOq_headText", "input": "Lx7MOq_input", "inputNumber": "Lx7MOq_inputNumber", "modelText": "Lx7MOq_modelText", "mono": "Lx7MOq_mono", "notice": "Lx7MOq_notice", "noticeError": "Lx7MOq_noticeError", "page": "Lx7MOq_page", "pageLoose": "Lx7MOq_pageLoose", "pager": "Lx7MOq_pager", "row": "Lx7MOq_row", "rowFail": "Lx7MOq_rowFail", "rowOk": "Lx7MOq_rowOk", "rowRunning": "Lx7MOq_rowRunning", "rowSlow": "Lx7MOq_rowSlow", "select": "Lx7MOq_select", "sessionLink": "Lx7MOq_sessionLink", "sessionText": "Lx7MOq_sessionText", "stuck": "Lx7MOq_stuck", "subtitle": "Lx7MOq_subtitle", "summary": "Lx7MOq_summary", "table": "Lx7MOq_table", "tableWrap": "Lx7MOq_tableWrap", "thSortable": "Lx7MOq_thSortable", "timeline": "Lx7MOq_timeline", "timelineItem": "Lx7MOq_timelineItem", "timelineLegend": "Lx7MOq_timelineLegend", "timelineMid": "Lx7MOq_timelineMid", "timelineMidTick": "Lx7MOq_timelineMidTick", "timelinePrep": "Lx7MOq_timelinePrep", "timelineRespTick": "Lx7MOq_timelineRespTick", "timelineSeg": "Lx7MOq_timelineSeg", "timelineStream": "Lx7MOq_timelineStream", "timelineStreamAlt": "Lx7MOq_timelineStreamAlt", "timelineSwatch": "Lx7MOq_timelineSwatch", "timelineTick": "Lx7MOq_timelineTick", "timelineTickSwatch": "Lx7MOq_timelineTickSwatch", "timelineTrack": "Lx7MOq_timelineTrack", "timelineTrackWrap": "Lx7MOq_timelineTrackWrap", "timelineWait": "Lx7MOq_timelineWait", "title": "Lx7MOq_title", "toolbar": "Lx7MOq_toolbar" };
2169
2220
 
2170
2221
  // src/client/RequestLogPage.tsx
2171
2222
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -2254,13 +2305,15 @@ function timelineOf(entry, t) {
2254
2305
  const first = entry.firstTokenMs;
2255
2306
  if (prep === void 0 && first === void 0) return void 0;
2256
2307
  const start = prep ?? 0;
2257
- const slices = [];
2308
+ const blocks = [];
2258
2309
  if (prep !== void 0 && prep > 0) {
2259
- slices.push({ kind: "prep", label: t("requestLogDetailPrep"), from: 0, to: Math.min(prep, end) });
2310
+ blocks.push({ kind: "prep", label: t("requestLogDetailPrep"), from: 0, to: Math.min(prep, end) });
2260
2311
  }
2312
+ const responseAt = entry.responseMs === void 0 ? void 0 : Math.min(Math.max(entry.responseMs, 0), end);
2313
+ let streamTotal;
2261
2314
  if (first !== void 0) {
2262
2315
  if (first > start) {
2263
- slices.push({
2316
+ blocks.push({
2264
2317
  kind: "wait",
2265
2318
  label: t("requestLogTimelineWait"),
2266
2319
  from: Math.min(start, end),
@@ -2269,14 +2322,30 @@ function timelineOf(entry, t) {
2269
2322
  }
2270
2323
  if (entry.streamMs !== void 0 && entry.streamMs > 0) {
2271
2324
  const last = Math.min(first + entry.streamMs, end);
2272
- if (last > first) slices.push({ kind: "stream", label: t("requestLogTimelineStream"), from: first, to: last });
2325
+ if (last > first) {
2326
+ const cuts = [first, ...(entry.streamMarksMs ?? []).filter((mark) => mark > first && mark < last), last];
2327
+ for (let index = 0; index + 1 < cuts.length; index += 1) {
2328
+ const from = cuts[index];
2329
+ const to = cuts[index + 1];
2330
+ if (from === void 0 || to === void 0) continue;
2331
+ blocks.push({
2332
+ kind: index % 2 === 0 ? "stream" : "streamAlt",
2333
+ label: t("requestLogTimelineStream"),
2334
+ from,
2335
+ to
2336
+ });
2337
+ }
2338
+ streamTotal = last - first;
2339
+ }
2273
2340
  }
2274
2341
  }
2275
- if (slices.length === 0) return void 0;
2342
+ if (blocks.length === 0) return void 0;
2276
2343
  return {
2277
- slices,
2344
+ blocks,
2278
2345
  end,
2279
- burst: !running && first !== void 0 && (entry.streamMs === void 0 || entry.streamMs < MIN_STREAM_MS_FOR_RATE)
2346
+ burst: !running && first !== void 0 && (entry.streamMs === void 0 || entry.streamMs < MIN_STREAM_MS_FOR_RATE),
2347
+ responseAt,
2348
+ streamTotal
2280
2349
  };
2281
2350
  }
2282
2351
  function percent(value) {
@@ -2491,44 +2560,69 @@ function RequestLogPage({ readLog, enabled, canOpenSession, openSession, t, getL
2491
2560
  (() => {
2492
2561
  const timeline = timelineOf(entry, t);
2493
2562
  if (timeline === void 0) return null;
2494
- const kindClass = (kind) => kind === "prep" ? request_log_default.timelinePrep : kind === "wait" ? request_log_default.timelineWait : request_log_default.timelineStream;
2495
- const spoken = [
2496
- ...timeline.slices.map((slice) => `${slice.label} ${duration(slice.to - slice.from)}`),
2497
- `${t("requestLogColLatency")} ${duration(timeline.end)}`
2498
- ].join(", ");
2563
+ const kindClass = (kind) => kind === "prep" ? request_log_default.timelinePrep : kind === "wait" ? request_log_default.timelineWait : kind === "stream" ? request_log_default.timelineStream : request_log_default.timelineStreamAlt;
2564
+ const legend = [];
2565
+ const legendKey = (kind) => kind === "streamAlt" ? "stream" : kind;
2566
+ for (const block of timeline.blocks) {
2567
+ const key = legendKey(block.kind);
2568
+ const seen = legend.some((item) => item.key === key);
2569
+ if (!seen) {
2570
+ legend.push({
2571
+ key,
2572
+ swatch: kindClass(key),
2573
+ text: `${block.label} ${duration(
2574
+ timeline.blocks.filter((other) => legendKey(other.kind) === key).reduce((sum, other) => sum + (other.to - other.from), 0)
2575
+ )}`
2576
+ });
2577
+ }
2578
+ }
2579
+ if (timeline.responseAt !== void 0) {
2580
+ legend.push({
2581
+ key: "response",
2582
+ swatch: request_log_default.timelineTickSwatch,
2583
+ text: `${t("requestLogTimelineFirstByte")} ${duration(timeline.responseAt)}`
2584
+ });
2585
+ }
2586
+ legend.push({
2587
+ key: "total",
2588
+ swatch: request_log_default.timelineTickSwatch,
2589
+ text: `${t("requestLogColLatency")} ${duration(timeline.end)}`
2590
+ });
2591
+ const spoken = legend.map((item) => item.text).join(", ");
2592
+ const percent2 = (at) => `${(at / timeline.end * 100).toFixed(2)}%`;
2499
2593
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: request_log_default.timeline, role: "img", "aria-label": spoken, children: [
2500
2594
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: request_log_default.timelineTrackWrap, children: [
2501
2595
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: request_log_default.timelineTrack, children: [
2502
- timeline.slices.map((slice) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2596
+ timeline.blocks.map((block, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2503
2597
  "div",
2504
2598
  {
2505
- className: `${request_log_default.timelineSeg} ${kindClass(slice.kind)}`,
2599
+ className: `${request_log_default.timelineSeg} ${kindClass(block.kind)}`,
2506
2600
  style: {
2507
- left: `${(slice.from / timeline.end * 100).toFixed(2)}%`,
2508
- width: `${Math.max(0, (slice.to - slice.from) / timeline.end * 100).toFixed(2)}%`
2601
+ left: percent2(block.from),
2602
+ width: percent2(Math.max(0, block.to - block.from))
2509
2603
  },
2510
- title: `${slice.label} ${duration(slice.to - slice.from)}`
2604
+ title: `${block.label} ${duration(block.to - block.from)}`
2511
2605
  },
2512
- slice.kind
2606
+ `${block.kind}-${index}`
2513
2607
  )),
2608
+ timeline.responseAt !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2609
+ "div",
2610
+ {
2611
+ className: request_log_default.timelineRespTick,
2612
+ style: { left: percent2(Math.min(timeline.responseAt, timeline.end)) },
2613
+ title: `${t("requestLogTimelineFirstByte")} ${duration(timeline.responseAt)}`
2614
+ }
2615
+ ),
2514
2616
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: request_log_default.timelineMidTick }),
2515
2617
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: request_log_default.timelineTick, title: duration(timeline.end) })
2516
2618
  ] }),
2517
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: request_log_default.timelineMid, title: duration(timeline.end / 2), children: duration(timeline.end / 2) })
2619
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: request_log_default.timelineMid, style: { left: "50%" }, title: duration(timeline.end / 2), children: duration(timeline.end / 2) })
2518
2620
  ] }),
2519
2621
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: request_log_default.timelineLegend, children: [
2520
- timeline.slices.map((slice) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: request_log_default.timelineItem, children: [
2521
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `${request_log_default.timelineSwatch} ${kindClass(slice.kind)}` }),
2522
- slice.label,
2523
- " ",
2524
- duration(slice.to - slice.from)
2525
- ] }, slice.kind)),
2526
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: request_log_default.timelineItem, children: [
2527
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `${request_log_default.timelineSwatch} ${request_log_default.timelineTickSwatch}` }),
2528
- t("requestLogColLatency"),
2529
- " ",
2530
- duration(timeline.end)
2531
- ] }),
2622
+ legend.map((item) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: request_log_default.timelineItem, children: [
2623
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `${request_log_default.timelineSwatch} ${item.swatch ?? ""}` }),
2624
+ item.text
2625
+ ] }, item.key)),
2532
2626
  timeline.burst && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: request_log_default.timelineItem, children: t("requestLogTimelineBurst") })
2533
2627
  ] })
2534
2628
  ] });
package/lib/index.js CHANGED
@@ -73672,7 +73672,7 @@ import dc from "node:diagnostics_channel";
73672
73672
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
73673
73673
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
73674
73674
  import * as llm2 from "@deepseek-ai/dsh-llm";
73675
- import { LlmError as LlmError13 } from "@deepseek-ai/dsh-llm";
73675
+ import { LlmError as LlmError14 } from "@deepseek-ai/dsh-llm";
73676
73676
 
73677
73677
  // src/adapter.ts
73678
73678
  init_models();
@@ -73682,7 +73682,7 @@ import {
73682
73682
  contentHasImage as contentHasImage2,
73683
73683
  isTokenDelta,
73684
73684
  LlmAdapter,
73685
- LlmError as LlmError9,
73685
+ LlmError as LlmError10,
73686
73686
  ReasoningEffortId,
73687
73687
  resolveRetryPolicy
73688
73688
  } from "@deepseek-ai/dsh-llm";
@@ -77887,6 +77887,74 @@ function sortModels(models, now = Date.now()) {
77887
77887
  return [...models].sort((left, right) => rank(left) - rank(right) || (isNewModel(left, now) && isNewModel(right, now) ? right.releaseDate.localeCompare(left.releaseDate) : 0));
77888
77888
  }
77889
77889
 
77890
+ // src/context-fit.ts
77891
+ import { CONTEXT_WINDOW_EXCEEDED_CODE as CONTEXT_WINDOW_EXCEEDED_CODE2, LlmError as LlmError9 } from "@deepseek-ai/dsh-llm";
77892
+ var CONTEXT_FIT_RESERVE_TOKENS = 2048;
77893
+ var CONTEXT_FIT_MIN_OUTPUT_TOKENS = 256;
77894
+ var CHARS_PER_TOKEN2 = 3;
77895
+ var TOKENS_PER_MESSAGE = 8;
77896
+ var TOKENS_PER_IMAGE = 2e3;
77897
+ var IMAGE_BYTES_PER_TOKEN = 2e3;
77898
+ var TOKENS_PER_FILE = 500;
77899
+ var FILE_BYTES_PER_TOKEN = 4e3;
77900
+ function estimateInputTokens(options) {
77901
+ let chars = 0;
77902
+ let images = 0;
77903
+ let imageBytes = 0;
77904
+ let files = 0;
77905
+ let fileBytes = 0;
77906
+ if (typeof options.system === "string") chars += options.system.length;
77907
+ for (const message of options.messages) {
77908
+ for (const block of message.content) {
77909
+ switch (block.type) {
77910
+ case "text":
77911
+ case "reasoning":
77912
+ chars += block.text.length;
77913
+ break;
77914
+ case "tool-call":
77915
+ chars += block.name.length + block.arguments.length;
77916
+ break;
77917
+ case "image":
77918
+ if (block.offloaded === true) {
77919
+ chars += 200;
77920
+ } else {
77921
+ images += 1;
77922
+ imageBytes += block.attachment.bytes;
77923
+ }
77924
+ break;
77925
+ case "file":
77926
+ files += 1;
77927
+ fileBytes += block.attachment.bytes;
77928
+ chars += block.attachment.name.length;
77929
+ break;
77930
+ default:
77931
+ break;
77932
+ }
77933
+ }
77934
+ }
77935
+ if (options.tools !== void 0 && options.tools.length > 0) {
77936
+ chars += JSON.stringify(options.tools).length;
77937
+ }
77938
+ const estimate = Math.ceil(chars / CHARS_PER_TOKEN2) + options.messages.length * TOKENS_PER_MESSAGE + images * TOKENS_PER_IMAGE + Math.ceil(imageBytes / IMAGE_BYTES_PER_TOKEN) + files * TOKENS_PER_FILE + Math.ceil(fileBytes / FILE_BYTES_PER_TOKEN);
77939
+ return Number.isFinite(estimate) && estimate > 0 ? Math.floor(estimate) : 0;
77940
+ }
77941
+ function resolveFittedMaxTokens(fit) {
77942
+ const { modelId, contextWindow, routeMaxTokens, requestedMaxTokens, estimatedInputTokens } = fit;
77943
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) return requestedMaxTokens;
77944
+ const input = Number.isFinite(estimatedInputTokens) && estimatedInputTokens > 0 ? Math.floor(estimatedInputTokens) : 0;
77945
+ const allowed = Math.floor(contextWindow - input - CONTEXT_FIT_RESERVE_TOKENS);
77946
+ const over = (what) => new LlmError9(
77947
+ `opencode-go model "${modelId}" ${what} (~${input} input tokens) leaves no room inside context window ${contextWindow} (reserve ${CONTEXT_FIT_RESERVE_TOKENS}); compact the conversation or lower maxTokens`,
77948
+ CONTEXT_WINDOW_EXCEEDED_CODE2
77949
+ );
77950
+ if (allowed < CONTEXT_FIT_MIN_OUTPUT_TOKENS) throw over("input");
77951
+ const effective = requestedMaxTokens ?? routeMaxTokens;
77952
+ if (effective === void 0) return void 0;
77953
+ if (!Number.isFinite(effective) || effective <= 0) return requestedMaxTokens;
77954
+ if (effective <= allowed) return requestedMaxTokens;
77955
+ return allowed;
77956
+ }
77957
+
77890
77958
  // src/session-header.ts
77891
77959
  import { randomUUID as randomUUID2 } from "node:crypto";
77892
77960
  import { attributionHeaders as attributionHeaders4 } from "@deepseek-ai/dsh-llm";
@@ -78008,7 +78076,7 @@ function withModelLimit(model, limits) {
78008
78076
  function stopSequencesFor(model, stop) {
78009
78077
  if (stop === void 0) return void 0;
78010
78078
  if (model.api === "openai-completions") return stop;
78011
- throw new LlmError9(
78079
+ throw new LlmError10(
78012
78080
  `opencode-go model "${model.id}" speaks ${model.api}, which has no stop-sequence parameter this adapter can reach; drop GenerateOptions.stop for this model`,
78013
78081
  "UNSUPPORTED_OPTION"
78014
78082
  );
@@ -78128,7 +78196,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78128
78196
  const config = this.options.config();
78129
78197
  const snapshot2 = await this.catalogOf(config).forModel(model);
78130
78198
  const facts = snapshot2.facts.get(model);
78131
- if (facts === void 0) throw new LlmError9(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
78199
+ if (facts === void 0) throw new LlmError10(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
78132
78200
  return this.modelInfo(withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits));
78133
78201
  }
78134
78202
  /** Describe one model: capacities plus the reasoning levels it really offers. */
@@ -78174,7 +78242,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78174
78242
  if (effort === void 0 || effort === "off") return void 0;
78175
78243
  const supported = getSupportedThinkingLevels(model);
78176
78244
  if (supported.some((level) => level === effort)) return effort;
78177
- throw new LlmError9(
78245
+ throw new LlmError10(
78178
78246
  `opencode-go model "${model.id}" does not support reasoning effort "${effort}"`,
78179
78247
  "UNSUPPORTED_REASONING_EFFORT"
78180
78248
  );
@@ -78227,7 +78295,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78227
78295
  hasImage: false,
78228
78296
  toolCount: 0,
78229
78297
  headers: {}
78230
- })?.finish(false, error instanceof LlmError9 ? error.code : void 0);
78298
+ })?.finish(false, error instanceof LlmError10 ? error.code : void 0);
78231
78299
  }
78232
78300
  async prepareCall(_provider, model, _signal) {
78233
78301
  const config = this.options.config();
@@ -78241,7 +78309,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78241
78309
  }
78242
78310
  const facts = snapshot2.facts.get(model);
78243
78311
  if (facts === void 0) {
78244
- const error = new LlmError9(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
78312
+ const error = new LlmError10(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
78245
78313
  this.logUnstartedFailure(config, model, error);
78246
78314
  throw error;
78247
78315
  }
@@ -78262,7 +78330,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78262
78330
  try {
78263
78331
  snapshot2 = await this.catalogOf(config).forModel(options.model);
78264
78332
  } catch (error) {
78265
- span?.finish(false, error instanceof LlmError9 ? error.code : void 0);
78333
+ span?.finish(false, error instanceof LlmError10 ? error.code : void 0);
78266
78334
  throw error;
78267
78335
  }
78268
78336
  yield* this.streamWithSnapshot(options, config, snapshot2, span);
@@ -78272,7 +78340,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78272
78340
  try {
78273
78341
  const span = outerSpan ?? this.startSpan(options, config);
78274
78342
  const failSpan = (error) => {
78275
- span?.finish(false, error instanceof LlmError9 ? error.code : void 0);
78343
+ span?.finish(false, error instanceof LlmError10 ? error.code : void 0);
78276
78344
  throw error;
78277
78345
  };
78278
78346
  try {
@@ -78282,7 +78350,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78282
78350
  }
78283
78351
  const facts = snapshot2.facts.get(options.model);
78284
78352
  if (facts === void 0) {
78285
- const error = new LlmError9(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
78353
+ const error = new LlmError10(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
78286
78354
  span?.finish(false, error.code);
78287
78355
  throw error;
78288
78356
  }
@@ -78290,10 +78358,22 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78290
78358
  const rates2 = usableRates(facts.cost);
78291
78359
  const stop = stopSequencesFor(model, options.stop);
78292
78360
  const ceiling = config.modelLimits[model.id]?.maxTokens;
78293
- const maxTokens = options.maxTokens === void 0 || ceiling === null || ceiling === void 0 ? options.maxTokens : Math.min(options.maxTokens, ceiling);
78361
+ const requested = options.maxTokens === void 0 || ceiling === null || ceiling === void 0 ? options.maxTokens : Math.min(options.maxTokens, ceiling);
78362
+ let maxTokens;
78363
+ try {
78364
+ maxTokens = resolveFittedMaxTokens({
78365
+ modelId: model.id,
78366
+ contextWindow: model.contextWindow,
78367
+ routeMaxTokens: model.maxTokens,
78368
+ requestedMaxTokens: requested,
78369
+ estimatedInputTokens: estimateInputTokens(options)
78370
+ });
78371
+ } catch (error) {
78372
+ failSpan(error);
78373
+ }
78294
78374
  const apiKey = await this.options.resolveApiKey();
78295
78375
  if (apiKey === void 0 || apiKey.length === 0) {
78296
- const error = new LlmError9("dshopencodego: no credential resolved for the opencode-go route", "MISSING_CREDENTIAL");
78376
+ const error = new LlmError10("dshopencodego: no credential resolved for the opencode-go route", "MISSING_CREDENTIAL");
78297
78377
  span?.finish(false, error.code);
78298
78378
  throw error;
78299
78379
  }
@@ -78323,14 +78403,14 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78323
78403
  };
78324
78404
  try {
78325
78405
  if (containsImage && !model.input.includes("image")) {
78326
- throw new LlmError9(`opencode-go model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
78406
+ throw new LlmError10(`opencode-go model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
78327
78407
  }
78328
78408
  let imageRequest;
78329
78409
  if (containsImage) {
78330
78410
  const access2 = this.options.imageAccess;
78331
78411
  const store = access2?.resolveAttachments();
78332
78412
  if (access2 === void 0 || store === void 0) {
78333
- throw new LlmError9("dshopencodego image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
78413
+ throw new LlmError10("dshopencodego image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
78334
78414
  }
78335
78415
  imageRequest = {
78336
78416
  attachments: store,
@@ -78374,7 +78454,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78374
78454
  while (true) {
78375
78455
  const result = await watchdog.next(iterator);
78376
78456
  if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
78377
- throw new LlmError9("opencode-go stream idle timeout", "TIMEOUT");
78457
+ throw new LlmError10("opencode-go stream idle timeout", "TIMEOUT");
78378
78458
  }
78379
78459
  if (result.done) {
78380
78460
  exhausted = true;
@@ -78427,13 +78507,13 @@ var OpencodeGoAdapter = class extends LlmAdapter {
78427
78507
  } catch (error) {
78428
78508
  if (timedOut || timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
78429
78509
  span?.finish(false, "TIMEOUT");
78430
- throw new LlmError9("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
78510
+ throw new LlmError10("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
78431
78511
  }
78432
78512
  if (options.signal?.aborted) {
78433
78513
  span?.finish(false, "ABORTED");
78434
- throw new LlmError9("opencode-go request aborted by caller", "ABORTED", { cause: error });
78514
+ throw new LlmError10("opencode-go request aborted by caller", "ABORTED", { cause: error });
78435
78515
  }
78436
- span?.finish(false, error instanceof LlmError9 ? error.code : void 0);
78516
+ span?.finish(false, error instanceof LlmError10 ? error.code : void 0);
78437
78517
  throw error;
78438
78518
  } finally {
78439
78519
  consumer.abort("opencode-go stream consumer stopped");
@@ -78618,6 +78698,199 @@ var networkRemote = {
78618
78698
  ]
78619
78699
  };
78620
78700
 
78701
+ // src/request-log/store.ts
78702
+ var MAX_POOL_EVENTS = 20;
78703
+ var STREAM_MARK_EVERY = 10;
78704
+ var MAX_STREAM_MARKS = 200;
78705
+ var MIN_STREAM_MS_FOR_RATE = 100;
78706
+ var RequestLog = class {
78707
+ gates;
78708
+ entries = [];
78709
+ poolEvents = [];
78710
+ seq = 0;
78711
+ totalRecords = 0;
78712
+ droppedRecords = 0;
78713
+ newConnections = 0;
78714
+ poolKey;
78715
+ constructor(gates) {
78716
+ this.gates = gates;
78717
+ }
78718
+ /**
78719
+ * Count one accepted socket. Called from the diagnostics channel, not the
78720
+ * request path, so its cost never lands on a request.
78721
+ */
78722
+ connectionOpened() {
78723
+ this.newConnections += 1;
78724
+ }
78725
+ /**
78726
+ * Record one pool lifecycle event. Pools live for minutes, so this is rare
78727
+ * by construction; still gated on debug so the common log stays request-only.
78728
+ */
78729
+ poolEvent(type, key) {
78730
+ if (!this.gates.enabled() || !this.gates.debug()) return;
78731
+ this.poolKey = type === "create" ? key : this.poolKey;
78732
+ if (type === "close" && this.poolKey === key) this.poolKey = void 0;
78733
+ this.poolEvents = [{ atMs: Date.now(), type, key }, ...this.poolEvents].slice(0, MAX_POOL_EVENTS);
78734
+ }
78735
+ /**
78736
+ * Begin one request's span, or nothing when the log is disabled.
78737
+ *
78738
+ * This is the single gate on the request path: a disabled log costs one
78739
+ * method call and one boolean read, and constructs nothing. When enabled,
78740
+ * the running row is appended before the span is returned, so the viewer
78741
+ * sees the request while it runs; `request` and `status` mutate that same
78742
+ * visible row as the wire facts arrive.
78743
+ */
78744
+ start(detail) {
78745
+ if (!this.gates.enabled()) return void 0;
78746
+ const debug = this.gates.debug();
78747
+ const startedAt = Date.now();
78748
+ const session = detail.sessionId ?? "";
78749
+ const record2 = {
78750
+ seq: ++this.seq,
78751
+ atMs: startedAt,
78752
+ model: detail.model,
78753
+ ...detail.protocol === void 0 ? {} : { protocol: detail.protocol },
78754
+ session,
78755
+ method: detail.method ?? "POST",
78756
+ url: detail.url ?? "",
78757
+ running: true,
78758
+ ok: false,
78759
+ shape: { messages: detail.messageCount, hasImage: detail.hasImage, tools: detail.toolCount },
78760
+ ...detail.imageBytes === void 0 ? {} : { imageBytes: detail.imageBytes },
78761
+ ...!debug ? {} : {
78762
+ headers: {
78763
+ ...detail.headers.session === void 0 ? {} : { session: detail.headers.session },
78764
+ ...detail.headers.userAgent === void 0 ? {} : { userAgent: detail.headers.userAgent }
78765
+ }
78766
+ }
78767
+ };
78768
+ this.insert(record2, true);
78769
+ let firstTokenAt;
78770
+ let lastTokenAt;
78771
+ let tokenDeltas = 0;
78772
+ const marks = [];
78773
+ let reported = false;
78774
+ let finished = false;
78775
+ return {
78776
+ identify: (protocol) => {
78777
+ if (record2.protocol === void 0) record2.protocol = protocol;
78778
+ },
78779
+ token: () => {
78780
+ const now = Date.now();
78781
+ if (firstTokenAt === void 0) {
78782
+ firstTokenAt = now;
78783
+ record2.firstTokenMs = Math.max(0, now - startedAt);
78784
+ }
78785
+ lastTokenAt = now;
78786
+ tokenDeltas += 1;
78787
+ if (tokenDeltas % STREAM_MARK_EVERY === 0 && marks.length < MAX_STREAM_MARKS) {
78788
+ marks.push(Math.max(0, now - startedAt));
78789
+ record2.streamMarksMs = marks;
78790
+ }
78791
+ },
78792
+ request: (nextMethod, nextUrl, nextRequestBytes) => {
78793
+ if (!reported) {
78794
+ reported = true;
78795
+ record2.method = nextMethod;
78796
+ record2.url = nextUrl;
78797
+ if (nextRequestBytes !== void 0) record2.requestBytes = nextRequestBytes;
78798
+ record2.prepMs = Math.max(0, Date.now() - startedAt);
78799
+ }
78800
+ },
78801
+ status: (value) => {
78802
+ record2.status = value;
78803
+ if (record2.responseMs === void 0) record2.responseMs = Math.max(0, Date.now() - startedAt);
78804
+ },
78805
+ finish: (ok, code, usage, costUsd) => {
78806
+ if (finished) return;
78807
+ finished = true;
78808
+ const seq = record2.seq;
78809
+ const endedAt = Date.now();
78810
+ const decodeMs = firstTokenAt === void 0 ? void 0 : Math.max(0, endedAt - firstTokenAt);
78811
+ const streamMs = firstTokenAt === void 0 || lastTokenAt === void 0 ? void 0 : Math.max(0, lastTokenAt - firstTokenAt);
78812
+ const tps = decodeMs === void 0 || decodeMs <= 0 || usage === void 0 || streamMs === void 0 || streamMs < MIN_STREAM_MS_FOR_RATE ? void 0 : usage.outputTokens / (decodeMs / 1e3);
78813
+ const entry = {
78814
+ seq,
78815
+ atMs: startedAt,
78816
+ model: detail.model,
78817
+ // The protocol may have arrived after the row opened, through
78818
+ // `identify` — the running row and the completion read the same
78819
+ // field, so they never disagree.
78820
+ ...record2.protocol === void 0 ? {} : { protocol: record2.protocol },
78821
+ session,
78822
+ method: record2.method,
78823
+ url: record2.url,
78824
+ ...record2.status === void 0 ? {} : { status: record2.status },
78825
+ ...record2.prepMs === void 0 ? {} : { prepMs: record2.prepMs },
78826
+ ...record2.responseMs === void 0 ? {} : { responseMs: record2.responseMs },
78827
+ ...record2.streamMarksMs === void 0 ? {} : { streamMarksMs: [...record2.streamMarksMs] },
78828
+ ...usage === void 0 ? {} : {
78829
+ inputTokens: usage.inputTokens,
78830
+ outputTokens: usage.outputTokens,
78831
+ ...usage.cacheReadTokens === void 0 || usage.cacheReadTokens <= 0 ? {} : { cacheReadTokens: usage.cacheReadTokens },
78832
+ ...usage.cacheWriteTokens === void 0 || usage.cacheWriteTokens <= 0 ? {} : { cacheWriteTokens: usage.cacheWriteTokens },
78833
+ ...usage.reasoningTokens === void 0 || usage.reasoningTokens <= 0 ? {} : { reasoningTokens: usage.reasoningTokens }
78834
+ },
78835
+ ...costUsd === void 0 ? {} : { costUsd },
78836
+ ...record2.requestBytes === void 0 ? {} : { requestBytes: record2.requestBytes },
78837
+ ...detail.imageBytes === void 0 ? {} : { imageBytes: detail.imageBytes },
78838
+ durationMs: Math.max(0, endedAt - startedAt),
78839
+ ...firstTokenAt === void 0 ? {} : { firstTokenMs: Math.max(0, firstTokenAt - startedAt) },
78840
+ ...decodeMs === void 0 ? {} : { decodeMs },
78841
+ ...streamMs === void 0 ? {} : { streamMs },
78842
+ ...tps === void 0 ? {} : { tps },
78843
+ ok,
78844
+ ...code === void 0 ? {} : { code },
78845
+ // The pool in force when the request finished. Pool switches are
78846
+ // rare enough that attributing the finishing pool is honest; the
78847
+ // create/close events carry the exact history.
78848
+ ...this.poolKey === void 0 ? {} : { poolKey: this.poolKey },
78849
+ shape: { messages: detail.messageCount, hasImage: detail.hasImage, tools: detail.toolCount },
78850
+ ...!debug ? {} : {
78851
+ headers: {
78852
+ ...detail.headers.session === void 0 ? {} : { session: detail.headers.session },
78853
+ ...detail.headers.userAgent === void 0 ? {} : { userAgent: detail.headers.userAgent }
78854
+ }
78855
+ }
78856
+ };
78857
+ const at = this.entries.findIndex((candidate) => candidate.seq === seq);
78858
+ if (at === -1) {
78859
+ this.insert(entry, false);
78860
+ } else {
78861
+ this.entries[at] = entry;
78862
+ }
78863
+ }
78864
+ };
78865
+ }
78866
+ insert(entry, count2) {
78867
+ if (count2) this.totalRecords += 1;
78868
+ const bound = Math.max(1, Math.floor(this.gates.maxEntries()));
78869
+ const next = [entry, ...this.entries].slice(0, bound);
78870
+ this.droppedRecords += this.entries.length + 1 - next.length;
78871
+ this.entries = next;
78872
+ }
78873
+ /** Shrink the ring to a new bound immediately; growing backfills nothing. */
78874
+ setMaxEntries(bound) {
78875
+ if (!Number.isSafeInteger(bound) || bound < 1) return;
78876
+ if (this.entries.length > bound) {
78877
+ this.droppedRecords += this.entries.length - bound;
78878
+ this.entries = this.entries.slice(0, bound);
78879
+ }
78880
+ }
78881
+ /** The current reading, newest first. Computed lazily: nothing on the hot path. */
78882
+ snapshot() {
78883
+ return {
78884
+ entries: this.entries,
78885
+ poolEvents: this.poolEvents,
78886
+ totalRecords: this.totalRecords,
78887
+ droppedRecords: this.droppedRecords,
78888
+ newConnections: this.newConnections,
78889
+ ...this.poolKey === void 0 ? {} : { poolKey: this.poolKey }
78890
+ };
78891
+ }
78892
+ };
78893
+
78621
78894
  // src/request-log/contract.ts
78622
78895
  function count(key, row, optional) {
78623
78896
  const entry = row[key];
@@ -78664,6 +78937,8 @@ function parseEntry(value) {
78664
78937
  ...row["costUsd"] === void 0 ? {} : { costUsd: rate("costUsd", row) },
78665
78938
  ...row["requestBytes"] === void 0 ? {} : { requestBytes: count("requestBytes", row) },
78666
78939
  ...row["prepMs"] === void 0 ? {} : { prepMs: count("prepMs", row) },
78940
+ ...row["responseMs"] === void 0 ? {} : { responseMs: count("responseMs", row) },
78941
+ ...row["streamMarksMs"] === void 0 ? {} : { streamMarksMs: parseMarks(row["streamMarksMs"]) },
78667
78942
  ...row["imageBytes"] === void 0 ? {} : { imageBytes: count("imageBytes", row) },
78668
78943
  ...row["durationMs"] === void 0 ? {} : { durationMs: count("durationMs", row) },
78669
78944
  ...row["firstTokenMs"] === void 0 ? {} : { firstTokenMs: count("firstTokenMs", row) },
@@ -78691,6 +78966,15 @@ function parseEntry(value) {
78691
78966
  } : {}
78692
78967
  };
78693
78968
  }
78969
+ function parseMarks(value) {
78970
+ if (!Array.isArray(value)) throw new Error("invalid request-log stream marks");
78971
+ const kept = [];
78972
+ for (const mark of value) {
78973
+ if (typeof mark !== "number" || !Number.isFinite(mark) || mark < 0 || kept.length >= MAX_STREAM_MARKS) continue;
78974
+ kept.push(mark);
78975
+ }
78976
+ return kept;
78977
+ }
78694
78978
  function rate(key, row) {
78695
78979
  const entry = row[key];
78696
78980
  if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 0) {
@@ -78906,7 +79190,7 @@ function registerRemotes(ctx) {
78906
79190
 
78907
79191
  // src/catalog/service.ts
78908
79192
  import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
78909
- import { LlmError as LlmError10 } from "@deepseek-ai/dsh-llm";
79193
+ import { LlmError as LlmError11 } from "@deepseek-ai/dsh-llm";
78910
79194
 
78911
79195
  // src/catalog/reading.ts
78912
79196
  function catalogReading(snapshot2, visibility, listingFailure, now) {
@@ -78987,7 +79271,7 @@ var OpencodeGoCatalogService = class extends TypertRemoteService {
78987
79271
  const reading = catalogReading(
78988
79272
  snapshot2,
78989
79273
  visibility,
78990
- failure === void 0 ? void 0 : failure instanceof LlmError10 || failure instanceof Error ? failure.message : String(failure)
79274
+ failure === void 0 ? void 0 : failure instanceof LlmError11 || failure instanceof Error ? failure.message : String(failure)
78991
79275
  );
78992
79276
  this.reportUnadvertised(snapshot2, visibility);
78993
79277
  return reading;
@@ -79213,187 +79497,6 @@ var OpencodeGoRequestLogService = class extends TypertRemoteService3 {
79213
79497
  }
79214
79498
  };
79215
79499
 
79216
- // src/request-log/store.ts
79217
- var MAX_POOL_EVENTS = 20;
79218
- var MIN_STREAM_MS_FOR_RATE = 100;
79219
- var RequestLog = class {
79220
- gates;
79221
- entries = [];
79222
- poolEvents = [];
79223
- seq = 0;
79224
- totalRecords = 0;
79225
- droppedRecords = 0;
79226
- newConnections = 0;
79227
- poolKey;
79228
- constructor(gates) {
79229
- this.gates = gates;
79230
- }
79231
- /**
79232
- * Count one accepted socket. Called from the diagnostics channel, not the
79233
- * request path, so its cost never lands on a request.
79234
- */
79235
- connectionOpened() {
79236
- this.newConnections += 1;
79237
- }
79238
- /**
79239
- * Record one pool lifecycle event. Pools live for minutes, so this is rare
79240
- * by construction; still gated on debug so the common log stays request-only.
79241
- */
79242
- poolEvent(type, key) {
79243
- if (!this.gates.enabled() || !this.gates.debug()) return;
79244
- this.poolKey = type === "create" ? key : this.poolKey;
79245
- if (type === "close" && this.poolKey === key) this.poolKey = void 0;
79246
- this.poolEvents = [{ atMs: Date.now(), type, key }, ...this.poolEvents].slice(0, MAX_POOL_EVENTS);
79247
- }
79248
- /**
79249
- * Begin one request's span, or nothing when the log is disabled.
79250
- *
79251
- * This is the single gate on the request path: a disabled log costs one
79252
- * method call and one boolean read, and constructs nothing. When enabled,
79253
- * the running row is appended before the span is returned, so the viewer
79254
- * sees the request while it runs; `request` and `status` mutate that same
79255
- * visible row as the wire facts arrive.
79256
- */
79257
- start(detail) {
79258
- if (!this.gates.enabled()) return void 0;
79259
- const debug = this.gates.debug();
79260
- const startedAt = Date.now();
79261
- const session = detail.sessionId ?? "";
79262
- const record2 = {
79263
- seq: ++this.seq,
79264
- atMs: startedAt,
79265
- model: detail.model,
79266
- ...detail.protocol === void 0 ? {} : { protocol: detail.protocol },
79267
- session,
79268
- method: detail.method ?? "POST",
79269
- url: detail.url ?? "",
79270
- running: true,
79271
- ok: false,
79272
- shape: { messages: detail.messageCount, hasImage: detail.hasImage, tools: detail.toolCount },
79273
- ...detail.imageBytes === void 0 ? {} : { imageBytes: detail.imageBytes },
79274
- ...!debug ? {} : {
79275
- headers: {
79276
- ...detail.headers.session === void 0 ? {} : { session: detail.headers.session },
79277
- ...detail.headers.userAgent === void 0 ? {} : { userAgent: detail.headers.userAgent }
79278
- }
79279
- }
79280
- };
79281
- this.insert(record2, true);
79282
- let firstTokenAt;
79283
- let lastTokenAt;
79284
- let reported = false;
79285
- let finished = false;
79286
- return {
79287
- identify: (protocol) => {
79288
- if (record2.protocol === void 0) record2.protocol = protocol;
79289
- },
79290
- token: () => {
79291
- const now = Date.now();
79292
- if (firstTokenAt === void 0) {
79293
- firstTokenAt = now;
79294
- record2.firstTokenMs = Math.max(0, now - startedAt);
79295
- }
79296
- lastTokenAt = now;
79297
- },
79298
- request: (nextMethod, nextUrl, nextRequestBytes) => {
79299
- if (!reported) {
79300
- reported = true;
79301
- record2.method = nextMethod;
79302
- record2.url = nextUrl;
79303
- if (nextRequestBytes !== void 0) record2.requestBytes = nextRequestBytes;
79304
- record2.prepMs = Math.max(0, Date.now() - startedAt);
79305
- }
79306
- },
79307
- status: (value) => {
79308
- record2.status = value;
79309
- },
79310
- finish: (ok, code, usage, costUsd) => {
79311
- if (finished) return;
79312
- finished = true;
79313
- const seq = record2.seq;
79314
- const endedAt = Date.now();
79315
- const decodeMs = firstTokenAt === void 0 ? void 0 : Math.max(0, endedAt - firstTokenAt);
79316
- const streamMs = firstTokenAt === void 0 || lastTokenAt === void 0 ? void 0 : Math.max(0, lastTokenAt - firstTokenAt);
79317
- const tps = decodeMs === void 0 || decodeMs <= 0 || usage === void 0 || streamMs === void 0 || streamMs < MIN_STREAM_MS_FOR_RATE ? void 0 : usage.outputTokens / (decodeMs / 1e3);
79318
- const entry = {
79319
- seq,
79320
- atMs: startedAt,
79321
- model: detail.model,
79322
- // The protocol may have arrived after the row opened, through
79323
- // `identify` — the running row and the completion read the same
79324
- // field, so they never disagree.
79325
- ...record2.protocol === void 0 ? {} : { protocol: record2.protocol },
79326
- session,
79327
- method: record2.method,
79328
- url: record2.url,
79329
- ...record2.status === void 0 ? {} : { status: record2.status },
79330
- ...record2.prepMs === void 0 ? {} : { prepMs: record2.prepMs },
79331
- ...usage === void 0 ? {} : {
79332
- inputTokens: usage.inputTokens,
79333
- outputTokens: usage.outputTokens,
79334
- ...usage.cacheReadTokens === void 0 || usage.cacheReadTokens <= 0 ? {} : { cacheReadTokens: usage.cacheReadTokens },
79335
- ...usage.cacheWriteTokens === void 0 || usage.cacheWriteTokens <= 0 ? {} : { cacheWriteTokens: usage.cacheWriteTokens },
79336
- ...usage.reasoningTokens === void 0 || usage.reasoningTokens <= 0 ? {} : { reasoningTokens: usage.reasoningTokens }
79337
- },
79338
- ...costUsd === void 0 ? {} : { costUsd },
79339
- ...record2.requestBytes === void 0 ? {} : { requestBytes: record2.requestBytes },
79340
- ...detail.imageBytes === void 0 ? {} : { imageBytes: detail.imageBytes },
79341
- durationMs: Math.max(0, endedAt - startedAt),
79342
- ...firstTokenAt === void 0 ? {} : { firstTokenMs: Math.max(0, firstTokenAt - startedAt) },
79343
- ...decodeMs === void 0 ? {} : { decodeMs },
79344
- ...streamMs === void 0 ? {} : { streamMs },
79345
- ...tps === void 0 ? {} : { tps },
79346
- ok,
79347
- ...code === void 0 ? {} : { code },
79348
- // The pool in force when the request finished. Pool switches are
79349
- // rare enough that attributing the finishing pool is honest; the
79350
- // create/close events carry the exact history.
79351
- ...this.poolKey === void 0 ? {} : { poolKey: this.poolKey },
79352
- shape: { messages: detail.messageCount, hasImage: detail.hasImage, tools: detail.toolCount },
79353
- ...!debug ? {} : {
79354
- headers: {
79355
- ...detail.headers.session === void 0 ? {} : { session: detail.headers.session },
79356
- ...detail.headers.userAgent === void 0 ? {} : { userAgent: detail.headers.userAgent }
79357
- }
79358
- }
79359
- };
79360
- const at = this.entries.findIndex((candidate) => candidate.seq === seq);
79361
- if (at === -1) {
79362
- this.insert(entry, false);
79363
- } else {
79364
- this.entries[at] = entry;
79365
- }
79366
- }
79367
- };
79368
- }
79369
- insert(entry, count2) {
79370
- if (count2) this.totalRecords += 1;
79371
- const bound = Math.max(1, Math.floor(this.gates.maxEntries()));
79372
- const next = [entry, ...this.entries].slice(0, bound);
79373
- this.droppedRecords += this.entries.length + 1 - next.length;
79374
- this.entries = next;
79375
- }
79376
- /** Shrink the ring to a new bound immediately; growing backfills nothing. */
79377
- setMaxEntries(bound) {
79378
- if (!Number.isSafeInteger(bound) || bound < 1) return;
79379
- if (this.entries.length > bound) {
79380
- this.droppedRecords += this.entries.length - bound;
79381
- this.entries = this.entries.slice(0, bound);
79382
- }
79383
- }
79384
- /** The current reading, newest first. Computed lazily: nothing on the hot path. */
79385
- snapshot() {
79386
- return {
79387
- entries: this.entries,
79388
- poolEvents: this.poolEvents,
79389
- totalRecords: this.totalRecords,
79390
- droppedRecords: this.droppedRecords,
79391
- newConnections: this.newConnections,
79392
- ...this.poolKey === void 0 ? {} : { poolKey: this.poolKey }
79393
- };
79394
- }
79395
- };
79396
-
79397
79500
  // src/update/check.ts
79398
79501
  var PACKAGE_NAME = "@dan-ai-studio/dshopencodego";
79399
79502
  var REGISTRY_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
@@ -79487,7 +79590,7 @@ function createUpdateChecker(deps) {
79487
79590
  }
79488
79591
 
79489
79592
  // src/update/version.ts
79490
- var PLUGIN_VERSION = true ? "0.1.84" : "0.0.0-dev";
79593
+ var PLUGIN_VERSION = true ? "0.1.86" : "0.0.0-dev";
79491
79594
 
79492
79595
  // src/update/service.ts
79493
79596
  import { TypertRemoteService as TypertRemoteService4 } from "@deepseek-ai/dsh-typert-protocol";
@@ -79516,7 +79619,7 @@ var OpencodeGoUpdateService = class extends TypertRemoteService4 {
79516
79619
  };
79517
79620
 
79518
79621
  // src/usage/windows.ts
79519
- import { attributionHeaders as attributionHeaders6, LlmError as LlmError11 } from "@deepseek-ai/dsh-llm";
79622
+ import { attributionHeaders as attributionHeaders6, LlmError as LlmError12 } from "@deepseek-ai/dsh-llm";
79520
79623
  var USAGE_MAX_BYTES = 1024 * 1024;
79521
79624
  var USAGE_TIMEOUT_MS = 1e4;
79522
79625
  function parseGoUsage(value) {
@@ -79534,7 +79637,7 @@ function parseGoUsage(value) {
79534
79637
  }
79535
79638
  async function readUsageWindows(options) {
79536
79639
  if (options.apiKey === void 0 || options.apiKey.length === 0) {
79537
- throw new LlmError11("dshopencodego: no credential is configured for the opencode-go route", "USAGE_UNAVAILABLE");
79640
+ throw new LlmError12("dshopencodego: no credential is configured for the opencode-go route", "USAGE_UNAVAILABLE");
79538
79641
  }
79539
79642
  const url = `${options.baseURL.replace(/\/+$/, "")}/usage`;
79540
79643
  const timeout = AbortSignal.timeout(USAGE_TIMEOUT_MS);
@@ -79551,12 +79654,12 @@ async function readUsageWindows(options) {
79551
79654
  signal: options.signal === void 0 ? timeout : AbortSignal.any([options.signal, timeout])
79552
79655
  });
79553
79656
  } catch (error) {
79554
- throw new LlmError11(`could not reach ${url}`, "USAGE_UNAVAILABLE", { cause: error });
79657
+ throw new LlmError12(`could not reach ${url}`, "USAGE_UNAVAILABLE", { cause: error });
79555
79658
  }
79556
79659
  if (!response.ok) {
79557
79660
  await response.body?.cancel().catch(() => {
79558
79661
  });
79559
- throw new LlmError11(`${url} answered HTTP ${response.status}`, "USAGE_UNAVAILABLE");
79662
+ throw new LlmError12(`${url} answered HTTP ${response.status}`, "USAGE_UNAVAILABLE");
79560
79663
  }
79561
79664
  const body = await readBoundedJson(response, url, USAGE_MAX_BYTES);
79562
79665
  try {
@@ -79565,16 +79668,16 @@ async function readUsageWindows(options) {
79565
79668
  ...options.source === void 0 ? {} : { source: options.source }
79566
79669
  };
79567
79670
  } catch (error) {
79568
- throw new LlmError11(`${url} returned an invalid usage document`, "USAGE_UNAVAILABLE", { cause: error });
79671
+ throw new LlmError12(`${url} returned an invalid usage document`, "USAGE_UNAVAILABLE", { cause: error });
79569
79672
  }
79570
79673
  }
79571
79674
 
79572
79675
  // src/usage/service.ts
79573
79676
  import { randomUUID as randomUUID3 } from "node:crypto";
79574
79677
  import { RemoteError as RemoteError2, TypertRemoteService as TypertRemoteService5 } from "@deepseek-ai/dsh-typert-protocol";
79575
- import { LlmError as LlmError12 } from "@deepseek-ai/dsh-llm";
79678
+ import { LlmError as LlmError13 } from "@deepseek-ai/dsh-llm";
79576
79679
  function usageFailure(error, source) {
79577
- const missing = error instanceof LlmError12 && error.code === "MISSING_CREDENTIAL";
79680
+ const missing = error instanceof LlmError13 && error.code === "MISSING_CREDENTIAL";
79578
79681
  return new RemoteError2(
79579
79682
  "dshopencodego/usage-unavailable",
79580
79683
  error instanceof Error ? error.message : "OpenCode Go usage is unavailable",
@@ -79601,7 +79704,7 @@ var OpencodeGoUsageService = class extends TypertRemoteService5 {
79601
79704
  }
79602
79705
  if (key === void 0 || key.length === 0) {
79603
79706
  this.identity = void 0;
79604
- throw usageFailure(new LlmError12("No OpenCode Go API key is configured", "MISSING_CREDENTIAL"), void 0);
79707
+ throw usageFailure(new LlmError13("No OpenCode Go API key is configured", "MISSING_CREDENTIAL"), void 0);
79605
79708
  }
79606
79709
  if (this.identity?.baseURL !== baseURL || this.identity.key !== key) {
79607
79710
  this.identity = { baseURL, key, source: randomUUID3() };
@@ -79650,7 +79753,7 @@ function apply(ctx, raw) {
79650
79753
  const credentials = ctx.get("credentials");
79651
79754
  const hit = credentials !== void 0 ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
79652
79755
  if (hit !== void 0 && hit.length > 0) return llm2.assertUsableApiKey(hit, name, ref);
79653
- throw new LlmError13(
79756
+ throw new LlmError14(
79654
79757
  `dshopencodego: no credential; the profile resolves ${ref}, which is not set \u2014 store ${ref} through the credentials service (the Web Models page writes it) or export it`,
79655
79758
  "MISSING_CREDENTIAL"
79656
79759
  );
@@ -79796,7 +79899,7 @@ function apply(ctx, raw) {
79796
79899
  settingsNs,
79797
79900
  async (request, signal) => {
79798
79901
  if (request.provider !== PROVIDER_ID && !(request.baseURL ?? "").includes("opencode.ai")) {
79799
- throw new LlmError13(
79902
+ throw new LlmError14(
79800
79903
  "dshopencodego discovers only OpenCode Go/Console endpoints; enter this provider's models by hand",
79801
79904
  "DISCOVERY_UNSUPPORTED"
79802
79905
  );
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 发送前按上下文窗口装配一次请求。
3
+ *
4
+ * 宿主拥有压缩会话的职责,插件拥有不上报谎言的职责:网关按
5
+ * `input + max_tokens <= contextWindow` 做严格校验(`muse-spark`
6
+ * 系 `1048576 / 131072` 即为一例),而宿主在 `input` 已占 `87%`
7
+ * 时仍可能按满额输出请求。本模块在发网关之前做两件事:
8
+ *
9
+ * - 输入本身已超窗:直接抛 `CONTEXT_WINDOW_EXCEEDED`,让宿主走压缩/
10
+ * 重试分支,而不是等网关 `400 context_length_exceeded` 再失败;
11
+ * - 输入加请求输出会超窗:把本次 `maxTokens` 收缩到剩余窗口内。
12
+ * 收缩是保底成功,不是替代压缩:历史照旧原样发送,不截断、不总结。
13
+ *
14
+ * 估算是有意保守的上界:网关分词器未知,按 `3 字符 ≈ 1 token`
15
+ * 加固定开销估算,多估只会多收一点输出,不会把能过的请求误杀。
16
+ * 图片/文件按固定开销加字节折算,视觉 token 的真实计费未知时宁可高估。
17
+ *
18
+ * @module @dan-ai-studio/dshopencodego/context-fit
19
+ */
20
+ import type { GenerateOptions } from '@deepseek-ai/dsh-llm';
21
+ /** 为分词器漂移和线上传输开销预留的窗口。 */
22
+ export declare const CONTEXT_FIT_RESERVE_TOKENS = 2048;
23
+ /** 收缩后仍值得发一次请求的最小输出。低于它就直接失败,逼宿主压缩。 */
24
+ export declare const CONTEXT_FIT_MIN_OUTPUT_TOKENS = 256;
25
+ /** 估算所需的请求子集:只读文本与附件元数据,不读字节。 */
26
+ export interface FittableRequest {
27
+ readonly messages: GenerateOptions['messages'];
28
+ readonly system?: GenerateOptions['system'];
29
+ readonly tools?: GenerateOptions['tools'];
30
+ }
31
+ /**
32
+ * 保守估算一次请求的输入 token 数(上界,非精确计数)。
33
+ * @param options - 本次请求的消息、系统提示与工具声明。
34
+ * @returns 估算的输入 token 数,至少为 0 的有限整数。
35
+ */
36
+ export declare function estimateInputTokens(options: FittableRequest): number;
37
+ /** `resolveFittedMaxTokens` 的输入。 */
38
+ export interface FitRequest {
39
+ /** 展示给宿主看的模型 id,只用于报错文本。 */
40
+ readonly modelId: string;
41
+ /** 该模型的上下文窗口(含本次已应用的覆盖)。 */
42
+ readonly contextWindow: number;
43
+ /** 路由自身的输出上限(含覆盖),未知时传 undefined。 */
44
+ readonly routeMaxTokens?: number | undefined;
45
+ /** 调用方本次请求的输出上限,未指定时传 undefined。 */
46
+ readonly requestedMaxTokens?: number | undefined;
47
+ /** `estimateInputTokens` 的输出。 */
48
+ readonly estimatedInputTokens: number;
49
+ }
50
+ /**
51
+ * 按剩余窗口装配本次的 `maxTokens`。
52
+ * @param fit - 窗口、路由上限、调用方请求与输入估算。
53
+ * @returns 发往网关的 `maxTokens`:不需要收缩时与调用方请求一致
54
+ * (调用方未指定则保持 `undefined`,不断言网关默认值)。
55
+ * @throws {LlmError} `CONTEXT_WINDOW_EXCEEDED`:输入本身已超窗,或剩余
56
+ * 窗口连最小输出都放不下。调用方应据此走压缩/截断分支。
57
+ */
58
+ export declare function resolveFittedMaxTokens(fit: FitRequest): number | undefined;
@@ -91,6 +91,20 @@ export interface RequestLogEntry {
91
91
  * reached the wire (rejected up front).
92
92
  */
93
93
  readonly prepMs?: number;
94
+ /**
95
+ * Wall time from request start to the response headers' arrival, in
96
+ * milliseconds. Splits the first-token wait in two: up to here the
97
+ * gateway is queuing, after here the model is thinking. Absent when the
98
+ * response never arrived.
99
+ */
100
+ readonly responseMs?: number;
101
+ /**
102
+ * Elapsed milliseconds of every tenth accepted token delta, capped.
103
+ * Cuts the streaming phase into visible blocks, so a stall mid-output
104
+ * reads as a gap rather than a uniformly slow bar. Absent when fewer
105
+ * than ten deltas arrived — then the output slice needs no subdivision.
106
+ */
107
+ readonly streamMarksMs?: readonly number[];
94
108
  /** Wall time from request start to terminal event, in milliseconds. */
95
109
  readonly durationMs?: number;
96
110
  /** Wall time from request start to the first token delta, in milliseconds. */
@@ -177,6 +191,13 @@ export interface RequestLogGates {
177
191
  /** How many entries to keep; the ring trims to it on every append. */
178
192
  readonly maxEntries: () => number;
179
193
  }
194
+ /**
195
+ * How many streaming timestamps one row keeps at most. Past the cap the
196
+ * trail simply stops growing: the newest marks matter more than the oldest
197
+ * for reading a stall, and the oldest are still bracketed by the first
198
+ * token and the last one.
199
+ */
200
+ export declare const MAX_STREAM_MARKS = 200;
180
201
  /**
181
202
  * Minimum content span for a meaningful rate, in milliseconds.
182
203
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dan-ai-studio/dshopencodego",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "description": "OpenCode Go provider for DeepSeek Harness: live gateway catalog, per-conversation session header, usage",
5
5
  "type": "module",
6
6
  "license": "MIT",