@dan-ai-studio/dshopencodego 0.1.56 → 0.1.58

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
@@ -173,6 +173,8 @@ The gateway's `/v1/models` answers only *which* models exist, never *how* to cal
173
173
  - a model the provider **retires** disappears on the next refresh;
174
174
  - 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.
175
175
 
176
+ 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.
177
+
176
178
  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.
177
179
 
178
180
  ## The model list in the settings page
@@ -260,7 +262,7 @@ npm test # contract tests against a real local HTTP mock gateway
260
262
 
261
263
  **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.
262
264
 
263
- 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 **56 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
265
+ 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 **57 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
264
266
 
265
267
  ## Releasing (maintainers)
266
268
 
package/README.md CHANGED
@@ -173,6 +173,8 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
173
173
  - 官方**下线**模型 → 下次刷新即消失;
174
174
  - 内置目录**冻结**在插件版本里,仅影响"怎么调"的推断质量,不影响"有哪些"。
175
175
 
176
+ 网关**临时不可用**时,插件保留上次成功获取的列表并在页面上标注陈旧,同时在后台重试:重试由**5 分钟的退避**控制,而不是等满一个 TTL,所以网关恢复后 5 分钟内列表就会更新;网关持续不可用时,它只被问一次,不会每次模型请求都去问一遍。设置页的「重新检查」随时可以强制重验,不受这个退避限制。
177
+
176
178
  只有当 pi-ai 内置目录**缺了某个模型的线怪癖**(如 DeepSeek 的 `thinkingFormat`)时,才需要等插件同步 pi-ai 的新版本——而模型本身早已可用。
177
179
 
178
180
  ## 设置页里的模型列表
@@ -260,7 +262,7 @@ npm test # 契约测试:真实 HTTP mock 网关,断言请求头/
260
262
 
261
263
  **顺序有讲究**:`npm run build` 要排在 `npm test` 之前——`tests/pack-contents.spec.ts` 会遍历 `exports` 的每个目标,`lib/` 存在与否决定这条检查是执行还是被跳过(不报错,但会少一层保护)。`package.json` 已移除 `prepare`,因此 `npm ci` 不会再替你隐式构建;三个 CI workflow 都显式跑了 `npm run build`。
262
264
 
263
- 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **56 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
265
+ 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **57 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
264
266
 
265
267
  ## 发布(维护者)
266
268
 
package/lib/index.js CHANGED
@@ -74390,12 +74390,15 @@ var OpencodeGoCatalog = class {
74390
74390
  return Promise.resolve(this.served);
74391
74391
  }
74392
74392
  }
74393
- this.pending ??= this.build(signal).then((snapshot2) => {
74394
- this.served = snapshot2;
74395
- return snapshot2;
74396
- }).finally(() => {
74397
- this.pending = void 0;
74398
- });
74393
+ if (this.pending === void 0) {
74394
+ this.lastRefreshAttemptAt = Date.now();
74395
+ this.pending = this.build(signal).then((snapshot2) => {
74396
+ this.served = snapshot2;
74397
+ return snapshot2;
74398
+ }).finally(() => {
74399
+ this.pending = void 0;
74400
+ });
74401
+ }
74399
74402
  return this.pending;
74400
74403
  }
