auto-model-router 0.2.0 → 0.2.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/.omp-plugin/marketplace.json +2 -2
- package/package.json +1 -1
- package/src/router/classify.ts +16 -3
- package/test/classify.test.ts +59 -0
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "auto-model-router: a local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
|
|
10
|
-
"version": "0.2.
|
|
10
|
+
"version": "0.2.1",
|
|
11
11
|
"pluginRoot": "."
|
|
12
12
|
},
|
|
13
13
|
"plugins": [
|
|
14
14
|
{
|
|
15
15
|
"name": "auto-model-router",
|
|
16
16
|
"description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter. Runs in-process, routes per turn by price and task complexity, with budget caps, mid-stream escalation, and cache-aware hysteresis.",
|
|
17
|
-
"version": "0.2.
|
|
17
|
+
"version": "0.2.1",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "drewappling",
|
|
20
20
|
"email": "drewappling@gmail.com"
|
package/package.json
CHANGED
package/src/router/classify.ts
CHANGED
|
@@ -43,9 +43,18 @@ const W_LARGE_CONTENT = 0.05;
|
|
|
43
43
|
const W_HUGE_CONTENT = 0.1;
|
|
44
44
|
const W_TURN_DEPTH = 0.004; // long conversations accumulate entangled context
|
|
45
45
|
const CAP_TURN_DEPTH = 0.08;
|
|
46
|
-
const W_LOOP_DEPTH = -0.008; // deep
|
|
46
|
+
const W_LOOP_DEPTH = -0.008; // a single deep step is mechanical
|
|
47
47
|
const CAP_LOOP_DEPTH = -0.06;
|
|
48
|
-
|
|
48
|
+
// A sustained autonomous loop is the signal the underlying task is substantial:
|
|
49
|
+
// the agent keeps grinding without human input. Unlike the mechanical-step
|
|
50
|
+
// penalty above, this term ACCUMULATES with depth past the agentic threshold so
|
|
51
|
+
// long/hard loops climb out of trivial instead of scoring like a one-line edit.
|
|
52
|
+
// Capped so pure depth tops out in `simple` (a competent-but-cheap model);
|
|
53
|
+
// reaching `moderate`/`hard` still requires real complexity signals (tool
|
|
54
|
+
// failure, repeated call, keywords) to stack on top.
|
|
55
|
+
const W_AUTONOMOUS_LOOP = 0.06; // base bonus at the threshold
|
|
56
|
+
const W_AUTONOMOUS_LOOP_PER_DEPTH = 0.018; // added per loop step beyond the threshold
|
|
57
|
+
const CAP_AUTONOMOUS_LOOP = 0.34;
|
|
49
58
|
const W_IMAGES = 0.04;
|
|
50
59
|
const W_TOOLS_OFFERED = 0.03;
|
|
51
60
|
|
|
@@ -100,7 +109,11 @@ export function scoreHeuristic(f: Features, cfg: RouterConfig): Classification {
|
|
|
100
109
|
add(Math.min(f.turnDepth * W_TURN_DEPTH, CAP_TURN_DEPTH), `conversation depth ${f.turnDepth}`);
|
|
101
110
|
add(Math.max(f.toolLoopDepth * W_LOOP_DEPTH, CAP_LOOP_DEPTH), `tool loop depth ${f.toolLoopDepth}`);
|
|
102
111
|
if (f.toolLoopDepth >= cfg.classifier.agenticLoopDepth && f.toolLoopDepth > 0) {
|
|
103
|
-
|
|
112
|
+
const excessDepth = f.toolLoopDepth - cfg.classifier.agenticLoopDepth;
|
|
113
|
+
add(
|
|
114
|
+
Math.min(W_AUTONOMOUS_LOOP + excessDepth * W_AUTONOMOUS_LOOP_PER_DEPTH, CAP_AUTONOMOUS_LOOP),
|
|
115
|
+
`autonomous loop depth ${f.toolLoopDepth} (sustained task)`,
|
|
116
|
+
);
|
|
104
117
|
}
|
|
105
118
|
if (f.hasImages) add(W_IMAGES, "image input");
|
|
106
119
|
if (f.toolCount > 0) add(W_TOOLS_OFFERED, `${f.toolCount} tools offered`);
|
package/test/classify.test.ts
CHANGED
|
@@ -37,6 +37,32 @@ function featuresFor(messages: unknown[], tools?: unknown[] | undefined): Featur
|
|
|
37
37
|
return extractFeatures(r, 5000);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** A tool-result continuation at a given autonomous-loop depth, no other signals. */
|
|
41
|
+
function contFeatures(toolLoopDepth: number, over: Partial<Features> = {}): Features {
|
|
42
|
+
return {
|
|
43
|
+
promptTokens: 30000,
|
|
44
|
+
newContentTokens: 200,
|
|
45
|
+
turnDepth: toolLoopDepth,
|
|
46
|
+
toolCount: 12,
|
|
47
|
+
toolSchemaBytes: 9783,
|
|
48
|
+
isToolResultContinuation: true,
|
|
49
|
+
toolLoopDepth,
|
|
50
|
+
distinctToolsUsed: 3,
|
|
51
|
+
lastToolFailed: false,
|
|
52
|
+
repeatedToolCall: false,
|
|
53
|
+
hasImages: false,
|
|
54
|
+
codeBlocks: 0,
|
|
55
|
+
codeBytes: 0,
|
|
56
|
+
looksLikeDiff: false,
|
|
57
|
+
complexityKeywords: [],
|
|
58
|
+
trivialityKeywords: [],
|
|
59
|
+
requestedReasoning: undefined,
|
|
60
|
+
questionCount: 0,
|
|
61
|
+
isTerseInstruction: false,
|
|
62
|
+
...over,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
40
66
|
const SYSTEM = { role: "system", content: "You are a coding agent." };
|
|
41
67
|
|
|
42
68
|
/** Upstream double that fails loudly if the adjudicator is consulted. */
|
|
@@ -160,6 +186,39 @@ describe("scoreHeuristic", () => {
|
|
|
160
186
|
expect(c.confidence).toBeGreaterThanOrEqual(0);
|
|
161
187
|
expect(c.confidence).toBeLessThanOrEqual(1);
|
|
162
188
|
});
|
|
189
|
+
|
|
190
|
+
test("a shallow tool-result continuation stays trivial", () => {
|
|
191
|
+
const shallow = scoreHeuristic(contFeatures(2), BASE);
|
|
192
|
+
expect(shallow.tier).toBe("trivial");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("a sustained autonomous loop climbs out of trivial", () => {
|
|
196
|
+
// The failure mode this fixes: a long coding loop pinned to the cheapest
|
|
197
|
+
// tier for dozens of turns because agentic complexity never accumulated.
|
|
198
|
+
const shallow = scoreHeuristic(contFeatures(2), BASE);
|
|
199
|
+
const deep = scoreHeuristic(contFeatures(20), BASE);
|
|
200
|
+
expect(tierIdx(deep.tier)).toBeGreaterThan(tierIdx(shallow.tier));
|
|
201
|
+
expect(deep.tier).not.toBe("trivial");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("score increases monotonically with loop depth past the agentic threshold", () => {
|
|
205
|
+
const depths = [4, 6, 8, 10, 15, 20, 30];
|
|
206
|
+
let prev = -1;
|
|
207
|
+
for (const d of depths) {
|
|
208
|
+
const s = scoreHeuristic(contFeatures(d), BASE).score;
|
|
209
|
+
expect(s).toBeGreaterThanOrEqual(prev);
|
|
210
|
+
prev = s;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("pure loop depth tops out at simple; complexity signals stack to moderate", () => {
|
|
215
|
+
// Depth alone means competent-but-cheap (simple), not a frontier model.
|
|
216
|
+
const veryDeep = scoreHeuristic(contFeatures(90), BASE);
|
|
217
|
+
expect(tierIdx(veryDeep.tier)).toBeLessThanOrEqual(tierIdx("simple"));
|
|
218
|
+
// A failing tool result on top of a deep loop is genuinely hard.
|
|
219
|
+
const deepAndFailing = scoreHeuristic(contFeatures(20, { lastToolFailed: true }), BASE);
|
|
220
|
+
expect(tierIdx(deepAndFailing.tier)).toBeGreaterThanOrEqual(tierIdx("moderate"));
|
|
221
|
+
});
|
|
163
222
|
});
|
|
164
223
|
|
|
165
224
|
describe("pickQualityAxis", () => {
|