troxy-cli 1.30.9 → 1.30.11
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.
|
|
3
|
+
"version": "1.30.11",
|
|
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": {
|
|
@@ -56,7 +56,7 @@ async function failClosed($, e, next, err) {
|
|
|
56
56
|
// to every step of its turn, so the cache prefix stays stable within a turn.
|
|
57
57
|
|
|
58
58
|
const MODELS = {
|
|
59
|
-
haiku: 'claude-haiku-
|
|
59
|
+
haiku: 'claude-haiku-5-5',
|
|
60
60
|
sonnet: 'claude-sonnet-5-5',
|
|
61
61
|
opus: 'claude-opus-5-5',
|
|
62
62
|
};
|
|
@@ -67,6 +67,15 @@ const RANK = { haiku: 0, sonnet: 1, opus: 2 };
|
|
|
67
67
|
// pick flipped between Sonnet and Opus. Sonnet is the default; only a clearly hard
|
|
68
68
|
// prompt (above 0.85) keeps the requested model.
|
|
69
69
|
const BANDS = [[0.85, 'sonnet']];
|
|
70
|
+
// Haiku band, off unless switched on (config haikuBand, or TROXY_HAIKU_BAND=1 for an A/B run):
|
|
71
|
+
// an easy prompt from a Sonnet session goes to Haiku 5.5. The 10-15 rejection was Haiku 4.5.
|
|
72
|
+
// Sonnet sessions only; Opus keeps the bands above.
|
|
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.6;
|
|
78
|
+
const HAIKU_MAX_TOKENS = 100000;
|
|
70
79
|
const ROUTE_CONTEXT_FLOOR_TOKENS = 50000;
|
|
71
80
|
const SCORE_TIMEOUT_MS = 3000;
|
|
72
81
|
const SCORER_PROMPT_CHARS = 4000;
|
|
@@ -104,7 +113,7 @@ function asRequest(text) {
|
|
|
104
113
|
return `<request>\n${String(text || '').replace(/<\/request>/gi, '</ request>')}\n</request>`;
|
|
105
114
|
}
|
|
106
115
|
|
|
107
|
-
function pickModel(score, requested, contextTokens, warm) {
|
|
116
|
+
function pickModel(score, requested, contextTokens, warm, haiku = false) {
|
|
108
117
|
const rq = tierOf(requested);
|
|
109
118
|
if (rq === null) return requested; // unknown family (e.g. a newer tier): leave it alone
|
|
110
119
|
// No score (scorer failed or echoed the prompt): stay on the warm model at any
|
|
@@ -114,7 +123,7 @@ function pickModel(score, requested, contextTokens, warm) {
|
|
|
114
123
|
return w !== null && RANK[w] <= RANK[rq] ? warm : requested;
|
|
115
124
|
}
|
|
116
125
|
let tier = rq;
|
|
117
|
-
for (const [limit, name] of BANDS) {
|
|
126
|
+
for (const [limit, name] of (haiku && rq === 'sonnet' ? HAIKU_BANDS : BANDS)) {
|
|
118
127
|
if (score <= limit) { if (RANK[name] < RANK[rq]) tier = name; break; }
|
|
119
128
|
}
|
|
120
129
|
// The cache is per model: the first prompt on a new model re-writes the whole
|
|
@@ -144,7 +153,8 @@ function troxyCommand(args, paused) {
|
|
|
144
153
|
// List $/MTok in (output keeps the same ratios) times usage-bar points per list dollar,
|
|
145
154
|
// measured from the bar's own readings (usage-audit bar_weights.py, 2026-10-13). The bar
|
|
146
155
|
// charges Haiku more per dollar than list prices imply; same weights as the dashboard.
|
|
147
|
-
|
|
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 };
|
|
148
158
|
const WINDOW_LABEL = { five_hour: '5h', seven_day: '7d' };
|
|
149
159
|
// Drawn in Troxy's teal: the status line can't be colored and shows as a yellow warning.
|
|
150
160
|
const LINE = { plugin: 'troxy-guard', key: 'line' };
|
|
@@ -522,10 +532,12 @@ async function rememberAsked($, sid, want, ran) {
|
|
|
522
532
|
// compaction) the cache is gone anyway, so pick by score. Otherwise hold the model the
|
|
523
533
|
// cache is warm on: every switch re-writes the whole conversation (2026-10-16 A/B: 4-6
|
|
524
534
|
// switches per session cost more bar than routing saved). /model always wins.
|
|
525
|
-
function stepModel({ arm, requested, switched, cold, score, warm }) {
|
|
535
|
+
function stepModel({ arm, requested, switched, cold, score, warm, haiku = false, tokens = 0 }) {
|
|
526
536
|
if (arm !== 'treated' || switched) return requested;
|
|
527
|
-
if (cold) return pickModel(score, requested, 0, null);
|
|
537
|
+
if (cold) return pickModel(score, requested, 0, null, haiku && tokens <= HAIKU_MAX_TOKENS);
|
|
528
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;
|
|
529
541
|
return w !== null && rq !== null && RANK[w] <= RANK[rq] ? warm : requested;
|
|
530
542
|
}
|
|
531
543
|
|
|
@@ -547,6 +559,11 @@ async function effortSwapOn($) {
|
|
|
547
559
|
return Boolean((await troxyConfig($)).effortSwap);
|
|
548
560
|
}
|
|
549
561
|
|
|
562
|
+
async function haikuBandOn($) {
|
|
563
|
+
if ((await $.env.get('TROXY_HAIKU_BAND')) === '1') return true;
|
|
564
|
+
return Boolean((await troxyConfig($)).haikuBand);
|
|
565
|
+
}
|
|
566
|
+
|
|
550
567
|
async function* routeStep($, e, next) {
|
|
551
568
|
// Main loop only: a subagent keeps whatever model it was given.
|
|
552
569
|
if (e.agentId) {
|
|
@@ -575,6 +592,8 @@ async function* routeStep($, e, next) {
|
|
|
575
592
|
arm, requested: want, switched,
|
|
576
593
|
cold: isCold,
|
|
577
594
|
score: decision ? decision.score : null, warm: ls && ls.model,
|
|
595
|
+
haiku: await haikuBandOn($).catch(() => false),
|
|
596
|
+
tokens: decision ? decision.tokens : 0,
|
|
578
597
|
});
|
|
579
598
|
coldNext.delete(sid);
|
|
580
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);
|
|
@@ -90,3 +90,26 @@ test('estimateTokens: counts the prompt being submitted, not just the context so
|
|
|
90
90
|
// a first-prompt of ~60k tokens must trip the floor
|
|
91
91
|
assert.equal(pickModel(0.05, MODELS.opus, estimateTokens(0, 'x'.repeat(240000))), MODELS.opus);
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
test('pickModel: haiku band (flag) sends an easy Sonnet prompt to Haiku 5.5, nothing else changes', () => {
|
|
95
|
+
assert.equal(MODELS.haiku, 'claude-haiku-5-5');
|
|
96
|
+
assert.equal(pickModel(0.1, MODELS.sonnet, 1000, null, true), MODELS.haiku);
|
|
97
|
+
assert.equal(pickModel(0.2, MODELS.sonnet, 1000, null, true), MODELS.haiku);
|
|
98
|
+
assert.equal(pickModel(0.3, MODELS.sonnet, 1000, null, true), MODELS.sonnet);
|
|
99
|
+
assert.equal(pickModel(0.1, MODELS.sonnet, 1000, null, false), MODELS.sonnet); // off by default
|
|
100
|
+
assert.equal(pickModel(0.1, MODELS.opus, 1000, null, true), MODELS.sonnet); // Opus keeps its bands
|
|
101
|
+
assert.equal(pickModel(0.1, MODELS.sonnet, 60000, null, true), MODELS.sonnet); // 50k floor still holds
|
|
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.7, warm: MODELS.haiku }), MODELS.sonnet); // hard prompt: up
|
|
111
|
+
assert.equal(stepModel({ ...base, cold: false, score: 0.1, warm: MODELS.haiku, tokens: 120000 }), MODELS.sonnet); // long: up
|
|
112
|
+
assert.equal(stepModel({ ...base, cold: false, score: 0.1, warm: MODELS.sonnet }), MODELS.sonnet); // Sonnet never goes down
|
|
113
|
+
assert.equal(stepModel({ ...base, haiku: false, cold: false, score: 0.1, warm: MODELS.haiku }), MODELS.sonnet); // flag off: up
|
|
114
|
+
assert.equal(stepModel({ ...base, arm: 'holdout', cold: true, score: 0.1, warm: null }), MODELS.sonnet); // holdout untouched
|
|
115
|
+
});
|