@xynogen/pix-9router 0.7.0 → 0.8.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.
- package/README.md +16 -2
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/provider.ts +152 -4
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.0",
|
|
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.12.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),
|
|
@@ -129,6 +165,109 @@ export default async function registerProvider(pi: ExtensionAPI): Promise<void>
|
|
|
129
165
|
.catch(() => {});
|
|
130
166
|
}
|
|
131
167
|
|
|
168
|
+
export async function classifySystemOne(
|
|
169
|
+
model: { id: string; baseUrl?: string; api?: string; provider?: string },
|
|
170
|
+
context: { state: Record<string, unknown>; questions: Record<string, any> },
|
|
171
|
+
apiKey: string,
|
|
172
|
+
options?: { signal?: AbortSignal; headers?: Record<string, string> },
|
|
173
|
+
): Promise<ClassifierResult> {
|
|
174
|
+
const output = {
|
|
175
|
+
api: model.api ?? "typesafe-system-one",
|
|
176
|
+
provider: model.provider ?? "9router",
|
|
177
|
+
model: model.id,
|
|
178
|
+
answers: {} as Record<string, any>,
|
|
179
|
+
stopReason: "stop" as const,
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const wireQuestions = Object.fromEntries(
|
|
185
|
+
Object.entries(context.questions ?? {}).map(([key, q]) => [
|
|
186
|
+
key,
|
|
187
|
+
q.type === "bool" ? { ...q, type: "noul" } : q,
|
|
188
|
+
]),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const base = (model.baseUrl || routerBaseUrl()).replace(/\/+$/, "");
|
|
192
|
+
const url = `${base}/systemone`;
|
|
193
|
+
const res = await fetch(url, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
signal: ioTimeoutSignal(options?.signal),
|
|
196
|
+
headers: {
|
|
197
|
+
Authorization: `Bearer ${apiKey}`,
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"User-Agent": "pi-coding-agent",
|
|
200
|
+
...(options?.headers ?? {}),
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
model: model.id,
|
|
204
|
+
state: context.state,
|
|
205
|
+
questions: wireQuestions,
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!res.ok) {
|
|
210
|
+
const errText = await res.text().catch(() => "");
|
|
211
|
+
return {
|
|
212
|
+
...output,
|
|
213
|
+
stopReason: "error" as const,
|
|
214
|
+
errorMessage: `9router /systemone returned ${res.status}: ${errText}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const data = (await res.json()) as any;
|
|
219
|
+
const answers: Record<string, any> = {};
|
|
220
|
+
|
|
221
|
+
for (const [key, q] of Object.entries(context.questions ?? {})) {
|
|
222
|
+
const ans = data?.answers?.[key];
|
|
223
|
+
if (!ans) continue;
|
|
224
|
+
if (q.type === "bool") {
|
|
225
|
+
answers[key] = {
|
|
226
|
+
type: "bool",
|
|
227
|
+
probability: typeof ans.noul === "number" ? ans.noul : (ans.probability ?? 0),
|
|
228
|
+
};
|
|
229
|
+
} else if (q.type === "choice") {
|
|
230
|
+
answers[key] = {
|
|
231
|
+
type: "choice",
|
|
232
|
+
choice: ans.choice,
|
|
233
|
+
probabilities: ans.probabilities ?? {},
|
|
234
|
+
confidence: ans.confidence ?? 0,
|
|
235
|
+
};
|
|
236
|
+
} else if (q.type === "score") {
|
|
237
|
+
answers[key] = {
|
|
238
|
+
type: "score",
|
|
239
|
+
score: ans.score ?? 0,
|
|
240
|
+
confidence: ans.confidence ?? 0,
|
|
241
|
+
};
|
|
242
|
+
} else {
|
|
243
|
+
answers[key] = ans;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const inputTokens = data?.usage?.input_tokens ?? 0;
|
|
248
|
+
const outputTokens = data?.usage?.output_tokens ?? 0;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
...output,
|
|
252
|
+
answers,
|
|
253
|
+
usage: {
|
|
254
|
+
input: inputTokens,
|
|
255
|
+
output: outputTokens,
|
|
256
|
+
cacheRead: 0,
|
|
257
|
+
cacheWrite: 0,
|
|
258
|
+
totalTokens: inputTokens + outputTokens,
|
|
259
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return {
|
|
264
|
+
...output,
|
|
265
|
+
stopReason: "error" as const,
|
|
266
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
132
271
|
function providerConfig(
|
|
133
272
|
apiKey: string,
|
|
134
273
|
models: RouterModel[],
|
|
@@ -141,6 +280,15 @@ function providerConfig(
|
|
|
141
280
|
api: "openai-completions",
|
|
142
281
|
headers: { "User-Agent": "pi-coding-agent" },
|
|
143
282
|
models: models.map(toModelConfig(devIndex)),
|
|
283
|
+
classifiers: {
|
|
284
|
+
"typesafe-system-one": {
|
|
285
|
+
classify: (
|
|
286
|
+
model: Parameters<typeof classifySystemOne>[0],
|
|
287
|
+
context: Parameters<typeof classifySystemOne>[1],
|
|
288
|
+
options?: Parameters<typeof classifySystemOne>[3],
|
|
289
|
+
) => classifySystemOne(model, context, apiKey, options),
|
|
290
|
+
},
|
|
291
|
+
},
|
|
144
292
|
// Live fetch on /model refresh — bypasses the disk cache.
|
|
145
293
|
// Pi calls refreshModels once per provider at every startup with
|
|
146
294
|
// allowNetwork:false (awaited, no timeout) — never touch the network
|