auto-model-router 0.2.33 → 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.
@@ -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.32",
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.32",
17
+ "version": "0.2.34",
18
18
  "author": {
19
19
  "name": "drewappling",
20
20
  "email": "drewappling@gmail.com"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-model-router",
3
- "version": "0.2.33",
3
+ "version": "0.2.34",
4
4
  "private": false,
5
5
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
6
6
  "type": "module",
@@ -133,7 +133,23 @@ export function scoreHeuristic(f: Features, cfg: RouterConfig): Classification {
133
133
  : "last tool result failed",
134
134
  );
135
135
  }
136
- if (f.circularToolCall) add(W_CIRCULAR_LOOP, "circular tool call (re-issued a prior call; stuck)");
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
+ }
137
153
  const rw = reasoningWeight(f.requestedReasoning, cfg);
138
154
  if (rw > 0) add(rw, `client requested reasoning=${f.requestedReasoning ?? ""}`);
139
155
  if (f.isTerseInstruction) add(W_TERSE, "terse instruction");
@@ -263,12 +263,26 @@ describe("scoreHeuristic", () => {
263
263
  expect(scoreHeuristic(contFeatures(400), BASE).tier).toBe("moderate");
264
264
  });
265
265
 
266
- test("a circular tool call on a deep loop escalates to hard", () => {
267
- // The stuck signal raw depth misses: a prior call re-issued verbatim.
268
- const deepCircular = scoreHeuristic(contFeatures(90, { circularToolCall: true }), BASE);
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
 
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
+
272
286
  test("a failing tool result on a deep loop is at least simple", () => {
273
287
  // Was 'at least moderate' before the mechanical-retry damp: the flat
274
288
  // +0.26 pushed deep mechanical retry loops into hard. A damped retry