74401
74404
  /**
@@ -74548,7 +74551,20 @@ var OpencodeGoCatalog = class {
74548
74551
  live: false,
74549
74552
  listingFailure: listing.reason,
74550
74553
  ...degradations.length === 0 ? {} : { degradations },
74551
- fetchedAtMs: Date.now(),
74554
+ // The age of this snapshot is the age of what it holds, not the age of
74555
+ // the failure.
74556
+ //
74557
+ // Stamping it "now" resets the TTL on every failure, and the refresh
74558
+ // behind it then waits a whole TTL before asking again — which is
74559
+ // exactly when a gateway that came back would be noticed. The age
74560
+ // stays where the last successful listing left it, so the TTL keeps
74561
+ // expiring and the back-off decides how often the gateway is asked
74562
+ // while it is down.
74563
+ //
74564
+ // Zero means "no listing has ever answered": there is nothing to be
74565
+ // old about, so the snapshot is treated as already stale and the
74566
+ // back-off is the only thing between it and the next attempt.
74567
+ fetchedAtMs: previous?.fetchedAtMs ?? 0,
74552
74568
  ...this.documentQuotas()
74553
74569
  };
74554
74570
  }
@@ -77318,6 +77334,13 @@ function hostOf(input) {
77318
77334
  return void 0;
77319
77335
  }
77320
77336
  }
77337
+ async function edgeRefused(response) {
77338
+ try {
77339
+ return (await response.clone().text()).includes("developers.cloudflare.com");
77340
+ } catch {
77341
+ return false;
77342
+ }
77343
+ }
77321
77344
  function gatewayFetch(pin) {
77322
77345
  const address = pin.address;
77323
77346
  if (address === void 0) return globalThis.fetch;
@@ -77327,10 +77350,17 @@ function gatewayFetch(pin) {
77327
77350
  if (isPinSpent(pin)) return globalThis.fetch(input, init);
77328
77351
  try {
77329
77352
  const dispatcher = dispatcherFor(pin.host, address);
77330
- return await dispatchingFetch(input, {
77353
+ const response = await dispatchingFetch(input, {
77331
77354
  ...init,
77332
77355
  dispatcher
77333
77356
  });
77357
+ if (response.status === 403 && await edgeRefused(response)) {
77358
+ await response.body?.cancel().catch(() => {
77359
+ });
77360
+ spent.add(spentKey(pin));
77361
+ return globalThis.fetch(input, init);
77362
+ }
77363
+ return response;
77334
77364
  } catch (error) {
77335
77365
  if (!addressIsGone(error)) throw error;
77336
77366
  if (init?.signal?.aborted === true) throw error;
@@ -77452,7 +77482,7 @@ function createUpdateChecker(deps) {
77452
77482
  }
77453
77483
 
77454
77484
  // src/update/version.ts
77455
- var PLUGIN_VERSION = true ? "0.1.56" : "0.0.0-dev";
77485
+ var PLUGIN_VERSION = true ? "0.1.58" : "0.0.0-dev";
77456
77486
 
77457
77487
  // src/update/service.ts
77458
77488
  import { TypertRemoteService as TypertRemoteService3 } from "@deepseek-ai/dsh-typert-protocol";
@@ -43,6 +43,11 @@ export declare const METADATA_RETRY_DELAY_MS = 1500;
43
43
  * Five minutes is short enough that a service that comes back is picked up
44
44
  * soon, and long enough that a service that stays down is asked once, not
45
45
  * once per request.
46
+ *
47
+ * This is also the ceiling on how long an outage costs. A failed refresh
48
+ * keeps the age of the last reading that succeeded, so the TTL stays expired
49
+ * and the next call after this window tries again — a gateway that comes back
50
+ * is noticed within five minutes, not within a whole TTL.
46
51
  */
47
52
  export declare const BACKGROUND_RETRY_BACKOFF_MS: number;
48
53
  /** Response caps: the listing is tiny, the metadata document is not. */
@@ -91,6 +91,16 @@ export interface CatalogSnapshot {
91
91
  * one that is true now.
92
92
  */
93
93
  readonly factsCachedAt?: number;
94
+ /**
95
+ * When the content of this snapshot was obtained.
96
+ *
97
+ * It is the age of the *knowledge*, not of the attempt: a snapshot that
98
+ * could not be rebuilt keeps the age of the last reading that succeeded,
99
+ * because that is when the content became true. Stamping it with the time
100
+ * of the failure instead would make a failed refresh look like a fresh one,
101
+ * and the TTL that governs the next attempt would then be measured from
102
+ * the failure rather than from the last thing that was actually known.
103
+ */
94
104
  readonly fetchedAtMs: number;
95
105
  }
96
106
  /** Observers the plugin logs through. */
@@ -48,7 +48,7 @@ export interface CatalogReading {
48
48
  * worse than one that admits it.
49
49
  */
50
50
  readonly cached?: boolean;
51
- /** When the underlying snapshot was built. */
51
+ /** When the content behind this reading was obtained. */
52
52
  readonly fetchedAtMs: number;
53
53
  /**
54
54
  * Where the per-model allowances came from: the provider's live document, or
@@ -80,6 +80,14 @@ export declare function closeDispatcher(): Promise<void>;
80
80
  * @returns true when the address was found to be gone.
81
81
  */
82
82
  export declare function isPinSpent(pin: GatewayPin): boolean;
83
+ /**
84
+ * Forget the addresses that were found to be gone.
85
+ *
86
+ * The memory belongs to the process, so it outlives every request. A test
87
+ * that needs a spent address to be unspent again has no other way to say
88
+ * so, and a spent address is exactly what such a test sets up.
89
+ */
90
+ export declare function resetSpentAddresses(): void;
83
91
  /**
84
92
  * A fetch that sends the gateway's name to the pinned address.
85
93
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dan-ai-studio/dshopencodego",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
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",