auto-model-router 0.2.32 → 0.2.34
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/config/defaults.ts +5 -2
- package/src/config/schema.ts +1 -0
- package/src/config/types.ts +7 -0
- package/src/router/classify.ts +32 -2
- package/test/classify.test.ts +40 -5
- package/test/failover.test.ts +1 -0
- package/test/turn.test.ts +1 -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.34",
|
|
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.34",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "drewappling",
|
|
20
20
|
"email": "drewappling@gmail.com"
|
package/package.json
CHANGED
package/src/config/defaults.ts
CHANGED
|
@@ -101,8 +101,11 @@ export const DEFAULT_CONFIG: RouterConfig = {
|
|
|
101
101
|
toolAxis: "coding",
|
|
102
102
|
chatAxis: "intelligence",
|
|
103
103
|
agenticLoopDepth: 3,
|
|
104
|
-
//
|
|
105
|
-
//
|
|
104
|
+
// A mechanical retry (failed tool call + tool-result continuation) keeps
|
|
105
|
+
// only a fifth of the +0.26; a user-visible failure keeps the full weight.
|
|
106
|
+
mechanicalRetryFactor: 0.2,
|
|
107
|
+
// Shipped reasoning values, unchanged. See ClassifierConfig.reasoningWeights:
|
|
108
|
+
// a harness that pins the level for a whole session turns these into a
|
|
106
109
|
// constant tier offset, in which case `medium` belongs near 0.
|
|
107
110
|
reasoningWeights: { medium: 0.14, high: 0.24, xhigh: 0.3, max: 0.34 },
|
|
108
111
|
},
|
package/src/config/schema.ts
CHANGED
|
@@ -86,6 +86,7 @@ const classifier = z.strictObject({
|
|
|
86
86
|
toolAxis: qualityAxis.optional(),
|
|
87
87
|
chatAxis: qualityAxis.optional(),
|
|
88
88
|
agenticLoopDepth: z.number().int().nonnegative().optional(),
|
|
89
|
+
mechanicalRetryFactor: z.number().min(0).max(1).optional(),
|
|
89
90
|
reasoningWeights: z
|
|
90
91
|
.strictObject({
|
|
91
92
|
medium: z.number().nonnegative().optional(),
|
package/src/config/types.ts
CHANGED
|
@@ -242,6 +242,13 @@ export interface ClassifierConfig {
|
|
|
242
242
|
chatAxis: QualityAxis;
|
|
243
243
|
/** Tool-loop depth above which the agentic axis takes over. */
|
|
244
244
|
agenticLoopDepth: number;
|
|
245
|
+
/**
|
|
246
|
+
* Fraction of the failed-tool weight that survives when the turn is a
|
|
247
|
+
* mechanical tool-result continuation. A retry after a failed tool call is
|
|
248
|
+
* the most mechanical turn there is; the flat weight let automated retry
|
|
249
|
+
* loops buy the hard tier. 1 preserves the shipped behaviour.
|
|
250
|
+
*/
|
|
251
|
+
mechanicalRetryFactor: number;
|
|
245
252
|
/**
|
|
246
253
|
* Score added when the CLIENT asks for a reasoning effort, per level. The
|
|
247
254
|
* premise is that asking for reasoning states expected difficulty directly.
|
package/src/router/classify.ts
CHANGED
|
@@ -118,8 +118,38 @@ export function scoreHeuristic(f: Features, cfg: RouterConfig): Classification {
|
|
|
118
118
|
Math.max(f.trivialityKeywords.length * W_TRIVIALITY_KEYWORD, CAP_TRIVIALITY),
|
|
119
119
|
`triviality keywords [${f.trivialityKeywords.join(", ")}]`,
|
|
120
120
|
);
|
|
121
|
-
if (f.lastToolFailed)
|
|
122
|
-
|
|
121
|
+
if (f.lastToolFailed) {
|
|
122
|
+
// A retry after a failed tool call is the MOST mechanical turn there is:
|
|
123
|
+
// no new user intent, same prompt prefix, the harness just re-asks. The
|
|
124
|
+
// flat +0.26 let an automated retry loop buy the hard tier ($7.02 of one
|
|
125
|
+
// measured day vs $0.19 for the same rows as moderate picks). A
|
|
126
|
+
// continuation keeps only a small nudge; a genuine user-visible failure
|
|
127
|
+
// (NOT a tool-result continuation) keeps the full weight.
|
|
128
|
+
const failed = f.isToolResultContinuation ? W_TOOL_FAILED * cfg.classifier.mechanicalRetryFactor : W_TOOL_FAILED;
|
|
129
|
+
add(
|
|
130
|
+
failed,
|
|
131
|
+
f.isToolResultContinuation
|
|
132
|
+
? `last tool result failed (mechanical retry, damped x${cfg.classifier.mechanicalRetryFactor})`
|
|
133
|
+
: "last tool result failed",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (f.circularToolCall) {
|
|
137
|
+
// Same logic as the failed-tool clamp above, measured separately: a hard
|
|
138
|
+
// escalation driven by a circular call NEVER shortened the loop (chains
|
|
139
|
+
// starting at hard: mean 5.74 turns; chains starting at moderate: mean
|
|
140
|
+
// 5.74 — identical). The loop ends when the underlying state changes,
|
|
141
|
+
// not because a pricier model re-read the same tool result. On a
|
|
142
|
+
// mechanical continuation the circular flag is a stuck retry, not novel
|
|
143
|
+
// difficulty, so it takes the same damping as the failed-tool bonus;
|
|
144
|
+
// off a continuation (fresh user turn) it keeps full weight.
|
|
145
|
+
const circ = f.isToolResultContinuation ? W_CIRCULAR_LOOP * cfg.classifier.mechanicalRetryFactor : W_CIRCULAR_LOOP;
|
|
146
|
+
add(
|
|
147
|
+
circ,
|
|
148
|
+
f.isToolResultContinuation
|
|
149
|
+
? `circular tool call (mechanical retry, damped x${cfg.classifier.mechanicalRetryFactor})`
|
|
150
|
+
: "circular tool call (re-issued a prior call; stuck)",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
123
153
|
const rw = reasoningWeight(f.requestedReasoning, cfg);
|
|
124
154
|
if (rw > 0) add(rw, `client requested reasoning=${f.requestedReasoning ?? ""}`);
|
|
125
155
|
if (f.isTerseInstruction) add(W_TERSE, "terse instruction");
|
package/test/classify.test.ts
CHANGED
|
@@ -263,15 +263,50 @@ describe("scoreHeuristic", () => {
|
|
|
263
263
|
expect(scoreHeuristic(contFeatures(400), BASE).tier).toBe("moderate");
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
test("a circular tool call on a
|
|
267
|
-
//
|
|
268
|
-
|
|
266
|
+
test("a circular tool call on a FRESH turn escalates to hard", () => {
|
|
267
|
+
// Off a continuation the stuck signal keeps full weight: the user is
|
|
268
|
+
// watching a live loop and a pricier model may actually break it.
|
|
269
|
+
const deepCircular = scoreHeuristic(
|
|
270
|
+
{ ...contFeatures(90, { circularToolCall: true }), isToolResultContinuation: false },
|
|
271
|
+
BASE,
|
|
272
|
+
);
|
|
269
273
|
expect(deepCircular.tier).toBe("hard");
|
|
270
274
|
});
|
|
271
275
|
|
|
272
|
-
test("a
|
|
276
|
+
test("a circular tool call on a mechanical continuation is damped, not hard", () => {
|
|
277
|
+
// Measured: hard escalations on circular calls never shortened the loop
|
|
278
|
+
// (chain means identical, 5.74 turns, hard vs moderate). 22 of 27 such
|
|
279
|
+
// hard turns were mechanical continuations paying up to 6x for nothing.
|
|
280
|
+
const retry = scoreHeuristic(contFeatures(90, { circularToolCall: true }), BASE);
|
|
281
|
+
const plain = scoreHeuristic(contFeatures(90), BASE);
|
|
282
|
+
expect(tierIdx(retry.tier)).toBeLessThanOrEqual(tierIdx("moderate"));
|
|
283
|
+
expect(retry.score).toBeLessThan(plain.score + 0.24);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("a failing tool result on a deep loop is at least simple", () => {
|
|
287
|
+
// Was 'at least moderate' before the mechanical-retry damp: the flat
|
|
288
|
+
// +0.26 pushed deep mechanical retry loops into hard. A damped retry
|
|
289
|
+
// still clears trivial.
|
|
273
290
|
const deepAndFailing = scoreHeuristic(contFeatures(20, { lastToolFailed: true }), BASE);
|
|
274
|
-
expect(tierIdx(deepAndFailing.tier)).toBeGreaterThanOrEqual(tierIdx("
|
|
291
|
+
expect(tierIdx(deepAndFailing.tier)).toBeGreaterThanOrEqual(tierIdx("simple"));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("a failed-tool retry on a mechanical continuation is damped, not hard", () => {
|
|
295
|
+
// A retry after a failed tool call is the most mechanical turn there is;
|
|
296
|
+
// the flat +0.26 let automated retry loops buy the hard tier ($7.02 of one
|
|
297
|
+
// measured day vs $0.19 for the same rows as moderate picks). The
|
|
298
|
+
// continuation keeps only mechanicalRetryFactor of the weight.
|
|
299
|
+
const retry = scoreHeuristic(contFeatures(20, { lastToolFailed: true }), BASE);
|
|
300
|
+
const quiet = scoreHeuristic(contFeatures(20), BASE);
|
|
301
|
+
expect(tierIdx(retry.tier)).toBeLessThan(tierIdx("hard"));
|
|
302
|
+
expect(retry.score - quiet.score).toBeCloseTo(
|
|
303
|
+
BASE.classifier.mechanicalRetryFactor * 0.26,
|
|
304
|
+
5,
|
|
305
|
+
);
|
|
306
|
+
// A failure the USER sees (not a tool-result continuation) keeps the full
|
|
307
|
+
// weight: that genuinely changes what the turn needs.
|
|
308
|
+
const userSeen = scoreHeuristic(contFeatures(2, { isToolResultContinuation: false, lastToolFailed: true }), BASE);
|
|
309
|
+
expect(userSeen.score - scoreHeuristic(contFeatures(2, { isToolResultContinuation: false }), BASE).score).toBeCloseTo(0.26, 5);
|
|
275
310
|
});
|
|
276
311
|
});
|
|
277
312
|
|
package/test/failover.test.ts
CHANGED
|
@@ -55,6 +55,7 @@ function mkConfig(escalation: Partial<EscalationConfig> = {}): RouterConfig {
|
|
|
55
55
|
toolAxis: "coding",
|
|
56
56
|
chatAxis: "intelligence",
|
|
57
57
|
agenticLoopDepth: 3,
|
|
58
|
+
mechanicalRetryFactor: 0.2,
|
|
58
59
|
reasoningWeights: { medium: 0.14, high: 0.24, xhigh: 0.3, max: 0.34 },
|
|
59
60
|
},
|
|
60
61
|
escalation: {
|
package/test/turn.test.ts
CHANGED
|
@@ -56,6 +56,7 @@ function mkConfig(escalation: Partial<EscalationConfig> = {}): RouterConfig {
|
|
|
56
56
|
toolAxis: "coding",
|
|
57
57
|
chatAxis: "intelligence",
|
|
58
58
|
agenticLoopDepth: 3,
|
|
59
|
+
mechanicalRetryFactor: 0.2,
|
|
59
60
|
reasoningWeights: { medium: 0.14, high: 0.24, xhigh: 0.3, max: 0.34 },
|
|
60
61
|
},
|
|
61
62
|
escalation: {
|