troxy-cli 1.30.10 → 1.30.12

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": "troxy-cli",
3
- "version": "1.30.10",
3
+ "version": "1.30.12",
4
4
  "description": "A secure control layer for AI agents: policies across payments, messages, logins, destructive actions, model usage, and secrets, all enforceable from the CLI",
5
5
  "homepage": "https://troxy.io",
6
6
  "bugs": {
@@ -71,6 +71,11 @@ const BANDS = [[0.85, 'sonnet']];
71
71
  // an easy prompt from a Sonnet session goes to Haiku 5.5. The 10-15 rejection was Haiku 4.5.
72
72
  // Sonnet sessions only; Opus keeps the bands above.
73
73
  const HAIKU_BANDS = [[0.2, 'haiku']];
74
+ // A Haiku session moves up to Sonnet, once and for good, when a later prompt scores above
75
+ // this or the context passes the Haiku price break (Haiku 5.5 is 5x dearer past 100k tokens).
76
+ // Never back down: every switch re-writes the cache.
77
+ const HAIKU_ESCALATE_ABOVE = 0.3; // job B's bug and fix prompts scored 0.4 to 0.5 and Sonnet won them 8 to 2; at 0.6 they stayed on Haiku
78
+ const HAIKU_MAX_TOKENS = 100000;
74
79
  const ROUTE_CONTEXT_FLOOR_TOKENS = 50000;
75
80
  const SCORE_TIMEOUT_MS = 3000;
76
81
  const SCORER_PROMPT_CHARS = 4000;
@@ -148,7 +153,8 @@ function troxyCommand(args, paused) {
148
153
  // List $/MTok in (output keeps the same ratios) times usage-bar points per list dollar,
149
154
  // measured from the bar's own readings (usage-audit bar_weights.py, 2026-10-13). The bar
150
155
  // charges Haiku more per dollar than list prices imply; same weights as the dashboard.
151
- const PRICE = { haiku: 1 * 4.45, sonnet: 2 * 3.19, opus: 4 * 2.80 };
156
+ // Haiku 5.5 ($0.10 in): 5.2 points per list dollar puts it 12x under Sonnet per token, as measured 2026-10-22.
157
+ const PRICE = { haiku: 0.1 * 5.2, sonnet: 2 * 3.19, opus: 4 * 2.80 };
152
158
  const WINDOW_LABEL = { five_hour: '5h', seven_day: '7d' };
153
159
  // Drawn in Troxy's teal: the status line can't be colored and shows as a yellow warning.
154
160
  const LINE = { plugin: 'troxy-guard', key: 'line' };
@@ -526,10 +532,12 @@ async function rememberAsked($, sid, want, ran) {
526
532
  // compaction) the cache is gone anyway, so pick by score. Otherwise hold the model the
527
533
  // cache is warm on: every switch re-writes the whole conversation (2026-10-16 A/B: 4-6
528
534
  // switches per session cost more bar than routing saved). /model always wins.
529
- function stepModel({ arm, requested, switched, cold, score, warm, haiku = false }) {
535
+ function stepModel({ arm, requested, switched, cold, score, warm, haiku = false, tokens = 0 }) {
530
536
  if (arm !== 'treated' || switched) return requested;
531
- if (cold) return pickModel(score, requested, 0, null, haiku);
537
+ if (cold) return pickModel(score, requested, 0, null, haiku && tokens <= HAIKU_MAX_TOKENS);
532
538
  const w = tierOf(warm), rq = tierOf(requested);
539
+ // ponytail: tokens is read at the prompt, so a turn that grows past 100k moves up at the next prompt.
540
+ if (w === 'haiku' && rq === 'sonnet' && (!haiku || score > HAIKU_ESCALATE_ABOVE || tokens > HAIKU_MAX_TOKENS)) return requested;
533
541
  return w !== null && rq !== null && RANK[w] <= RANK[rq] ? warm : requested;
534
542
  }
535
543
 
@@ -585,6 +593,7 @@ async function* routeStep($, e, next) {
585
593
  cold: isCold,
586
594
  score: decision ? decision.score : null, warm: ls && ls.model,
587
595
  haiku: await haikuBandOn($).catch(() => false),
596
+ tokens: decision ? decision.tokens : 0,
588
597
  });
589
598
  coldNext.delete(sid);
590
599
  lastSent.set(sid, { model, at: now });
@@ -2,7 +2,7 @@ import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { _internal } from '../mods/troxy-guard/hooks/register.js';
4
4
 
5
- const { parseScore, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS } = _internal;
5
+ const { parseScore, pickModel, troxyCommand, modRouteFile, estimateTokens, stepModel, MODELS } = _internal;
6
6
 
7
7
  test('parseScore: reads a bare number and clamps to 0..1', () => {
8
8
  assert.equal(parseScore('0.35'), 0.35);
@@ -100,3 +100,17 @@ test('pickModel: haiku band (flag) sends an easy Sonnet prompt to Haiku 5.5, not
100
100
  assert.equal(pickModel(0.1, MODELS.opus, 1000, null, true), MODELS.sonnet); // Opus keeps its bands
101
101
  assert.equal(pickModel(0.1, MODELS.sonnet, 60000, null, true), MODELS.sonnet); // 50k floor still holds
102
102
  });
103
+
104
+ test('stepModel: a Haiku session moves up to Sonnet on a hard prompt or a long context, never back down', () => {
105
+ const base = { arm: 'treated', requested: MODELS.sonnet, switched: false, haiku: true };
106
+ assert.equal(stepModel({ ...base, cold: true, score: 0.1, warm: null }), MODELS.haiku);
107
+ assert.equal(stepModel({ ...base, cold: true, score: 0.1, warm: null, tokens: 150000 }), MODELS.sonnet); // too long to start on Haiku
108
+ assert.equal(stepModel({ ...base, cold: false, score: 0.1, warm: MODELS.haiku }), MODELS.haiku); // easy prompt: hold
109
+ assert.equal(stepModel({ ...base, cold: false, score: null, warm: MODELS.haiku }), MODELS.haiku); // no score: hold
110
+ assert.equal(stepModel({ ...base, cold: false, score: 0.4, warm: MODELS.haiku }), MODELS.sonnet); // harder prompt: up
111
+ assert.equal(stepModel({ ...base, cold: false, score: 0.3, warm: MODELS.haiku }), MODELS.haiku); // 0.3 or lower: hold
112
+ assert.equal(stepModel({ ...base, cold: false, score: 0.1, warm: MODELS.haiku, tokens: 120000 }), MODELS.sonnet); // long: up
113
+ assert.equal(stepModel({ ...base, cold: false, score: 0.1, warm: MODELS.sonnet }), MODELS.sonnet); // Sonnet never goes down
114
+ assert.equal(stepModel({ ...base, haiku: false, cold: false, score: 0.1, warm: MODELS.haiku }), MODELS.sonnet); // flag off: up
115
+ assert.equal(stepModel({ ...base, arm: 'holdout', cold: true, score: 0.1, warm: null }), MODELS.sonnet); // holdout untouched
116
+ });