@haoyiyin/9router 0.1.2 → 0.1.4

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.md CHANGED
@@ -1,38 +1,28 @@
1
1
  # @haoyiyin/9router
2
2
 
3
- pi 扩展 — 9Router provider(含 OpenCode Free `oc/` 模型)+ fetch / search / transcribe 工具。
3
+ Pi extension — a **9Router provider** (including OpenCode Free `oc/` models) plus `fetch`, `search`, and `transcribe` tools.
4
4
 
5
- Fork of [`@xynogen/pix-9router`](https://www.npmjs.com/package/@xynogen/pix-9router),修复了 9router 的 `/v1/models` 永远不列出 OpenCode Free `-free` 模型的问题,本扩展直接暴露这些 `oc/` 模型。
5
+ A fork of [`@xynogen/pix-9router`](https://www.npmjs.com/package/@xynogen/pix-9router) that fixes the issue where 9router's `/v1/models` never lists OpenCode Free `-free` models — this extension exposes those `oc/` models directly.
6
6
 
7
- ## 安装
7
+ ## Install
8
8
 
9
9
  ```
10
10
  pi install npm:@haoyiyin/9router
11
11
  ```
12
12
 
13
- ## 配置
13
+ ## Configure
14
14
 
15
- 默认无内置地址。运行 `/9router` → 输入 9Router 地址和 API Key → 存入 `~/.pi/agent/haoyiyin/9router.json`(权限 `0600`)。Key 已配置时 `/9router` 可留空保持原值。
15
+ There is no built-in default endpoint. Run `/9router`, enter the 9Router base URL and API key, and they are saved to `~/.pi/agent/haoyiyin/9router.json` (mode `0600`). Leave the key empty to keep the currently configured key.
16
16
 
17
- 也可以设置环境变量兜底:`ROUTER_API_BASE` / `ROUTER_API_KEY`(配置文件优先)。
17
+ Environment variables are supported as a fallback: `ROUTER_API_BASE` / `ROUTER_API_KEY` (the config file takes precedence).
18
18
 
19
- ## oc 模型
19
+ ## oc models
20
20
 
21
- 每 30 分钟自动拉取 `opencode.ai/zen/v1/models` 中 id 以 `-free` 结尾的模型,以 `oc/<id>` 形式注册进 9router provider。列表波动会自动跟随。缓存文件:`~/.cache/pi/zen-oc-models.json`。
21
+ Every 30 minutes the extension fetches models from `opencode.ai/zen/v1/models` whose ids end in `-free`, and registers them under the 9router provider as `oc/<id>`. Changes to the upstream list are picked up automatically. Cache file: `~/.cache/pi/zen-oc-models.json`.
22
22
 
23
- ## 迁移
23
+ ## Tools
24
24
 
25
- 如果你装过 `@xynogen/pix-9router`:
26
-
27
- ```
28
- pi remove npm:@xynogen/pix-9router
29
- ```
30
-
31
- 并(可选)删除 `~/.zshrc` 里的 `ROUTER_API_KEY` / `ROUTER_API_BASE` export。
32
-
33
- ## 工具
34
-
35
- `fetch` / `search` / `transcribe` 与 `pix-9router` 一致;collapse 配置仍在 `~/.pi/agent/pix.json`。
25
+ `fetch` / `search` / `transcribe` behave the same as `pix-9router`; collapse config still lives in `~/.pi/agent/pix.json`.
36
26
 
37
27
  ## License
38
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haoyiyin/9router",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "description": "Pi extension — 9Router provider (OpenCode Free oc/-free models + router models) + fetch/search/transcribe tools",
package/src/command.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { loadConfig, saveConfig, DEFAULT_BASE } from "./config.ts";
3
- import { registerProvider } from "./provider.ts";
3
+ import { registerProvider, isStaleCtxError } from "./provider.ts";
4
4
 
5
5
  // Never contains a real key. If a dialog pre-fills this placeholder as an
6
6
  // editable default, an exact match is treated as "keep the current key".
@@ -41,7 +41,9 @@ export function registerCommand(pi: ExtensionAPI): void {
41
41
  await registerProvider(pi);
42
42
  } catch (err) {
43
43
  ctx.ui.notify(
44
- `9Router saved, but provider re-registration failed: ${err instanceof Error ? err.message : String(err)}`,
44
+ isStaleCtxError(err)
45
+ ? "9Router saved. This extension instance was replaced by a session change/reload; the new instance has re-registered the provider."
46
+ : `9Router saved, but provider re-registration failed: ${err instanceof Error ? err.message : String(err)}`,
45
47
  "warning",
46
48
  );
47
49
  }
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import registerFetch from "./fetch.ts";
3
3
  import registerSearch from "./search.ts";
4
4
  import registerTranscribe from "./transcribe.ts";
5
- import { registerProvider } from "./provider.ts";
5
+ import { registerProvider, isStaleCtxError } from "./provider.ts";
6
6
  import { registerCommand } from "./command.ts";
7
7
 
8
8
  export default async function (pi: ExtensionAPI): Promise<void> {
@@ -15,14 +15,27 @@ export default async function (pi: ExtensionAPI): Promise<void> {
15
15
  await registerProvider(pi);
16
16
 
17
17
  let refreshing = false;
18
+ let closed = false;
18
19
  const timer = setInterval(
19
20
  async () => {
20
21
  // ponytail: models refresh by unregister+register; zen cache TTL is
21
22
  // 30m so refetch happens naturally. Overlap guard avoids re-entrancy.
22
- if (refreshing) return;
23
+ if (closed || refreshing) return;
23
24
  refreshing = true;
24
25
  try {
25
26
  await registerProvider(pi);
27
+ } catch (err) {
28
+ // The captured `pi` goes stale after session replacement or
29
+ // extension reload, and pi's assertActive() throw must never
30
+ // escape this callback — it would crash the whole process as an
31
+ // uncaughtException. The new session's extension instance
32
+ // re-registers the provider itself, so stop this timer for good.
33
+ if (isStaleCtxError(err)) {
34
+ closed = true;
35
+ clearInterval(timer);
36
+ }
37
+ // Any other error (network hiccup, bad upstream payload) is
38
+ // swallowed: the next tick retries.
26
39
  } finally {
27
40
  refreshing = false;
28
41
  }
@@ -32,4 +45,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
32
45
  if (typeof (timer as { unref?: () => void }).unref === "function") {
33
46
  (timer as { unref: () => void }).unref();
34
47
  }
48
+ pi.on("session_shutdown", () => {
49
+ closed = true;
50
+ clearInterval(timer);
51
+ });
35
52
  }
package/src/provider.ts CHANGED
@@ -69,6 +69,16 @@ export function getReasoning(model: RouterModel, devModel?: ModelsDevModel): boo
69
69
  return /reasoner|thinking|xhigh|high|max|pro|codex|opus|sonnet/i.test(model.id ?? "");
70
70
  }
71
71
 
72
+ /**
73
+ * True if `err` is pi's stale-extension-ctx error, thrown when a captured
74
+ * `pi`/command ctx is used after session replacement (newSession / fork /
75
+ * switchSession) or extension reload. A stale instance can never recover —
76
+ * the new session's extension instance re-registers everything itself.
77
+ */
78
+ export function isStaleCtxError(err: unknown): boolean {
79
+ return err instanceof Error && /stale after session replacement|ctx is stale/.test(err.message);
80
+ }
81
+
72
82
  export async function registerProvider(pi: ExtensionAPI): Promise<void> {
73
83
  const cfg = loadConfig();
74
84