auto-model-router 0.2.32 → 0.2.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-model-router",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
4
4
  "private": false,
5
5
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
6
6
  "type": "module",
@@ -101,8 +101,11 @@ export const DEFAULT_CONFIG: RouterConfig = {
101
101
  toolAxis: "coding",
102
102
  chatAxis: "intelligence",
103
103
  agenticLoopDepth: 3,
104
- // Shipped values, unchanged. See ClassifierConfig.reasoningWeights: a
105
- // harness that pins the level for a whole session turns these into a
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
  },
@@ -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(),
@@ -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.
@@ -118,7 +118,21 @@ 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) add(W_TOOL_FAILED, "last tool result failed");
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
+ }
122
136
  if (f.circularToolCall) add(W_CIRCULAR_LOOP, "circular tool call (re-issued a prior call; stuck)");
123
137
  const rw = reasoningWeight(f.requestedReasoning, cfg);
124
138
  if (rw > 0) add(rw, `client requested reasoning=${f.requestedReasoning ?? ""}`);
@@ -269,9 +269,30 @@ describe("scoreHeuristic", () => {
269
269
  expect(deepCircular.tier).toBe("hard");
270
270
  });
271
271
 
272
- test("a failing tool result on a deep loop is at least moderate", () => {
272
+ test("a failing tool result on a deep loop is at least simple", () => {
273
+ // Was 'at least moderate' before the mechanical-retry damp: the flat
274
+ // +0.26 pushed deep mechanical retry loops into hard. A damped retry
275
+ // still clears trivial.
273
276
  const deepAndFailing = scoreHeuristic(contFeatures(20, { lastToolFailed: true }), BASE);
274
- expect(tierIdx(deepAndFailing.tier)).toBeGreaterThanOrEqual(tierIdx("moderate"));
277
+ expect(tierIdx(deepAndFailing.tier)).toBeGreaterThanOrEqual(tierIdx("simple"));
278
+ });
279
+
280
+ test("a failed-tool retry on a mechanical continuation is damped, not hard", () => {
281
+ // A retry after a failed tool call is the most mechanical turn there is;
282
+ // the flat +0.26 let automated retry loops buy the hard tier ($7.02 of one
283
+ // measured day vs $0.19 for the same rows as moderate picks). The
284
+ // continuation keeps only mechanicalRetryFactor of the weight.
285
+ const retry = scoreHeuristic(contFeatures(20, { lastToolFailed: true }), BASE);
286
+ const quiet = scoreHeuristic(contFeatures(20), BASE);
287
+ expect(tierIdx(retry.tier)).toBeLessThan(tierIdx("hard"));
288
+ expect(retry.score - quiet.score).toBeCloseTo(
289
+ BASE.classifier.mechanicalRetryFactor * 0.26,
290
+ 5,
291
+ );
292
+ // A failure the USER sees (not a tool-result continuation) keeps the full
293
+ // weight: that genuinely changes what the turn needs.
294
+ const userSeen = scoreHeuristic(contFeatures(2, { isToolResultContinuation: false, lastToolFailed: true }), BASE);
295
+ expect(userSeen.score - scoreHeuristic(contFeatures(2, { isToolResultContinuation: false }), BASE).score).toBeCloseTo(0.26, 5);
275
296
  });
276
297
  });
277
298
 
@@ -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: {