@xynogen/pix-9router 0.7.0 → 0.8.1
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 +16 -2
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/provider.ts +156 -5
package/README.md
CHANGED
|
@@ -33,8 +33,22 @@ The standard names win when both sets exist.
|
|
|
33
33
|
pi install npm:@xynogen/pix-9router
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
> Standalone and opt-in. [`@xynogen/pix-core`](https://www.npmjs.com/package/@xynogen/pix-core) does not bundle it. It needs a 9Router API key.
|
|
37
|
+
|
|
38
|
+
## Full distro
|
|
39
|
+
|
|
40
|
+
This package is part of [Pix](https://github.com/xynogen/pix-mono). The installer sets up Pi and the full distro. See [Install](https://github.com/xynogen/pix-mono#install) for the notes for each OS.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Linux / macOS
|
|
44
|
+
curl -fsSL https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.sh | sh
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
# Windows
|
|
49
|
+
irm https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.ps1 | iex
|
|
50
|
+
```
|
|
37
51
|
|
|
38
52
|
## License
|
|
39
53
|
|
|
40
|
-
MIT
|
|
54
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-9router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Pi extension — 9Router model provider",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@xynogen/pix-data": "^0.5.1",
|
|
48
|
-
"@xynogen/pix-runtime": "^0.
|
|
48
|
+
"@xynogen/pix-runtime": "^0.13.0"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import registerProvider from "./provider.
|
|
15
|
+
import registerProvider from "./provider.ts";
|
|
16
16
|
|
|
17
17
|
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
18
18
|
await registerProvider(pi);
|
package/src/provider.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
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
|
+
import type { ClassifierResult, RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
13
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { ioTimeoutSignal } from "@xynogen/pix-runtime/io";
|
|
15
|
-
import type { ModelsDevModel, RouterModel } from "./data.
|
|
16
|
-
import { fetchModelsDevIndex, lookupInIndex, routerBaseUrl, routerModels } from "./data.
|
|
15
|
+
import type { ModelsDevModel, RouterModel } from "./data.ts";
|
|
16
|
+
import { fetchModelsDevIndex, lookupInIndex, routerBaseUrl, routerModels } from "./data.ts";
|
|
17
17
|
|
|
18
18
|
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
19
19
|
const DEFAULT_MAX_TOKENS = 16_384;
|
|
@@ -28,6 +28,28 @@ const ZERO_COST = {
|
|
|
28
28
|
// Fallback pattern-based detection if models.dev lookup fails
|
|
29
29
|
const IMAGE_CAPABLE_PATTERNS = [/claude/i, /gpt-5/i, /gpt-4/i, /kimi-k2/i, /hy3/i];
|
|
30
30
|
|
|
31
|
+
/** Known patterns for classifier models (System One protocol) */
|
|
32
|
+
export const CLASSIFIER_PATTERNS = [
|
|
33
|
+
/\bjev\b/i,
|
|
34
|
+
/\bspan\b/i,
|
|
35
|
+
/\bsolar-decide\b/i,
|
|
36
|
+
/\bclassifier\b/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export function isClassifierModel(model: RouterModel): boolean {
|
|
40
|
+
const id = model.id ?? "";
|
|
41
|
+
const name = model.name ?? "";
|
|
42
|
+
const custom = (process.env.NINEROUTER_CLASSIFIER_MODELS || "")
|
|
43
|
+
.split(",")
|
|
44
|
+
.map((s) => s.trim().toLowerCase())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
|
|
47
|
+
if (custom.some((c) => id.toLowerCase().includes(c) || name.toLowerCase().includes(c))) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return CLASSIFIER_PATTERNS.some((p) => p.test(id) || p.test(name));
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
interface RouterModelsResponse {
|
|
32
54
|
data?: RouterModel[];
|
|
33
55
|
}
|
|
@@ -38,10 +60,24 @@ const COMPAT = {
|
|
|
38
60
|
maxTokensField: "max_tokens",
|
|
39
61
|
} as const;
|
|
40
62
|
|
|
41
|
-
function toModelConfig(devIndex: Map<string, ModelsDevModel>) {
|
|
63
|
+
export function toModelConfig(devIndex: Map<string, ModelsDevModel>) {
|
|
42
64
|
return (model: RouterModel) => {
|
|
43
65
|
const id = model.id ?? "";
|
|
44
66
|
const devModel = lookupInIndex(id, devIndex);
|
|
67
|
+
|
|
68
|
+
if (isClassifierModel(model)) {
|
|
69
|
+
return {
|
|
70
|
+
type: "classifier" as const,
|
|
71
|
+
id,
|
|
72
|
+
name: getModelName(model, devModel),
|
|
73
|
+
api: "typesafe-system-one",
|
|
74
|
+
baseUrl: routerBaseUrl(),
|
|
75
|
+
input: ["text" as const],
|
|
76
|
+
cost: ZERO_COST,
|
|
77
|
+
contextWindow: getContextWindow(model, devModel),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
45
81
|
return {
|
|
46
82
|
id,
|
|
47
83
|
name: getModelName(model, devModel),
|
|
@@ -93,7 +129,10 @@ export function getMaxTokens(model: RouterModel, devModel?: ModelsDevModel): num
|
|
|
93
129
|
|
|
94
130
|
export function getReasoning(model: RouterModel, devModel?: ModelsDevModel): boolean {
|
|
95
131
|
if (typeof devModel?.reasoning === "boolean") return devModel.reasoning;
|
|
96
|
-
|
|
132
|
+
// ponytail: Detect GPT-5/6 offline. Add metadata for new families instead of a network wait.
|
|
133
|
+
return /reasoner|thinking|xhigh|high|max|pro|codex|opus|sonnet|(?:^|\/)gpt-[56](?:[.-]|$)/i.test(
|
|
134
|
+
model.id ?? "",
|
|
135
|
+
);
|
|
97
136
|
}
|
|
98
137
|
|
|
99
138
|
export default async function registerProvider(pi: ExtensionAPI): Promise<void> {
|
|
@@ -129,6 +168,109 @@ export default async function registerProvider(pi: ExtensionAPI): Promise<void>
|
|
|
129
168
|
.catch(() => {});
|
|
130
169
|
}
|
|
131
170
|
|
|
171
|
+
export async function classifySystemOne(
|
|
172
|
+
model: { id: string; baseUrl?: string; api?: string; provider?: string },
|
|
173
|
+
context: { state: Record<string, unknown>; questions: Record<string, any> },
|
|
174
|
+
apiKey: string,
|
|
175
|
+
options?: { signal?: AbortSignal; headers?: Record<string, string> },
|
|
176
|
+
): Promise<ClassifierResult> {
|
|
177
|
+
const output = {
|
|
178
|
+
api: model.api ?? "typesafe-system-one",
|
|
179
|
+
provider: model.provider ?? "9router",
|
|
180
|
+
model: model.id,
|
|
181
|
+
answers: {} as Record<string, any>,
|
|
182
|
+
stopReason: "stop" as const,
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const wireQuestions = Object.fromEntries(
|
|
188
|
+
Object.entries(context.questions ?? {}).map(([key, q]) => [
|
|
189
|
+
key,
|
|
190
|
+
q.type === "bool" ? { ...q, type: "noul" } : q,
|
|
191
|
+
]),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const base = (model.baseUrl || routerBaseUrl()).replace(/\/+$/, "");
|
|
195
|
+
const url = `${base}/systemone`;
|
|
196
|
+
const res = await fetch(url, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
signal: ioTimeoutSignal(options?.signal),
|
|
199
|
+
headers: {
|
|
200
|
+
Authorization: `Bearer ${apiKey}`,
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
"User-Agent": "pi-coding-agent",
|
|
203
|
+
...(options?.headers ?? {}),
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
model: model.id,
|
|
207
|
+
state: context.state,
|
|
208
|
+
questions: wireQuestions,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
const errText = await res.text().catch(() => "");
|
|
214
|
+
return {
|
|
215
|
+
...output,
|
|
216
|
+
stopReason: "error" as const,
|
|
217
|
+
errorMessage: `9router /systemone returned ${res.status}: ${errText}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const data = (await res.json()) as any;
|
|
222
|
+
const answers: Record<string, any> = {};
|
|
223
|
+
|
|
224
|
+
for (const [key, q] of Object.entries(context.questions ?? {})) {
|
|
225
|
+
const ans = data?.answers?.[key];
|
|
226
|
+
if (!ans) continue;
|
|
227
|
+
if (q.type === "bool") {
|
|
228
|
+
answers[key] = {
|
|
229
|
+
type: "bool",
|
|
230
|
+
probability: typeof ans.noul === "number" ? ans.noul : (ans.probability ?? 0),
|
|
231
|
+
};
|
|
232
|
+
} else if (q.type === "choice") {
|
|
233
|
+
answers[key] = {
|
|
234
|
+
type: "choice",
|
|
235
|
+
choice: ans.choice,
|
|
236
|
+
probabilities: ans.probabilities ?? {},
|
|
237
|
+
confidence: ans.confidence ?? 0,
|
|
238
|
+
};
|
|
239
|
+
} else if (q.type === "score") {
|
|
240
|
+
answers[key] = {
|
|
241
|
+
type: "score",
|
|
242
|
+
score: ans.score ?? 0,
|
|
243
|
+
confidence: ans.confidence ?? 0,
|
|
244
|
+
};
|
|
245
|
+
} else {
|
|
246
|
+
answers[key] = ans;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const inputTokens = data?.usage?.input_tokens ?? 0;
|
|
251
|
+
const outputTokens = data?.usage?.output_tokens ?? 0;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...output,
|
|
255
|
+
answers,
|
|
256
|
+
usage: {
|
|
257
|
+
input: inputTokens,
|
|
258
|
+
output: outputTokens,
|
|
259
|
+
cacheRead: 0,
|
|
260
|
+
cacheWrite: 0,
|
|
261
|
+
totalTokens: inputTokens + outputTokens,
|
|
262
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return {
|
|
267
|
+
...output,
|
|
268
|
+
stopReason: "error" as const,
|
|
269
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
132
274
|
function providerConfig(
|
|
133
275
|
apiKey: string,
|
|
134
276
|
models: RouterModel[],
|
|
@@ -141,6 +283,15 @@ function providerConfig(
|
|
|
141
283
|
api: "openai-completions",
|
|
142
284
|
headers: { "User-Agent": "pi-coding-agent" },
|
|
143
285
|
models: models.map(toModelConfig(devIndex)),
|
|
286
|
+
classifiers: {
|
|
287
|
+
"typesafe-system-one": {
|
|
288
|
+
classify: (
|
|
289
|
+
model: Parameters<typeof classifySystemOne>[0],
|
|
290
|
+
context: Parameters<typeof classifySystemOne>[1],
|
|
291
|
+
options?: Parameters<typeof classifySystemOne>[3],
|
|
292
|
+
) => classifySystemOne(model, context, apiKey, options),
|
|
293
|
+
},
|
|
294
|
+
},
|
|
144
295
|
// Live fetch on /model refresh — bypasses the disk cache.
|
|
145
296
|
// Pi calls refreshModels once per provider at every startup with
|
|
146
297
|
// allowNetwork:false (awaited, no timeout) — never touch the network
|