@ejstembler/pi-classifier-router 1.0.0 → 1.0.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 +37 -7
- package/examples/class-router.yml +44 -0
- package/package.json +1 -1
- package/src/config.ts +71 -16
package/README.md
CHANGED
|
@@ -260,17 +260,47 @@ so it is not a drop-in accuracy replacement for a hosted Jev model.
|
|
|
260
260
|
|
|
261
261
|
## Configuration
|
|
262
262
|
|
|
263
|
-
Config files are
|
|
263
|
+
Config files are searched in this order, project before global and JSON before
|
|
264
|
+
YAML:
|
|
264
265
|
|
|
265
266
|
1. `<project>/.omp/class-router.json`
|
|
266
|
-
2.
|
|
267
|
+
2. `<project>/.omp/class-router.yml`
|
|
268
|
+
3. `<project>/.omp/class-router.yaml`
|
|
269
|
+
4. `~/.omp/class-router.json`
|
|
270
|
+
5. `~/.omp/class-router.yml`
|
|
271
|
+
6. `~/.omp/class-router.yaml`
|
|
272
|
+
|
|
273
|
+
Both formats describe exactly the same configuration, so use whichever you
|
|
274
|
+
prefer:
|
|
275
|
+
|
|
276
|
+
```yaml
|
|
277
|
+
# ~/.omp/class-router.yml
|
|
278
|
+
backend: jev
|
|
279
|
+
routing:
|
|
280
|
+
confidenceThreshold: 0.7
|
|
281
|
+
modelMapping:
|
|
282
|
+
hard: @slow
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{ "backend": "jev", "routing": { "confidenceThreshold": 0.7, "modelMapping": { "hard": "@slow" } } }
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
YAML is parsed by the host (`Bun.YAML`), which is free because pi and omp are both
|
|
290
|
+
Bun. A YAML file on a host without a parser is reported as an error rather than
|
|
291
|
+
silently skipped, so a `.yml` that is being ignored is always visible.
|
|
267
292
|
|
|
268
293
|
These paths are literal and identical on both hosts: the extension discovers its
|
|
269
|
-
own config
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
294
|
+
own config and never reads host settings. In particular this file is **not**
|
|
295
|
+
omp's `~/.omp/agent/config.yml` (that is omp's own settings: `modelRoles`, theme,
|
|
296
|
+
advisor) and not pi's `~/.pi/agent/settings.json`. They sit alongside each other
|
|
297
|
+
and are unrelated.
|
|
298
|
+
|
|
299
|
+
The first existing file that parses and validates wins; a project file therefore
|
|
300
|
+
overrides a global one, and a `.json` overrides a `.yml` in the same scope. A
|
|
301
|
+
missing file everywhere is not an error: built-in defaults apply. A file that
|
|
302
|
+
cannot be read or parsed is reported and the search continues; a file that parses
|
|
303
|
+
but violates the contract is rejected outright
|
|
274
304
|
(routing falls back to defaults and the errors are notified), because silent
|
|
275
305
|
mis-routing is worse than leaving the model alone.
|
|
276
306
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# YAML form of the configuration. Identical in meaning to class-router.json;
|
|
2
|
+
# use whichever you prefer. Copy to ~/.omp/class-router.yml (global) or
|
|
3
|
+
# <project>/.omp/class-router.yml (project-local, which wins).
|
|
4
|
+
#
|
|
5
|
+
# This is the extension's own config file, unrelated to omp's
|
|
6
|
+
# ~/.omp/agent/config.yml or pi's ~/.pi/agent/settings.json.
|
|
7
|
+
|
|
8
|
+
backend: jev
|
|
9
|
+
|
|
10
|
+
jev:
|
|
11
|
+
endpoint: https://api.typesafe.ai/v1/systemone
|
|
12
|
+
model: jev-latest
|
|
13
|
+
apiKeyEnvVar: TYPESAFE_API_KEY
|
|
14
|
+
timeoutMs: 3000
|
|
15
|
+
|
|
16
|
+
routing:
|
|
17
|
+
# Must name a choice question: score and noul answers cannot name a category.
|
|
18
|
+
primaryQuestion: task_complexity
|
|
19
|
+
# Alias specs are omp-only; on pi use concrete provider/id specs, e.g.
|
|
20
|
+
# fireworks/accounts/fireworks/routers/deepseek-pro-latest
|
|
21
|
+
modelMapping:
|
|
22
|
+
trivial: "@smol"
|
|
23
|
+
standard: "@default"
|
|
24
|
+
hard: "@slow"
|
|
25
|
+
fallbackChains:
|
|
26
|
+
"@smol":
|
|
27
|
+
- "@smol"
|
|
28
|
+
- "@default"
|
|
29
|
+
"@default":
|
|
30
|
+
- "@default"
|
|
31
|
+
- "@slow"
|
|
32
|
+
"@slow":
|
|
33
|
+
- "@slow"
|
|
34
|
+
- "@default"
|
|
35
|
+
confidenceThreshold: 0.5
|
|
36
|
+
defaultCategory: standard
|
|
37
|
+
|
|
38
|
+
circuitBreaker:
|
|
39
|
+
failureThreshold: 3
|
|
40
|
+
cooldownMs: 120000
|
|
41
|
+
halfOpenMaxTrials: 1
|
|
42
|
+
|
|
43
|
+
notify: true
|
|
44
|
+
applyTo: all
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Config discovery, defaults, merge, and validation.
|
|
3
3
|
*
|
|
4
|
-
* Precedence is project over global;
|
|
5
|
-
* wins. A file that fails to read or
|
|
6
|
-
* that parses yet violates the
|
|
7
|
-
* mis-routing is worse than falling back
|
|
4
|
+
* Precedence is project over global; within a scope JSON is preferred over YAML.
|
|
5
|
+
* The first existing, parseable, valid file wins. A file that fails to read or
|
|
6
|
+
* parse is reported and skipped, but a file that parses yet violates the
|
|
7
|
+
* contract is rejected outright: silently mis-routing is worse than falling back
|
|
8
|
+
* to the session's own model.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import * as fs from "node:fs";
|
|
@@ -13,9 +14,12 @@ import * as path from "node:path";
|
|
|
13
14
|
|
|
14
15
|
import type { Question, RouterConfig, RouterConfigInput } from "./types.ts";
|
|
15
16
|
|
|
17
|
+
export type ConfigFormat = "json" | "yaml";
|
|
18
|
+
|
|
16
19
|
export interface ConfigSource {
|
|
17
20
|
path: string;
|
|
18
21
|
scope: "project" | "global";
|
|
22
|
+
format: ConfigFormat;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export interface ConfigLoadResult {
|
|
@@ -24,12 +28,45 @@ export interface ConfigLoadResult {
|
|
|
24
28
|
errors: string[];
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
/**
|
|
31
|
+
/** Parses one YAML document. Supplied by the host, since only Bun has one. */
|
|
32
|
+
export type YamlParser = (text: string) => unknown;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Bun ships a YAML parser; Node does not. Absent a parser, YAML candidates are
|
|
36
|
+
* reported as errors rather than silently ignored, so a `.yml` config that is
|
|
37
|
+
* being skipped is visible.
|
|
38
|
+
*/
|
|
39
|
+
export function defaultYamlParser(): YamlParser | null {
|
|
40
|
+
const scope: unknown = globalThis;
|
|
41
|
+
if (typeof scope !== "object" || scope === null || !("Bun" in scope)) return null;
|
|
42
|
+
const bun = scope.Bun;
|
|
43
|
+
if (typeof bun !== "object" || bun === null || !("YAML" in bun)) return null;
|
|
44
|
+
const yaml = bun.YAML;
|
|
45
|
+
if (typeof yaml !== "object" || yaml === null || !("parse" in yaml)) return null;
|
|
46
|
+
const parse = yaml.parse;
|
|
47
|
+
if (typeof parse !== "function") return null;
|
|
48
|
+
// Verified callable just above; only its signature is unknowable statically.
|
|
49
|
+
const call = parse as (value: string) => unknown;
|
|
50
|
+
return (text: string) => call(text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Candidate files per scope, in precedence order. */
|
|
54
|
+
const CANDIDATES: ReadonlyArray<{ name: string; format: ConfigFormat }> = [
|
|
55
|
+
{ name: "class-router.json", format: "json" },
|
|
56
|
+
{ name: "class-router.yml", format: "yaml" },
|
|
57
|
+
{ name: "class-router.yaml", format: "yaml" },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** Every candidate, project first, JSON before YAML, then global. */
|
|
28
61
|
export function configPaths(cwd: string, home: string): ConfigSource[] {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
62
|
+
const sources: ConfigSource[] = [];
|
|
63
|
+
for (const scope of ["project", "global"] as const) {
|
|
64
|
+
const base = scope === "project" ? cwd : home;
|
|
65
|
+
for (const candidate of CANDIDATES) {
|
|
66
|
+
sources.push({ path: path.join(base, ".omp", candidate.name), scope, format: candidate.format });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return sources;
|
|
33
70
|
}
|
|
34
71
|
|
|
35
72
|
/** A complete, valid configuration. Fresh objects on every call. */
|
|
@@ -242,6 +279,7 @@ export function loadConfig(
|
|
|
242
279
|
cwd: string,
|
|
243
280
|
home: string = os.homedir(),
|
|
244
281
|
env: Record<string, string | undefined> = process.env,
|
|
282
|
+
yaml: YamlParser | null = defaultYamlParser(),
|
|
245
283
|
): ConfigLoadResult {
|
|
246
284
|
void env;
|
|
247
285
|
const errors: string[] = [];
|
|
@@ -259,16 +297,33 @@ export function loadConfig(
|
|
|
259
297
|
}
|
|
260
298
|
|
|
261
299
|
let parsed: unknown;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
300
|
+
if (source.format === "yaml") {
|
|
301
|
+
if (yaml === null) {
|
|
302
|
+
errors.push(
|
|
303
|
+
`${source.scope} config ${source.path}: YAML config needs a YAML parser and this host has none ` +
|
|
304
|
+
`(only Bun provides one); use class-router.json instead`,
|
|
305
|
+
);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
parsed = yaml(raw);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
312
|
+
errors.push(`${source.scope} config ${source.path}: invalid YAML: ${message}`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
try {
|
|
317
|
+
parsed = JSON.parse(raw);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
320
|
+
errors.push(`${source.scope} config ${source.path}: invalid JSON: ${message}`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
268
323
|
}
|
|
269
324
|
|
|
270
325
|
if (!isPlainObject(parsed)) {
|
|
271
|
-
errors.push(`${source.scope} config ${source.path}: expected a JSON object at the top level`);
|
|
326
|
+
errors.push(`${source.scope} config ${source.path}: expected a ${source.format === "yaml" ? "YAML" : "JSON"} object at the top level`);
|
|
272
327
|
continue;
|
|
273
328
|
}
|
|
274
329
|
|