@dan-ai-studio/dshopencodego 0.1.82 → 0.1.83

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
@@ -152,7 +152,7 @@ The API key comes from the Harness credential store (reference name `OPENCODE_GO
152
152
  - **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.
153
153
  - Session ids are **always stored whole**: an id is a conversation identity, not a credential, and a truncated one reads two conversations as one.
154
154
  - The log is a ring buffer (oldest dropped first) that dies with the process; snapshots are read on open and on manual refresh, plus once a second while any request is running, so a page nobody opens costs no traffic.
155
- - The credential never enters a record: the recorder takes no headers argument at all, only the allowlisted session id and user agent. A request gets a "Requesting" placeholder row the moment it starts (the status column counts its seconds) and the row is completed in place when it ends. Failed requests are recorded with their error code; a stream the consumer abandoned early is recorded as `ABORTED` instead of vanishing silently.
155
+ - The credential never enters a record: the recorder takes no headers argument at all, only the allowlisted session id and user agent. A request gets a "Requesting" placeholder row the moment it starts (the status column counts its seconds) and the row is completed in place when it ends. Failed requests are recorded with their error code — including failures before the first byte (unresolvable models, missing credentials), whose rows carry the reason but no usage. A stream the consumer abandoned early is recorded as `ABORTED` instead of vanishing silently.
156
156
 
157
157
  **Images**: an over-budget image is refused explicitly rather than silently downscaled or dropped. The budgets ride one preset `imageBudgetPreset` (three choices, default `dsh`), not three numbers:
158
158
 
package/README.md CHANGED
@@ -151,7 +151,7 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
151
151
  - **内容拆分**:明细里"请求体"下面跟一行"文本 X /图片 Y"——图片是各去重后附件按 base64 估的占比(单张按路由的编码上限封顶),文本是总数减图片的剩余,两者恒等于请求体总数。图片 dominant 的请求一眼就能看出来。
152
152
  - 会话 id **始终完整记录**(它是会话身份,不是凭据;截断后两个会话会读成一个)。
153
153
  - 记录的是环形缓冲(超限丢最旧),进程重启即清;页面平时只在打开与手动刷新时读快照,有请求中时每秒自动刷新一次直到结束,没人打开的页面零流量。
154
- - 凭据永远不进记录:记录器根本不接收请求头参数,只收白名单里的会话 id 与 UA;请求一开始就有一条"请求中"占位行(状态列读秒),结束时原位补全为完整记录。失败的请求同样记一条(含错误码);消费者提前终止的流记一条 `ABORTED`,不再无声丢失。
154
+ - 凭据永远不进记录:记录器根本不接收请求头参数,只收白名单里的会话 id 与 UA;请求一开始就有一条"请求中"占位行(状态列读秒),结束时原位补全为完整记录。失败的请求同样记一条(含错误码)——包括目录解析失败(未知模型)与凭证缺失这类"第一个字节之前"的失败,行里没有用量只有原因;消费者提前终止的流记一条 `ABORTED`,不再无声丢失。
155
155
 
156
156
  **图片**:三个字段各管一层,超限的图片会被明确拒绝,而不是静默降质或丢弃:
157
157
 
package/lib/index.js CHANGED
@@ -76907,30 +76907,113 @@ var OpencodeGoAdapter = class extends LlmAdapter {
76907
76907
  "UNSUPPORTED_REASONING_EFFORT"
76908
76908
  );
76909
76909
  }
76910
+ /**
76911
+ * Open the request's log row.
76912
+ *
76913
+ * Called before any wait the request may hit — the catalog included — so
76914
+ * a cold catalog reads as preparation instead of invisible time, and a
76915
+ * failure before the first byte still leaves a row behind. Everything
76916
+ * here is already known or pure: the protocol arrives later, through
76917
+ * `identify`, once the catalog has answered.
76918
+ */
76919
+ startSpan(options, config, protocol) {
76920
+ const containsImage = options.messages.some((message) => contentHasImage2(message.content));
76921
+ const headers = providerHeaders(options.sessionId === void 0 ? void 0 : String(options.sessionId));
76922
+ const imageBudget = IMAGE_BUDGETS[config.imageBudgetPreset];
76923
+ const imageBytes = containsImage ? estimateImageWireBytes(options.messages, imageBudget.maxBytes) : void 0;
76924
+ return this.options.requestLog?.start({
76925
+ model: options.model,
76926
+ sessionId: options.sessionId === void 0 ? void 0 : String(options.sessionId),
76927
+ ...protocol === void 0 ? {} : { protocol },
76928
+ method: "POST",
76929
+ url: config.baseURL,
76930
+ messageCount: options.messages.length,
76931
+ hasImage: containsImage,
76932
+ toolCount: options.tools?.length ?? 0,
76933
+ ...imageBytes === void 0 ? {} : { imageBytes },
76934
+ headers: {
76935
+ ...options.sessionId === void 0 ? {} : { session: String(options.sessionId) },
76936
+ ...typeof headers["user-agent"] === "string" ? { userAgent: headers["user-agent"] } : {}
76937
+ }
76938
+ });
76939
+ }
76940
+ /**
76941
+ * Record a failure that happened before any request was built.
76942
+ *
76943
+ * Preparation (unlike dispatch) knows no session and no messages, so the
76944
+ * row reads as unbuilt rather than empty — but it still names the model
76945
+ * and the code, instead of leaving the caller with an error the log never
76946
+ * heard of.
76947
+ */
76948
+ logUnstartedFailure(config, model, error) {
76949
+ this.options.requestLog?.start({
76950
+ model,
76951
+ sessionId: void 0,
76952
+ method: "POST",
76953
+ url: config.baseURL,
76954
+ messageCount: 0,
76955
+ hasImage: false,
76956
+ toolCount: 0,
76957
+ headers: {}
76958
+ })?.finish(false, error instanceof LlmError9 ? error.code : void 0);
76959
+ }
76910
76960
  async prepareCall(_provider, model, _signal) {
76911
76961
  const config = this.options.config();
76912
76962
  assertStreamIdleTimeoutMs(config.streamIdleTimeoutMs);
76913
- const snapshot2 = await this.catalogOf(config).forModel(model);
76963
+ let snapshot2;
76964
+ try {
76965
+ snapshot2 = await this.catalogOf(config).forModel(model);
76966
+ } catch (error) {
76967
+ this.logUnstartedFailure(config, model, error);
76968
+ throw error;
76969
+ }
76914
76970
  const facts = snapshot2.facts.get(model);
76915
- if (facts === void 0) throw new LlmError9(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
76971
+ if (facts === void 0) {
76972
+ const error = new LlmError9(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
76973
+ this.logUnstartedFailure(config, model, error);
76974
+ throw error;
76975
+ }
76916
76976
  const prepared = this.modelInfo(withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits));
76917
76977
  return {
76918
76978
  model: prepared,
76919
- stream: (options) => this.streamWithSnapshot(options, config, snapshot2)
76979
+ stream: (options) => {
76980
+ const span = this.startSpan(options, config, facts.api);
76981
+ return this.streamWithSnapshot(options, config, snapshot2, span);
76982
+ }
76920
76983
  };
76921
76984
  }
76922
76985
  async *stream(options) {
76923
76986
  const config = this.options.config();
76924
76987
  assertStreamIdleTimeoutMs(config.streamIdleTimeoutMs);
76925
- const snapshot2 = await this.catalogOf(config).forModel(options.model);
76926
- yield* this.streamWithSnapshot(options, config, snapshot2);
76988
+ const span = this.startSpan(options, config);
76989
+ let snapshot2;
76990
+ try {
76991
+ snapshot2 = await this.catalogOf(config).forModel(options.model);
76992
+ } catch (error) {
76993
+ span?.finish(false, error instanceof LlmError9 ? error.code : void 0);
76994
+ throw error;
76995
+ }
76996
+ yield* this.streamWithSnapshot(options, config, snapshot2, span);
76927
76997
  }
76928
- async *streamWithSnapshot(options, config, snapshot2) {
76998
+ async *streamWithSnapshot(options, config, snapshot2, outerSpan) {
76929
76999
  var _stack = [];
76930
77000
  try {
76931
- assertStreamIdleTimeoutMs(config.streamIdleTimeoutMs);
77001
+ const span = outerSpan ?? this.startSpan(options, config);
77002
+ const failSpan = (error) => {
77003
+ span?.finish(false, error instanceof LlmError9 ? error.code : void 0);
77004
+ throw error;
77005
+ };
77006
+ try {
77007
+ assertStreamIdleTimeoutMs(config.streamIdleTimeoutMs);
77008
+ } catch (error) {
77009
+ failSpan(error);
77010
+ }
76932
77011
  const facts = snapshot2.facts.get(options.model);
76933
- if (facts === void 0) throw new LlmError9(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
77012
+ if (facts === void 0) {
77013
+ const error = new LlmError9(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
77014
+ span?.finish(false, error.code);
77015
+ throw error;
77016
+ }
76934
77017
  const model = withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits);
76935
77018
  const rates2 = usableRates(facts.cost);
76936
77019
  const stop = stopSequencesFor(model, options.stop);
@@ -76938,32 +77021,24 @@ var OpencodeGoAdapter = class extends LlmAdapter {
76938
77021
  const maxTokens = options.maxTokens === void 0 || ceiling === null || ceiling === void 0 ? options.maxTokens : Math.min(options.maxTokens, ceiling);
76939
77022
  const apiKey = await this.options.resolveApiKey();
76940
77023
  if (apiKey === void 0 || apiKey.length === 0) {
76941
- throw new LlmError9("dshopencodego: no credential resolved for the opencode-go route", "MISSING_CREDENTIAL");
77024
+ const error = new LlmError9("dshopencodego: no credential resolved for the opencode-go route", "MISSING_CREDENTIAL");
77025
+ span?.finish(false, error.code);
77026
+ throw error;
77027
+ }
77028
+ let reasoning;
77029
+ try {
77030
+ reasoning = this.resolveReasoningLevel(model, options.reasoningEffort);
77031
+ } catch (error) {
77032
+ failSpan(error);
76942
77033
  }
76943
- const reasoning = this.resolveReasoningLevel(model, options.reasoningEffort);
76944
77034
  const consumer = new AbortController();
76945
77035
  const upstream = options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]);
76946
77036
  const watchdog = __using(_stack, idleWatchdog(upstream, config.streamIdleTimeoutMs, "LLM_STREAM_IDLE_TIMEOUT"));
76947
77037
  let timedOut = false;
77038
+ span?.identify(facts.api);
76948
77039
  const containsImage = options.messages.some((message) => contentHasImage2(message.content));
76949
77040
  const headers = providerHeaders(options.sessionId === void 0 ? void 0 : String(options.sessionId));
76950
77041
  const imageBudget = IMAGE_BUDGETS[config.imageBudgetPreset];
76951
- const imageBytes = containsImage ? estimateImageWireBytes(options.messages, imageBudget.maxBytes) : void 0;
76952
- const span = this.options.requestLog?.start({
76953
- model: options.model,
76954
- sessionId: options.sessionId === void 0 ? void 0 : String(options.sessionId),
76955
- protocol: facts.api,
76956
- method: "POST",
76957
- url: config.baseURL,
76958
- messageCount: options.messages.length,
76959
- hasImage: containsImage,
76960
- toolCount: options.tools?.length ?? 0,
76961
- ...imageBytes === void 0 ? {} : { imageBytes },
76962
- headers: {
76963
- ...options.sessionId === void 0 ? {} : { session: String(options.sessionId) },
76964
- ...typeof headers["user-agent"] === "string" ? { userAgent: headers["user-agent"] } : {}
76965
- }
76966
- });
76967
77042
  const fetchFactory = this.options.fetch;
76968
77043
  const requestFetch = span === void 0 || fetchFactory === void 0 ? fetchFactory : () => {
76969
77044
  const base = fetchFactory();
@@ -77937,6 +78012,9 @@ var RequestLog = class {
77937
78012
  let reported = false;
77938
78013
  let finished = false;
77939
78014
  return {
78015
+ identify: (protocol) => {
78016
+ if (record2.protocol === void 0) record2.protocol = protocol;
78017
+ },
77940
78018
  token: () => {
77941
78019
  const now = Date.now();
77942
78020
  if (firstTokenAt === void 0) {
@@ -77969,7 +78047,10 @@ var RequestLog = class {
77969
78047
  seq,
77970
78048
  atMs: startedAt,
77971
78049
  model: detail.model,
77972
- ...detail.protocol === void 0 ? {} : { protocol: detail.protocol },
78050
+ // The protocol may have arrived after the row opened, through
78051
+ // `identify` — the running row and the completion read the same
78052
+ // field, so they never disagree.
78053
+ ...record2.protocol === void 0 ? {} : { protocol: record2.protocol },
77973
78054
  session,
77974
78055
  method: record2.method,
77975
78056
  url: record2.url,
@@ -78134,7 +78215,7 @@ function createUpdateChecker(deps) {
78134
78215
  }
78135
78216
 
78136
78217
  // src/update/version.ts
78137
- var PLUGIN_VERSION = true ? "0.1.82" : "0.0.0-dev";
78218
+ var PLUGIN_VERSION = true ? "0.1.83" : "0.0.0-dev";
78138
78219
 
78139
78220
  // src/update/service.ts
78140
78221
  import { TypertRemoteService as TypertRemoteService4 } from "@deepseek-ai/dsh-typert-protocol";
@@ -156,6 +156,25 @@ export declare class OpencodeGoAdapter extends LlmAdapter {
156
156
  providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined;
157
157
  /** Validate an explicit effort against the model's own levels, without clamping. */
158
158
  private resolveReasoningLevel;
159
+ /**
160
+ * Open the request's log row.
161
+ *
162
+ * Called before any wait the request may hit — the catalog included — so
163
+ * a cold catalog reads as preparation instead of invisible time, and a
164
+ * failure before the first byte still leaves a row behind. Everything
165
+ * here is already known or pure: the protocol arrives later, through
166
+ * `identify`, once the catalog has answered.
167
+ */
168
+ private startSpan;
169
+ /**
170
+ * Record a failure that happened before any request was built.
171
+ *
172
+ * Preparation (unlike dispatch) knows no session and no messages, so the
173
+ * row reads as unbuilt rather than empty — but it still names the model
174
+ * and the code, instead of leaving the caller with an error the log never
175
+ * heard of.
176
+ */
177
+ private logUnstartedFailure;
159
178
  prepareCall(_provider: string, model: string, _signal?: AbortSignal): Promise<PreparedAdapterCall>;
160
179
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
161
180
  private streamWithSnapshot;
@@ -83,10 +83,12 @@ export interface RequestLogEntry {
83
83
  */
84
84
  readonly streamMs?: number;
85
85
  /**
86
- * Wall time from request start to the moment the fetch actually went out,
87
- * in milliseconds — the local preparation (catalog, conversion, images).
88
- * The rest of the first-token wait belongs to the wire and the gateway.
89
- * Absent when the request never reached the wire (rejected up front).
86
+ * Wall time from request start to the moment the fetch went out,
87
+ * in milliseconds — the whole local path, catalog wait included: the row
88
+ * opens before the catalog is consulted, so a cold catalog reads as
89
+ * preparation rather than invisible time. The rest of the first-token
90
+ * wait belongs to the wire and the gateway. Absent when the request never
91
+ * reached the wire (rejected up front).
90
92
  */
91
93
  readonly prepMs?: number;
92
94
  /** Wall time from request start to terminal event, in milliseconds. */
@@ -218,6 +220,14 @@ export interface RequestSpan {
218
220
  request(method: string, url: string, requestBytes?: number): void;
219
221
  /** Mark the response status line's arrival. */
220
222
  status(status: number): void;
223
+ /**
224
+ * Fill in the wire protocol once the catalog has answered.
225
+ *
226
+ * The row opens before the catalog wait (so the wait itself is recorded),
227
+ * but the protocol is only known after it. Called at most once, before
228
+ * `finish`; a second call keeps the first.
229
+ */
230
+ identify(protocol: WireProtocol): void;
221
231
  /**
222
232
  * Close the span and complete its running row in place.
223
233
  * @param ok - whether the request completed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dan-ai-studio/dshopencodego",
3
- "version": "0.1.82",
3
+ "version": "0.1.83",
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",