auto-model-router 0.4.4 → 0.4.6
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/.omp-plugin/marketplace.json +2 -2
- package/README.md +152 -0
- package/hermes-plugin/__init__.py +3 -0
- package/hermes-plugin/native/__init__.py +348 -0
- package/hermes-plugin/native/__pycache__/__init__.cpython-311.pyc +0 -0
- package/hermes-plugin/native/plugin.yaml +5 -0
- package/omp-extension/digest-logic.ts +9 -2
- package/omp-extension/pi-coding-agent.d.ts +6 -0
- package/omp-extension/router-switch.ts +100 -0
- package/omp-extension/switch-logic.ts +81 -0
- package/package.json +1 -1
- package/src/cli/config-wizard.ts +8 -0
- package/src/config/defaults.ts +20 -0
- package/src/config/schema.ts +8 -0
- package/src/config/types.ts +28 -0
- package/src/server/advise.ts +82 -0
- package/src/server/digest.ts +7 -1
- package/src/server/http.ts +28 -4
- package/src/wire/openai/request.ts +7 -0
- package/test/config-wizard.test.ts +2 -1
- package/test/failover.test.ts +2 -1
- package/test/harness-requests.test.ts +137 -0
- package/test/harness-switch.test.ts +59 -0
- package/test/turn.test.ts +2 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_CONFIG } from "../src/config/defaults.ts";
|
|
4
|
+
import { TIER_ORDER } from "../src/router/types.ts";
|
|
5
|
+
import { advise } from "../src/server/advise.ts";
|
|
6
|
+
import { decideSwitch, nativeModelFor, parseSwitchPolicy, type SwitchPolicy } from "../omp-extension/switch-logic.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Harness-side model switch: the router's prompt-only advice, and the
|
|
10
|
+
* extension's decision about moving omp's active model.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
describe("advise", () => {
|
|
14
|
+
test("classifies a prompt without a ledger or dispatch and reports the shape the extension reads", () => {
|
|
15
|
+
const a = advise(DEFAULT_CONFIG, null, { ompSessionId: "s", harnessId: "", text: "Redesign the routing pipeline so escalation and failover share one retry loop; consider cache costs and write the migration plan." });
|
|
16
|
+
expect(TIER_ORDER).toContain(a.tier);
|
|
17
|
+
expect(a.confidence).toBeGreaterThanOrEqual(0);
|
|
18
|
+
expect(a.confidence).toBeLessThanOrEqual(1);
|
|
19
|
+
expect(a.reasons.length).toBeGreaterThan(0);
|
|
20
|
+
expect(a.lastTier).toBeNull();
|
|
21
|
+
const terse = advise(DEFAULT_CONFIG, null, { ompSessionId: "", harnessId: "", text: "ok" });
|
|
22
|
+
expect(TIER_ORDER.indexOf(terse.tier)).toBeLessThanOrEqual(TIER_ORDER.indexOf(a.tier));
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("switch policy", () => {
|
|
27
|
+
test("parses defensively and maps a tier to the nearest configured tier at or below it", () => {
|
|
28
|
+
expect(parseSwitchPolicy(null).enabled).toBe(false);
|
|
29
|
+
const p = parseSwitchPolicy({ enabled: true, models: { moderate: "anthropic/claude-sonnet-5", hard: "anthropic/claude-opus-4-8", simple: "no-slash" }, minConfidence: 0.5 });
|
|
30
|
+
expect(p.models).toEqual({ moderate: "anthropic/claude-sonnet-5", hard: "anthropic/claude-opus-4-8" });
|
|
31
|
+
expect(nativeModelFor(p, "hard")).toBe("anthropic/claude-opus-4-8");
|
|
32
|
+
expect(nativeModelFor(p, "moderate")).toBe("anthropic/claude-sonnet-5");
|
|
33
|
+
expect(nativeModelFor(p, "simple")).toBeUndefined();
|
|
34
|
+
expect(nativeModelFor(p, "trivial")).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("decideSwitch", () => {
|
|
39
|
+
const policy: SwitchPolicy = { enabled: true, models: { hard: "anthropic/claude-opus-4-8" }, minConfidence: 0.6 };
|
|
40
|
+
const router = "auto-model-router/auto";
|
|
41
|
+
|
|
42
|
+
test("moves up from the router for confident hard work, back for lighter work, and never past a manual choice", () => {
|
|
43
|
+
const up = decideSwitch({ policy, advised: { tier: "hard", confidence: 0.8 }, active: router, activeIsRouter: true, switchedTo: null, returnTo: null });
|
|
44
|
+
expect(up).toMatchObject({ action: "up", model: "anthropic/claude-opus-4-8" });
|
|
45
|
+
// Low confidence: stay.
|
|
46
|
+
expect(decideSwitch({ policy, advised: { tier: "hard", confidence: 0.3 }, active: router, activeIsRouter: true, switchedTo: null, returnTo: null }).action).toBe("none");
|
|
47
|
+
// Unmapped tier while on the router: stay.
|
|
48
|
+
expect(decideSwitch({ policy, advised: { tier: "moderate", confidence: 0.9 }, active: router, activeIsRouter: true, switchedTo: null, returnTo: null }).action).toBe("none");
|
|
49
|
+
// On our own switch, hard again: already there.
|
|
50
|
+
expect(decideSwitch({ policy, advised: { tier: "hard", confidence: 0.9 }, active: "anthropic/claude-opus-4-8", activeIsRouter: false, switchedTo: "anthropic/claude-opus-4-8", returnTo: router }).action).toBe("none");
|
|
51
|
+
// On our own switch, lighter work: back to the router model we left.
|
|
52
|
+
expect(decideSwitch({ policy, advised: { tier: "simple", confidence: 0.9 }, active: "anthropic/claude-opus-4-8", activeIsRouter: false, switchedTo: "anthropic/claude-opus-4-8", returnTo: router })).toMatchObject({ action: "back", model: router });
|
|
53
|
+
// The user chose a model by hand: leave it alone whatever the advice.
|
|
54
|
+
expect(decideSwitch({ policy, advised: { tier: "hard", confidence: 0.9 }, active: "openai/gpt-5", activeIsRouter: false, switchedTo: null, returnTo: null }).action).toBe("none");
|
|
55
|
+
expect(decideSwitch({ policy, advised: { tier: "trivial", confidence: 0.9 }, active: "openai/gpt-5", activeIsRouter: false, switchedTo: "anthropic/claude-opus-4-8", returnTo: router }).action).toBe("none");
|
|
56
|
+
// Disabled: nothing moves.
|
|
57
|
+
expect(decideSwitch({ policy: { ...policy, enabled: false }, advised: { tier: "hard", confidence: 0.9 }, active: router, activeIsRouter: true, switchedTo: null, returnTo: null }).action).toBe("none");
|
|
58
|
+
});
|
|
59
|
+
});
|
package/test/turn.test.ts
CHANGED
|
@@ -77,7 +77,8 @@ function mkConfig(escalation: Partial<EscalationConfig> = {}): RouterConfig {
|
|
|
77
77
|
compaction: { enabled: false, budgetTokens: 40_000, floorRatio: 1, fitToWindow: true, protectRecentTurns: 4, maxToolResultBytes: 4_096, keepHeadBytes: 512, keepTailBytes: 512, elideSupersededReads: true, collapseDuplicateResults: true, replanGrowthRatio: 1, digestToolResults: false, digestMaxPerTurn: 2 },
|
|
78
78
|
budget: { onExceeded: "downgrade" },
|
|
79
79
|
report: { baselines: [], dailySummary: false },
|
|
80
|
-
|
|
80
|
+
harnessSwitch: { enabled: false, models: {}, minConfidence: 0.6 },
|
|
81
|
+
digest: { enabled: false, minBytes: 12_000, maxBytes: 400_000, tools: ["read"], fromTier: "moderate", tier: "simple", model: "", maxOutputTokens: 700, maxCostUsd: 0.02, timeoutMs: 25_000, toolAliases: {} },
|
|
81
82
|
profiles: [],
|
|
82
83
|
ledger: { path: ":memory:", blendWindowDays: 7, blendMinSamples: 20, fallbackBlend: { inputPerMtok: 1, outputPerMtok: 4 }, conversationTtlMs: 86_400_000 , retentionDays: 0,},
|
|
83
84
|
adaptiveTierFloors: true,
|