@xynogen/pix-9router 0.3.22 → 0.4.0

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/provider.ts +70 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-9router",
3
- "version": "0.3.22",
3
+ "version": "0.4.0",
4
4
  "description": "Pi extension — 9Router provider + fetch/search tools via router API",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/provider.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * ROUTER_API_KEY — bearer token (required for live model list)
10
10
  */
11
11
 
12
+ import type { RefreshModelsContext } from "@earendil-works/pi-ai";
12
13
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
14
  import type { ModelsDevModel, RouterModel } from "./data.js";
14
15
  import { fetchModelsDevIndex, lookupInIndex, routerBaseUrl, routerModels } from "./data.js";
@@ -26,6 +27,33 @@ const ZERO_COST = {
26
27
  // Fallback pattern-based detection if models.dev lookup fails
27
28
  const IMAGE_CAPABLE_PATTERNS = [/claude/i, /gpt-5/i, /gpt-4/i, /kimi-k2/i, /hy3/i];
28
29
 
30
+ interface RouterModelsResponse {
31
+ data?: RouterModel[];
32
+ }
33
+
34
+ const COMPAT = {
35
+ supportsDeveloperRole: false,
36
+ supportsUsageInStreaming: false,
37
+ maxTokensField: "max_tokens",
38
+ } as const;
39
+
40
+ function toModelConfig(devIndex: Map<string, ModelsDevModel>) {
41
+ return (model: RouterModel) => {
42
+ const id = model.id ?? "";
43
+ const devModel = lookupInIndex(id, devIndex);
44
+ return {
45
+ id,
46
+ name: getModelName(model, devModel),
47
+ reasoning: getReasoning(model, devModel),
48
+ input: getInputTypes(model, devModel),
49
+ cost: ZERO_COST,
50
+ contextWindow: getContextWindow(model, devModel),
51
+ maxTokens: getMaxTokens(model, devModel),
52
+ compat: COMPAT,
53
+ };
54
+ };
55
+ }
56
+
29
57
  export function getInputTypes(model: RouterModel, devModel?: ModelsDevModel): ("text" | "image")[] {
30
58
  if (devModel?.modalities?.input) {
31
59
  const inputs = devModel.modalities.input.filter(
@@ -82,39 +110,54 @@ export default async function registerProvider(pi: ExtensionAPI): Promise<void>
82
110
  return;
83
111
  }
84
112
 
85
- const [models, devIndex] = await Promise.all([
113
+ // Upstream moved OpenAI `compat` settings from the provider level to the
114
+ // per-model level (ProviderModelConfig.compat). Applied in toModelConfig.
115
+
116
+ // Register from disk cache only — startup must not block on network.
117
+ const cached = routerModels.getCached();
118
+ pi.registerProvider("9router", providerConfig(apiKey, cached, new Map()));
119
+
120
+ // Background refresh; re-registering after startup applies immediately.
121
+ void Promise.all([
86
122
  routerModels.get(),
87
123
  fetchModelsDevIndex().catch(() => new Map<string, ModelsDevModel>()),
88
- ]);
124
+ ])
125
+ .then(([models, devIndex]) =>
126
+ pi.registerProvider("9router", providerConfig(apiKey, models, devIndex)),
127
+ )
128
+ .catch(() => {});
129
+ }
89
130
 
90
- // Upstream moved OpenAI `compat` settings from the provider level to the
91
- // per-model level (ProviderModelConfig.compat). Apply the same shim to every
92
- // registered model.
93
- const COMPAT = {
94
- supportsDeveloperRole: false,
95
- supportsUsageInStreaming: false,
96
- maxTokensField: "max_tokens",
97
- } as const;
98
-
99
- pi.registerProvider("9router", {
131
+ function providerConfig(
132
+ apiKey: string,
133
+ models: RouterModel[],
134
+ devIndex: Map<string, ModelsDevModel>,
135
+ ) {
136
+ return {
100
137
  name: "9Router",
101
138
  baseUrl: routerBaseUrl(),
102
139
  apiKey: "$ROUTER_API_KEY",
103
140
  api: "openai-completions",
104
141
  headers: { "User-Agent": "pi-coding-agent" },
105
- models: models.map((model) => {
106
- const id = model.id ?? "";
107
- const devModel = lookupInIndex(id, devIndex);
108
- return {
109
- id,
110
- name: getModelName(model, devModel),
111
- reasoning: getReasoning(model, devModel),
112
- input: getInputTypes(model, devModel),
113
- cost: ZERO_COST,
114
- contextWindow: getContextWindow(model, devModel),
115
- maxTokens: getMaxTokens(model, devModel),
116
- compat: COMPAT,
117
- };
118
- }),
119
- });
142
+ models: models.map(toModelConfig(devIndex)),
143
+ // Live fetch on /model refresh — bypasses the disk cache.
144
+ // Pi calls refreshModels once per provider at every startup with
145
+ // allowNetwork:false (awaited, no timeout) — never touch the network
146
+ // there; serve the registered snapshot instead.
147
+ async refreshModels({ signal, allowNetwork }: RefreshModelsContext) {
148
+ if (!allowNetwork) return models.map(toModelConfig(devIndex));
149
+ const res = await fetch(`${routerBaseUrl()}/models`, {
150
+ signal: AbortSignal.any([AbortSignal.timeout(8_000), ...(signal ? [signal] : [])]),
151
+ headers: {
152
+ Authorization: `Bearer ${apiKey}`,
153
+ "User-Agent": "pi-coding-agent",
154
+ },
155
+ });
156
+ if (!res.ok) throw new Error(`9router /models: ${res.status}`);
157
+ const raw = (await res.json()) as RouterModelsResponse;
158
+ const list = (raw.data ?? []).filter((m) => Boolean(m.id));
159
+ const fresh = await fetchModelsDevIndex().catch(() => new Map<string, ModelsDevModel>());
160
+ return list.map(toModelConfig(fresh));
161
+ },
162
+ };
120
163
  }