@dan-ai-studio/dshopencodego 0.1.70 → 0.1.73

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
@@ -101,7 +101,6 @@ Configuration lives in the profile's `cordis.patch.yml`:
101
101
  # the timer restarts on every answer, so a connection in
102
102
  # frequent use is never dropped; a long pause in a
103
103
  # conversation costs one handshake (about 200ms)
104
- requestGzip: true # gzip JSON request bodies of 1 KB or more; turn off if the gateway rejects them
105
104
  maxRequestImageBytes: 6291456 # whole-request image payload cap (after base64, bytes)
106
105
  requestImagePixelBudget: 1440000 # total-pixel budget for one image
107
106
  requestImageMaxBytes: 400000 # encoded-byte target for one image (before base64)
@@ -142,8 +141,6 @@ The API key comes from the Harness credential store (reference name `OPENCODE_GO
142
141
  - The settings page shows and accepts the value in **seconds** (a number of milliseconds is not readable at a glance) and stores it in milliseconds. Range: 1 to 1800000 milliseconds.
143
142
  - Once a connection has been idle that long, the pool **reclaims it** on its own — no separate mechanism is needed. The actual release comes a couple of seconds later than the setting, which is the client's own small margin beyond the deadline the server states. When the plugin is unloaded, the pool is **closed with it**: the pool belongs to the plugin and should not be left behind on the machine.
144
143
 
145
- **Request body gzip**: `requestGzip` is on by default; JSON bodies of 1 KB or more are gzipped (fastest level, off the event loop) with a `Content-Encoding: gzip` header, smaller bodies travel plain. The log's body size is the **actual wire bytes** — compare the same large request with the switch on and off to see what it saved. **If the gateway does not accept gzip it fails requests outright; turn the switch off** (one setting, no other coupling).
146
-
147
144
  **Request log**: per-request records, on by default; turning them off returns the request path to exactly as without the log. The settings page's "Request log" group holds the master switch, the debug switch (pool events and the request user agent, default off), and the retention count (default 200).
148
145
 
149
146
  - **Two entries, one page**:
@@ -154,6 +151,7 @@ The API key comes from the Harness credential store (reference name `OPENCODE_GO
154
151
  - The session column names the **session title** (from the session catalog; falls back to the id, whose full spelling stays one hover away). The session filter matches titles and ids; session groups are headed the same way. **Clicking a title opens that session** (an archived one refuses, and says so).
155
152
  - **Cache hit** is the share of input tokens served from cache (cache reads over uncached input plus cache reads). No reported input means no share — none is invented either.
156
153
  - **The In column is uncached new input**: the gateway reports a prompt as "uncached plus cache hits" and the log records only the new part — a fully prefix-cached turn reads as a tiny input with a 100% hit, which is not a bug. Hover a column head for its definition.
154
+ - **Content split**: below the body size the detail shows **Text X / Images Y** — images estimated as base64 per deduped attachment (each capped at the route's encoded target), text as the residual, so the two always sum to the total. Image-dominated turns read at a glance.
157
155
  - Session ids are **always stored whole**: an id is a conversation identity, not a credential, and a truncated one reads two conversations as one.
158
156
  - The log is a ring buffer (oldest dropped first) that dies with the process; snapshots are read on demand, never polled, so a page nobody opens costs no traffic.
159
157
  - The credential never enters a record: the recorder takes no headers argument at all, only the allowlisted session id and user agent. Failed requests are recorded with their error code; a request cut off before any terminal event is not recorded — no total reported, none invented.
package/README.md CHANGED
@@ -100,7 +100,6 @@ DSH 桌面端是 Electron 壳 + 完整的 dsh Web 应用,本插件对它同样
100
100
  keepAliveTimeoutMs: 600000 # 连接空闲保留:两次请求之间连接留多久(毫秒,1 ~ 1800000)
101
101
  # 每次收到回答后重新计时,所以常用时连接不会被断开;
102
102
  # 对话中每次停顿较久都要重新握手一次(约 200ms)
103
- requestGzip: true # 1KB 以上的 JSON 请求体先 gzip 再发;网关如果拒绝就关掉它
104
103
  maxRequestImageBytes: 6291456 # 整个请求的图片载荷上限(base64 之后,字节)
105
104
  requestImagePixelBudget: 1440000 # 单张图的总像素预算
106
105
  requestImageMaxBytes: 400000 # 单张图编码前的字节目标(base64 之前)
@@ -136,8 +135,6 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
136
135
 
137
136
  **连接保留**:`keepAliveTimeoutMs` 决定**与网关的连接在两次请求之间保留多久**。计时器**每次收到回答后重新开始**,所以常用时连接不会被断开,它约束的只是两次请求之间的静默。默认 10 分钟。
138
137
 
139
- **请求体压缩**:`requestGzip` 默认开启,1KB 以上的 JSON 请求先 gzip(最快档,异步压,不堵事件循环)再发,`Content-Encoding: gzip` 随行;1KB 以下原样走(压了也省不出什么)。日志里"请求体"记的是** wire 上的实际字节**——同一条大请求,开关两边对比一下就知道压了多少。**网关如果不吃 gzip 会直接 4xx,那时关掉这个开关就行**(一处配置,没有别的连带)。
140
-
141
138
  - 背后的原因很实际:连接一旦被断开,下一次请求要重新做一次 TCP 与 TLS 握手,**到网关这一侧约 200 毫秒**,而这笔开销正好落在**下一轮的第一个 token 上**。对话里的停顿(读答案、思考、再打字)通常在 5 ~ 30 秒,正好跨过 HTTP 客户端默认的 4 秒,于是**几乎每一轮都在白付一次握手**。
142
139
  - 这条只影响**固定了地址**(`pinnedAddresses`)的部署:没有固定地址时走的是系统自己的 fetch,本插件不加任何东西。
143
140
  - 设置页里以**秒**显示与填写(毫秒的数字人读不出量级),存储时换算成毫秒。取值 1 ~ 1800000 毫秒。
@@ -153,6 +150,7 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
153
150
  - 会话列显示**会话标题**(标题来自会话目录;拿不到时回退显示 id,悬停可见完整 id)。会话筛选同时匹配标题与 id;按会话分组的组头同样显示标题。**点标题直接打开那个会话**(归档的打不开,会明说)。
154
151
  - **缓存命中**是输入 token 里走缓存的占比(缓存读取/(未缓存输入+缓存读取))。网关没报输入就没有占比——同样不编数。
155
152
  - **输入列是未缓存的新增 token**:网关把整段 prompt 拆成"未缓存+缓存命中"两部分上报,输入列只记新增的——同一会话前缀缓存全中时输入很小、缓存命中 100%,不是 bug。悬停列头有口径说明。
153
+ - **内容拆分**:明细里"请求体"下面跟一行"文本 X /图片 Y"——图片是各去重后附件按 base64 估的占比(单张按路由的编码上限封顶),文本是总数减图片的剩余,两者恒等于请求体总数。图片 dominant 的请求一眼就能看出来。
156
154
  - 会话 id **始终完整记录**(它是会话身份,不是凭据;截断后两个会话会读成一个)。
157
155
  - 记录的是环形缓冲(超限丢最旧),进程重启即清;只读快照,不轮询,没人打开的页面零流量。
158
156
  - 凭据永远不进记录:记录器根本不接收请求头参数,只收白名单里的会话 id 与 UA;失败的请求同样记一条(含错误码),中断到没有终端事件的不记(网关没报总数就不编数)。
package/lib/client.js CHANGED
@@ -252,6 +252,7 @@ function parseEntry(value) {
252
252
  ...row["reasoningTokens"] === void 0 ? {} : { reasoningTokens: count("reasoningTokens", row) },
253
253
  ...row["costUsd"] === void 0 ? {} : { costUsd: rate("costUsd", row) },
254
254
  ...row["requestBytes"] === void 0 ? {} : { requestBytes: count("requestBytes", row) },
255
+ ...row["imageBytes"] === void 0 ? {} : { imageBytes: count("imageBytes", row) },
255
256
  ...row["durationMs"] === void 0 ? {} : { durationMs: count("durationMs", row) },
256
257
  ...row["firstTokenMs"] === void 0 ? {} : { firstTokenMs: count("firstTokenMs", row) },
257
258
  ...row["decodeMs"] === void 0 ? {} : { decodeMs: count("decodeMs", row) },
@@ -506,8 +507,6 @@ var en = {
506
507
  refreshHint: "How long requests and the model picker reuse one listing.",
507
508
  keepAliveLabel: "Connection kept idle (seconds)",
508
509
  keepAliveHint: "How long a connection to the gateway is held between requests. Longer pauses in a conversation cost one handshake each; the timer restarts on every answer.",
509
- requestGzipLabel: "Gzip request bodies",
510
- requestGzipHint: "Compress JSON bodies of 1 KB or more before sending. Turn off if the gateway rejects requests.",
511
510
  requestLogTitle: "Request log",
512
511
  requestLogHint: "Per-request records for troubleshooting. On by default; turning it off returns the request path to exactly as without the log.",
513
512
  requestLogPageHint: "One row per request. Click a column head to sort; the table scrolls and the filters stay put.",
@@ -566,6 +565,9 @@ var en = {
566
565
  requestLogDetailProtocol: "Protocol",
567
566
  requestLogDetailUrl: "URL",
568
567
  requestLogDetailRequestBytes: "Request body",
568
+ requestLogDetailContentSplit: "Content split",
569
+ requestLogDetailText: "Text",
570
+ requestLogDetailImages: "Images",
569
571
  requestLogDetailSession: "Session",
570
572
  requestLogDetailPool: "Pool",
571
573
  requestLogDetailError: "Error",
@@ -698,8 +700,6 @@ var zh = {
698
700
  refreshHint: "\u8BF7\u6C42\u4E0E\u6A21\u578B\u9009\u62E9\u5668\u590D\u7528\u540C\u4E00\u4EFD\u5217\u8868\u7684\u65F6\u957F\u3002",
699
701
  keepAliveLabel: "\u8FDE\u63A5\u7A7A\u95F2\u4FDD\u7559\uFF08\u79D2\uFF09",
700
702
  keepAliveHint: "\u4E0E\u7F51\u5173\u7684\u8FDE\u63A5\u5728\u4E24\u6B21\u8BF7\u6C42\u4E4B\u95F4\u4FDD\u7559\u591A\u4E45\u3002\u5BF9\u8BDD\u4E2D\u6BCF\u6B21\u505C\u987F\u8F83\u4E45\u90FD\u8981\u91CD\u65B0\u63E1\u624B\u4E00\u6B21\uFF1B\u6BCF\u6B21\u6536\u5230\u56DE\u7B54\u540E\u8BA1\u65F6\u90FD\u4F1A\u91CD\u65B0\u5F00\u59CB\u3002",
701
- requestGzipLabel: "\u8BF7\u6C42\u4F53 gzip \u538B\u7F29",
702
- requestGzipHint: "1KB \u4EE5\u4E0A\u7684 JSON \u8BF7\u6C42\u4F53\u5148\u538B\u7F29\u518D\u53D1\uFF1B\u7F51\u5173\u5982\u679C\u62D2\u7EDD\u8BF7\u6C42\u5C31\u5173\u6389\u5B83\u3002",
703
703
  requestLogTitle: "\u8BF7\u6C42\u65E5\u5FD7",
704
704
  requestLogHint: "\u6392\u67E5\u7528\u7684\u9010\u8BF7\u6C42\u8BB0\u5F55\u3002\u9ED8\u8BA4\u5F00\u542F\uFF1B\u5173\u95ED\u540E\u8BF7\u6C42\u8DEF\u5F84\u4E0E\u6CA1\u6709\u8BE5\u65E5\u5FD7\u9010\u5B57\u8282\u4E00\u81F4\u3002",
705
705
  requestLogPageHint: "\u6BCF\u4E2A\u8BF7\u6C42\u4E00\u884C\u3002\u70B9\u5217\u5934\u53EF\u6392\u5E8F\uFF1B\u8868\u683C\u6EDA\u52A8\uFF0C\u7B5B\u9009\u680F\u56FA\u5B9A\u5728\u4E0A\u65B9\u3002",
@@ -758,6 +758,9 @@ var zh = {
758
758
  requestLogDetailProtocol: "\u534F\u8BAE",
759
759
  requestLogDetailUrl: "\u5730\u5740",
760
760
  requestLogDetailRequestBytes: "\u8BF7\u6C42\u4F53",
761
+ requestLogDetailContentSplit: "\u5185\u5BB9\u62C6\u5206",
762
+ requestLogDetailText: "\u6587\u672C",
763
+ requestLogDetailImages: "\u56FE\u7247",
761
764
  requestLogDetailSession: "\u4F1A\u8BDD",
762
765
  requestLogDetailPool: "\u8FDE\u63A5\u6C60",
763
766
  requestLogDetailError: "\u9519\u8BEF",
@@ -1035,13 +1038,11 @@ function settingsOf(value) {
1035
1038
  const requestLogEnabled = record["requestLogEnabled"];
1036
1039
  const requestLogDebug = record["requestLogDebug"];
1037
1040
  const requestLogMaxEntries = record["requestLogMaxEntries"];
1038
- const requestGzip = record["requestGzip"];
1039
1041
  return {
1040
1042
  modelVisibility: Object.fromEntries(entries),
1041
1043
  refreshMinutes: minutes,
1042
1044
  pinnedAddresses: Object.fromEntries(pinEntries),
1043
1045
  keepAliveTimeoutMs: typeof keepAlive === "number" ? keepAlive : DEFAULT_KEEP_ALIVE_TIMEOUT_MS,
1044
- requestGzip: typeof requestGzip === "boolean" ? requestGzip : true,
1045
1046
  requestLogEnabled: typeof requestLogEnabled === "boolean" ? requestLogEnabled : true,
1046
1047
  requestLogDebug: typeof requestLogDebug === "boolean" ? requestLogDebug : false,
1047
1048
  requestLogMaxEntries: typeof requestLogMaxEntries === "number" ? requestLogMaxEntries : DEFAULT_REQUEST_LOG_MAX_ENTRIES
@@ -1456,20 +1457,6 @@ var SectionController = class _SectionController {
1456
1457
  () => this.set({ settings: { ...current, keepAliveTimeoutMs: milliseconds } })
1457
1458
  );
1458
1459
  }
1459
- /**
1460
- * Flip request-body gzip. Off returns every request to plain JSON; it is
1461
- * the whole fallback plan if the gateway ever rejects a compressed body.
1462
- */
1463
- async setRequestGzip(enabled) {
1464
- const scope = this.services.scope;
1465
- const current = this.state.settings;
1466
- if (scope === void 0 || current === void 0 || !this.state.writable) return;
1467
- if (typeof enabled !== "boolean") return;
1468
- await this.write(
1469
- () => scope.set("requestGzip", enabled),
1470
- () => this.set({ settings: { ...current, requestGzip: enabled } })
1471
- );
1472
- }
1473
1460
  /**
1474
1461
  * Flip the request-log master switch. Off (the default) means no record is
1475
1462
  * kept and the request path reads exactly as without the log.
@@ -1911,25 +1898,6 @@ function Section({ controller, t, getLocale, openRequestLog }) {
1911
1898
  ),
1912
1899
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.hint, children: t("keepAliveHint") })
1913
1900
  ] }),
1914
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.row, children: [
1915
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: section_default.hint, children: [
1916
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1917
- "input",
1918
- {
1919
- type: "checkbox",
1920
- checked: state.settings?.requestGzip ?? true,
1921
- disabled: !editable || state.saving,
1922
- onChange: (event) => {
1923
- void act(async () => {
1924
- await controller.setRequestGzip(event.target.checked);
1925
- });
1926
- }
1927
- }
1928
- ),
1929
- ` ${t("requestGzipLabel")}`
1930
- ] }),
1931
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.hint, children: t("requestGzipHint") })
1932
- ] }),
1933
1901
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: section_default.row, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("requestLogTitle") }) }),
1934
1902
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: section_default.hint, children: t("requestLogHint") }),
1935
1903
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: section_default.row, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: section_default.hint, children: [
@@ -2179,6 +2147,11 @@ function bytes(value) {
2179
2147
  if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
2180
2148
  return `${(value / (1024 * 1024)).toFixed(2)} MB`;
2181
2149
  }
2150
+ function contentSplit(entry, text2, images) {
2151
+ const imageBytes = entry.imageBytes ?? 0;
2152
+ const textBytes = entry.requestBytes === void 0 ? void 0 : Math.max(0, entry.requestBytes - imageBytes);
2153
+ return `${text2} ${textBytes === void 0 ? "\u2014" : bytes(textBytes)} / ${images} ${bytes(imageBytes)}`;
2154
+ }
2182
2155
  function cacheRate(entry) {
2183
2156
  if (entry.inputTokens === void 0) return void 0;
2184
2157
  const cached = entry.cacheReadTokens ?? 0;
@@ -2391,6 +2364,10 @@ function RequestLogPage({ readLog, enabled, canOpenSession, openSession, t, getL
2391
2364
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: request_log_default.detailLabel, children: t("requestLogDetailRequestBytes") }),
2392
2365
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: request_log_default.detailValue, children: bytes(entry.requestBytes) })
2393
2366
  ] }),
2367
+ entry.imageBytes !== void 0 && entry.imageBytes > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: request_log_default.detailItem, children: [
2368
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: request_log_default.detailLabel, children: t("requestLogDetailContentSplit") }),
2369
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: request_log_default.detailValue, children: contentSplit(entry, t("requestLogDetailText"), t("requestLogDetailImages")) })
2370
+ ] }),
2394
2371
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: request_log_default.detailItem, children: [
2395
2372
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: request_log_default.detailLabel, children: t("requestLogDetailSession") }),
2396
2373
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
package/lib/index.js CHANGED
@@ -59979,7 +59979,7 @@ var require_mock_interceptor = __commonJS({
59979
59979
  var require_mock_client = __commonJS({
59980
59980
  "node_modules/undici/lib/mock/mock-client.js"(exports, module) {
59981
59981
  "use strict";
59982
- var { promisify: promisify2 } = __require("node:util");
59982
+ var { promisify } = __require("node:util");
59983
59983
  var Client = require_client();
59984
59984
  var { buildMockDispatch } = require_mock_utils();
59985
59985
  var {
@@ -60027,7 +60027,7 @@ var require_mock_client = __commonJS({
60027
60027
  this[kDispatches] = [];
60028
60028
  }
60029
60029
  async [kClose]() {
60030
- await promisify2(this[kOriginalClose])();
60030
+ await promisify(this[kOriginalClose])();
60031
60031
  this[kConnected] = 0;
60032
60032
  this[kMockAgent][Symbols.kClients].delete(this[kOrigin]);
60033
60033
  }
@@ -60240,7 +60240,7 @@ var require_mock_call_history = __commonJS({
60240
60240
  var require_mock_pool = __commonJS({
60241
60241
  "node_modules/undici/lib/mock/mock-pool.js"(exports, module) {
60242
60242
  "use strict";
60243
- var { promisify: promisify2 } = __require("node:util");
60243
+ var { promisify } = __require("node:util");
60244
60244
  var Pool = require_pool();
60245
60245
  var { buildMockDispatch } = require_mock_utils();
60246
60246
  var {
@@ -60288,7 +60288,7 @@ var require_mock_pool = __commonJS({
60288
60288
  this[kDispatches] = [];
60289
60289
  }
60290
60290
  async [kClose]() {
60291
- await promisify2(this[kOriginalClose])();
60291
+ await promisify(this[kOriginalClose])();
60292
60292
  this[kConnected] = 0;
60293
60293
  this[kMockAgent][Symbols.kClients].delete(this[kOrigin]);
60294
60294
  }
@@ -73563,8 +73563,6 @@ import {
73563
73563
  resolveRetryPolicy
73564
73564
  } from "@deepseek-ai/dsh-llm";
73565
73565
  import { idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
73566
- import { promisify } from "node:util";
73567
- import { constants as zlibConstants, gzip as gzipCallback } from "node:zlib";
73568
73566
 
73569
73567
  // src/catalog/index.ts
73570
73568
  init_models();
@@ -75759,10 +75757,6 @@ var fields = {
75759
75757
  maxTokens: Schema.union([Schema.const(null), Schema.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)])
75760
75758
  })])).default({}),
75761
75759
  modelProtocols: Schema.dict(Schema.string()).default({}),
75762
- // On by default, off is the whole fallback plan: if the gateway ever
75763
- // rejects a compressed body, one switch returns every request to plain
75764
- // JSON with no other change.
75765
- requestGzip: Schema.boolean().default(true),
75766
75760
  // On by default: recording costs one call and one boolean per request
75767
75761
  // plus a bounded ring, and buys a per-request trail whenever something
75768
75762
  // looks wrong. Off returns the request path to exactly as without the
@@ -76616,20 +76610,20 @@ function bodyBytes(body) {
76616
76610
  if (ArrayBuffer.isView(body)) return body.byteLength;
76617
76611
  return void 0;
76618
76612
  }
76619
- var MIN_GZIP_BODY_BYTES = 1024;
76620
- var gzipAsync = promisify(gzipCallback);
76621
- async function gzipBody(armed, input, init) {
76622
- const original = init?.body;
76623
- const originalBytes = bodyBytes(original ?? void 0);
76624
- if (!armed || typeof input !== "string" && !(input instanceof URL) || originalBytes === void 0 || originalBytes < MIN_GZIP_BODY_BYTES || typeof original !== "string" && !(original instanceof ArrayBuffer) && !ArrayBuffer.isView(original)) {
76625
- return { body: original ?? null, bytes: originalBytes, headers: void 0 };
76626
- }
76627
- const bytes = typeof original === "string" ? Buffer.from(original) : original instanceof ArrayBuffer ? Buffer.from(original) : Buffer.from(original.buffer, original.byteOffset, original.byteLength);
76628
- const compressed = await gzipAsync(bytes, { level: zlibConstants.Z_BEST_SPEED });
76629
- const headers = new Headers(init?.headers);
76630
- headers.delete("content-length");
76631
- headers.set("content-encoding", "gzip");
76632
- return { body: compressed, bytes: compressed.byteLength, headers };
76613
+ function estimateImageWireBytes(messages, capBytes) {
76614
+ const seen = /* @__PURE__ */ new Set();
76615
+ let total = 0;
76616
+ const cap = Math.max(0, capBytes);
76617
+ for (const message of messages) {
76618
+ for (const block of message.content) {
76619
+ if (block.type !== "image" || block.offloaded === true) continue;
76620
+ const id = block.attachment.attachmentId;
76621
+ if (seen.has(id)) continue;
76622
+ seen.add(id);
76623
+ total += Math.ceil(Math.min(block.attachment.bytes, cap) / 3) * 4;
76624
+ }
76625
+ }
76626
+ return seen.size === 0 ? void 0 : total;
76633
76627
  }
76634
76628
  async function* observingTerminalUsage(events, observe) {
76635
76629
  for await (const event of events) {
@@ -76801,6 +76795,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
76801
76795
  let timedOut = false;
76802
76796
  const containsImage = options.messages.some((message) => contentHasImage2(message.content));
76803
76797
  const headers = providerHeaders(options.sessionId === void 0 ? void 0 : String(options.sessionId));
76798
+ const imageBytes = containsImage ? estimateImageWireBytes(options.messages, config.requestImageMaxBytes) : void 0;
76804
76799
  const span = this.options.requestLog?.start({
76805
76800
  model: options.model,
76806
76801
  sessionId: options.sessionId === void 0 ? void 0 : String(options.sessionId),
@@ -76810,23 +76805,19 @@ var OpencodeGoAdapter = class extends LlmAdapter {
76810
76805
  messageCount: options.messages.length,
76811
76806
  hasImage: containsImage,
76812
76807
  toolCount: options.tools?.length ?? 0,
76808
+ ...imageBytes === void 0 ? {} : { imageBytes },
76813
76809
  headers: {
76814
76810
  ...options.sessionId === void 0 ? {} : { session: String(options.sessionId) },
76815
76811
  ...typeof headers["user-agent"] === "string" ? { userAgent: headers["user-agent"] } : {}
76816
76812
  }
76817
76813
  });
76818
76814
  const fetchFactory = this.options.fetch;
76819
- const requestFetch = fetchFactory === void 0 || span === void 0 && !config.requestGzip ? fetchFactory : () => {
76815
+ const requestFetch = span === void 0 || fetchFactory === void 0 ? fetchFactory : () => {
76820
76816
  const base = fetchFactory();
76821
76817
  return (async (input, init) => {
76822
- const outgoing = await gzipBody(config.requestGzip, input, init);
76823
- const response = await base(input, {
76824
- ...init,
76825
- ...outgoing.body === void 0 ? {} : { body: outgoing.body },
76826
- ...outgoing.headers === void 0 ? {} : { headers: outgoing.headers }
76827
- });
76828
- span?.request(typeof init?.method === "string" ? init.method : "POST", requestUrl(input), outgoing.bytes);
76829
- span?.status(response.status);
76818
+ const response = await base(input, init);
76819
+ span.request(typeof init?.method === "string" ? init.method : "POST", requestUrl(input), bodyBytes(init?.body));
76820
+ span.status(response.status);
76830
76821
  return response;
76831
76822
  });
76832
76823
  };
@@ -77171,6 +77162,7 @@ function parseEntry(value) {
77171
77162
  ...row["reasoningTokens"] === void 0 ? {} : { reasoningTokens: count("reasoningTokens", row) },
77172
77163
  ...row["costUsd"] === void 0 ? {} : { costUsd: rate("costUsd", row) },
77173
77164
  ...row["requestBytes"] === void 0 ? {} : { requestBytes: count("requestBytes", row) },
77165
+ ...row["imageBytes"] === void 0 ? {} : { imageBytes: count("imageBytes", row) },
77174
77166
  ...row["durationMs"] === void 0 ? {} : { durationMs: count("durationMs", row) },
77175
77167
  ...row["firstTokenMs"] === void 0 ? {} : { firstTokenMs: count("firstTokenMs", row) },
77176
77168
  ...row["decodeMs"] === void 0 ? {} : { decodeMs: count("decodeMs", row) },
@@ -77807,6 +77799,7 @@ var RequestLog = class {
77807
77799
  },
77808
77800
  ...costUsd === void 0 ? {} : { costUsd },
77809
77801
  ...requestBytes === void 0 ? {} : { requestBytes },
77802
+ ...detail.imageBytes === void 0 ? {} : { imageBytes: detail.imageBytes },
77810
77803
  durationMs: Math.max(0, endedAt - startedAt),
77811
77804
  ...firstTokenAt === void 0 ? {} : { firstTokenMs: Math.max(0, firstTokenAt - startedAt) },
77812
77805
  ...decodeMs === void 0 ? {} : { decodeMs },
@@ -77951,7 +77944,7 @@ function createUpdateChecker(deps) {
77951
77944
  }
77952
77945
 
77953
77946
  // src/update/version.ts
77954
- var PLUGIN_VERSION = true ? "0.1.70" : "0.0.0-dev";
77947
+ var PLUGIN_VERSION = true ? "0.1.73" : "0.0.0-dev";
77955
77948
 
77956
77949
  // src/update/service.ts
77957
77950
  import { TypertRemoteService as TypertRemoteService4 } from "@deepseek-ai/dsh-typert-protocol";
@@ -20,6 +20,25 @@ import type { GoDoc } from './catalog/go-doc.ts';
20
20
  import type { OpencodeGoConfig } from './config.ts';
21
21
  import type { RequestRecorder } from './request-log/store.ts';
22
22
  import type { MeteredUsage } from './usage/meter.ts';
23
+ /**
24
+ * Estimate the image share of one request's wire bytes, from the messages
25
+ * before conversion.
26
+ *
27
+ * Every attached image travels as base64 (or raw bytes on the one protocol
28
+ * that sends them so), so the estimate uses the base64 size — the larger
29
+ * of the two — and can only understate the text residual, never inflate
30
+ * the parts past the whole. Repeated occurrences of one attachment travel
31
+ * once: the conversion dedupes by attachment id, and so does this walk.
32
+ * An offloaded placeholder travels as text, not bytes, and is skipped the
33
+ * same way. Each image is capped at the route's encoded-byte target,
34
+ * mirroring the preparation the conversion applies. Absent when the
35
+ * request carries no image bytes at all, so the common text-only request
36
+ * records nothing extra.
37
+ * @param messages - the request's messages, exactly as the caller passed them.
38
+ * @param capBytes - per-image encoded-byte target, before base64 expansion.
39
+ * @returns estimated image wire bytes, or undefined when there are none.
40
+ */
41
+ export declare function estimateImageWireBytes(messages: GenerateOptions['messages'], capBytes: number): number | undefined;
23
42
  /** Image machinery the adapter reads once per request. */
24
43
  export interface OpencodeGoImageAccess {
25
44
  /** Resolve the durable attachment service at request time. */
@@ -75,14 +75,6 @@ export interface OpencodeGoConfig {
75
75
  * The escape hatch for a model whose family rule guessed wrong.
76
76
  */
77
77
  modelProtocols: Record<string, string>;
78
- /**
79
- * Whether JSON request bodies of 1 KB or more are gzip-compressed before
80
- * sending. On by default: the gateway is expected to accept
81
- * `Content-Encoding: gzip`, and small bodies are never worth the overhead.
82
- * If the gateway ever rejects compressed bodies, turn this off — the
83
- * off switch is the whole fallback plan.
84
- */
85
- requestGzip: boolean;
86
78
  /**
87
79
  * Whether the request log records anything at all. On by default: one
88
80
  * call and one boolean per request, plus a bounded ring. Off returns the
@@ -66,6 +66,14 @@ export interface RequestLogEntry {
66
66
  * sensitive: a multi-megabyte turn uploads slowly no matter the model.
67
67
  */
68
68
  readonly requestBytes?: number;
69
+ /**
70
+ * Estimated image share of the wire body, in bytes: unique attached
71
+ * images as base64, each capped at the route's encoded-byte target.
72
+ * The text share is the residual (`requestBytes` minus this), so the two
73
+ * always reconcile with the total. Absent when the request carries no
74
+ * image bytes at all.
75
+ */
76
+ readonly imageBytes?: number;
69
77
  /**
70
78
  * Wall time from the first to the last content token, in milliseconds —
71
79
  * the window the output actually streamed in. Absent when fewer than two
@@ -206,6 +214,8 @@ export interface RequestStart {
206
214
  readonly messageCount: number;
207
215
  readonly hasImage: boolean;
208
216
  readonly toolCount: number;
217
+ /** Estimated image share of the wire body, in bytes; see the entry field. */
218
+ readonly imageBytes?: number;
209
219
  /** Allowlisted request headers; the recorder never sees the credential. */
210
220
  readonly headers: {
211
221
  readonly session?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dan-ai-studio/dshopencodego",
3
- "version": "0.1.70",
3
+ "version": "0.1.73",
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",