troxy-cli 1.30.3 → 1.30.4
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.4",
|
|
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": {
|
|
@@ -161,7 +161,7 @@ function bar(fraction) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
// One line, most important first, e.g.
|
|
164
|
-
//
|
|
164
|
+
// 5h: 19% used · 7d: 12% used
|
|
165
165
|
function savedLine(money, rateLimits = []) {
|
|
166
166
|
const paid = money && money.cost_paid_usd;
|
|
167
167
|
if (!(paid > 0)) return undefined;
|
|
@@ -170,8 +170,7 @@ function savedLine(money, rateLimits = []) {
|
|
|
170
170
|
// measured replacement exists.
|
|
171
171
|
const moved = rateLimits.filter(w => WINDOW_LABEL[w.kind] && Math.round(w.percentUsed) >= 1);
|
|
172
172
|
if (!(money.dollars_saved_usd > 0) && !moved.length) return undefined; // nothing routed or cleared yet
|
|
173
|
-
return ['
|
|
174
|
-
...moved.map(w => `${WINDOW_LABEL[w.kind]}: ${Math.round(w.percentUsed)}% used`)].join(' · ');
|
|
173
|
+
return moved.map(w => `${WINDOW_LABEL[w.kind]}: ${Math.round(w.percentUsed)}% used`).join(' · ') || undefined;
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
// t.kept: what the content Troxy cleared or compressed would have cost (server's
|
|
@@ -181,19 +180,15 @@ function sessionLine(t) {
|
|
|
181
180
|
Object.entries(t.meter).map(([kind, percentUsed]) => ({ kind, percentUsed })));
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
//
|
|
185
|
-
// lighter model
|
|
186
|
-
//
|
|
187
|
-
//
|
|
183
|
+
// What Troxy did in this session, from the server's count of its calls: the share that
|
|
184
|
+
// ran on a lighter model instead of the model asked for. A holdout session says so: it is never
|
|
185
|
+
// routed. undefined until there is something to say. (Tokens cleared by the old proxy are
|
|
186
|
+
// not shown: subscription sessions no longer go through it.)
|
|
188
187
|
function savingsText(s, arm) {
|
|
189
188
|
const calls = (s && s.calls) || 0, routed = (s && s.routed_calls) || 0;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const parts = [];
|
|
194
|
-
if (calls) parts.push(`${routed} of ${calls} calls on a lighter model`);
|
|
195
|
-
if (kept > 0) parts.push(`${k(kept)} tokens not re-sent`);
|
|
196
|
-
return parts.length ? parts.join(' · ') : undefined;
|
|
189
|
+
if (!calls) return undefined;
|
|
190
|
+
if (arm === 'holdout') return 'comparison session: not routed, so Troxy can measure the difference';
|
|
191
|
+
return routed ? `${Math.max(1, Math.round(routed / calls * 100))}% of this session ran on a lighter model` : 'no calls moved to a lighter model yet';
|
|
197
192
|
}
|
|
198
193
|
|
|
199
194
|
// This session's savings from the server, or null when there is no key, no session id or
|
|
@@ -278,10 +273,9 @@ async function flushReport($) {
|
|
|
278
273
|
}
|
|
279
274
|
|
|
280
275
|
// /troxy status: everything about this session's Troxy in one place.
|
|
281
|
-
function statusText({ version, paused,
|
|
276
|
+
function statusText({ version, paused, savings, rateLimits = [], compactOff = false }) {
|
|
282
277
|
const lines = [`Troxy ${version || ''}`.trim() + ` · routing is ${paused ? 'off' : 'on'}`];
|
|
283
|
-
lines.push(
|
|
284
|
-
: 'This session: nothing moved to a cheaper model yet');
|
|
278
|
+
lines.push(`This session: ${savings || 'nothing moved to a lighter model yet'}`);
|
|
285
279
|
const meter = rateLimits.filter(w => WINDOW_LABEL[w.kind]).map(w => `${WINDOW_LABEL[w.kind]} ${Math.round(w.percentUsed)}%`);
|
|
286
280
|
if (meter.length) lines.push(`Plan usage now: ${meter.join(' · ')}`);
|
|
287
281
|
lines.push('The model is picked at the start, after an hour idle or after a compaction, then kept (switching would rewrite the cache). /model always wins.');
|
|
@@ -606,8 +600,7 @@ export function register(on) {
|
|
|
606
600
|
await $.store.set('paused', r.paused);
|
|
607
601
|
if (/^\s*(on|off)\s*$/i.test(String(e.args || ''))) return { text: r.text };
|
|
608
602
|
const manifest = JSON.parse(await $.fs.read(`${$.plugin.root}/.claude-plugin/plugin.json`).catch(() => '{}'));
|
|
609
|
-
|
|
610
|
-
return { text: statusText({ version: manifest.version, paused: r.paused, totals, rateLimits: (await $.session.usage()).rateLimits, compactOff: Boolean(await $.store.get('compactOff')) }) };
|
|
603
|
+
return { text: statusText({ version: manifest.version, paused: r.paused, savings: (await $.state.get(SAVINGS)).value, rateLimits: (await $.session.usage()).rateLimits, compactOff: Boolean(await $.store.get('compactOff')) }) };
|
|
611
604
|
});
|
|
612
605
|
// Routing fails open: a scorer or hook error must never break the session,
|
|
613
606
|
// it just means the requested model is used as-is.
|
|
@@ -624,7 +617,7 @@ export function register(on) {
|
|
|
624
617
|
const { value: kept } = await $.state.get(SAVINGS);
|
|
625
618
|
if ((!line && !kept && !note) || e.props.hasSurvey) return next(e);
|
|
626
619
|
const { Text } = $.ui.resolve(e);
|
|
627
|
-
return h(Text, { color: TROXY_TEAL }, `Troxy ${[
|
|
620
|
+
return h(Text, { color: TROXY_TEAL }, `Troxy · ${[kept, line, note && note.text].filter(Boolean).join(' · ')}`);
|
|
628
621
|
});
|
|
629
622
|
// After the answer, so counting never delays a prompt.
|
|
630
623
|
on('turn.complete', async ($, e, next) => {
|
|
@@ -67,10 +67,10 @@ test('savedLine: no savings claim until measured, then the meter', () => {
|
|
|
67
67
|
const { savedLine } = _internal;
|
|
68
68
|
const meter = [{ kind: 'five_hour', percentUsed: 19 }, { kind: 'seven_day', percentUsed: 12.4 }];
|
|
69
69
|
assert.equal(savedLine({ cost_paid_usd: 19, dollars_saved_usd: 13 }, meter),
|
|
70
|
-
'
|
|
70
|
+
'5h: 19% used · 7d: 12% used');
|
|
71
71
|
assert.equal(savedLine({ cost_paid_usd: 92, dollars_saved_usd: 8 }, [{ kind: 'five_hour', percentUsed: 12 }, { kind: 'seven_day', percentUsed: 0 }]),
|
|
72
|
-
'
|
|
73
|
-
assert.equal(savedLine({ cost_paid_usd: 1.64, dollars_saved_usd: 4.53 }, []),
|
|
72
|
+
'5h: 12% used');
|
|
73
|
+
assert.equal(savedLine({ cost_paid_usd: 1.64, dollars_saved_usd: 4.53 }, []), undefined);
|
|
74
74
|
assert.equal(savedLine(null, meter), undefined);
|
|
75
75
|
assert.doesNotMatch(savedLine({ cost_paid_usd: 10, dollars_saved_usd: 50 }, meter), /longer|instead of/);
|
|
76
76
|
});
|
|
@@ -83,11 +83,11 @@ test('addTurn: a session total from its own turns, a routed one counted at the a
|
|
|
83
83
|
// Not routed; the 5h window reset mid-turn, so its move counts as 0, not negative.
|
|
84
84
|
t = addTurn(t, { cost: 1.5, meter: { five_hour: 13, seven_day: 5 } }, { cost: 2, meter: { five_hour: 1, seven_day: 6 } }, 1);
|
|
85
85
|
assert.deepEqual(t, { paid: 1, full: 1.5, meter: { five_hour: 3, seven_day: 2 } });
|
|
86
|
-
assert.equal(sessionLine(t), '
|
|
86
|
+
assert.equal(sessionLine(t), '5h: 3% used · 7d: 2% used');
|
|
87
87
|
// Cleared/compressed content counts too, and survives the next turn: $1 paid, $0.5 swap + $1.5 kept = 3x.
|
|
88
88
|
t = addTurn({ ...t, kept: 1.5 }, { cost: 2, meter: {} }, { cost: 2, meter: {} }, 1);
|
|
89
89
|
assert.equal(t.kept, 1.5);
|
|
90
|
-
assert.match(sessionLine(t), /
|
|
90
|
+
assert.match(sessionLine(t), /5h: 3% used/);
|
|
91
91
|
// Server total of a session already 28 dollars deep: only growth after the first reading counts.
|
|
92
92
|
const { withKept } = _internal;
|
|
93
93
|
let k = { paid: 1, full: 1, meter: {} };
|
|
@@ -102,16 +102,15 @@ test('addTurn: a session total from its own turns, a routed one counted at the a
|
|
|
102
102
|
|
|
103
103
|
test('statusText: version, routing, this session, plan usage, where the account view is', () => {
|
|
104
104
|
const { statusText } = _internal;
|
|
105
|
-
|
|
106
|
-
assert.equal(statusText({ version: '1.29.42', paused: false, totals, rateLimits: [{ kind: 'five_hour', percentUsed: 23.4 }, { kind: 'seven_day', percentUsed: 77 }] }),
|
|
105
|
+
assert.equal(statusText({ version: '1.29.42', paused: false, savings: '55% of this session ran on a lighter model', rateLimits: [{ kind: 'five_hour', percentUsed: 23.4 }, { kind: 'seven_day', percentUsed: 77 }] }),
|
|
107
106
|
'Troxy 1.29.42 · routing is on\n'
|
|
108
|
-
+ 'This session:
|
|
107
|
+
+ 'This session: 55% of this session ran on a lighter model\n'
|
|
109
108
|
+ 'Plan usage now: 5h 23% · 7d 77%\n'
|
|
110
109
|
+ 'The model is picked at the start, after an hour idle or after a compaction, then kept (switching would rewrite the cache). /model always wins.\n'
|
|
111
110
|
+ 'Idle compaction is on: sessions over 100k tokens are compacted after 55 min idle (/troxy compact off).\n'
|
|
112
111
|
+ 'All sessions: dash.troxy.io\n'
|
|
113
112
|
+ 'Type /troxy off to pause routing.');
|
|
114
|
-
assert.match(statusText({ paused: true }), /^Troxy · routing is off\nThis session: nothing moved to a
|
|
113
|
+
assert.match(statusText({ paused: true }), /^Troxy · routing is off\nThis session: nothing moved to a lighter model yet\n/);
|
|
115
114
|
});
|
|
116
115
|
|
|
117
116
|
test('compactPlan: big idle sessions compact, small ones and a nearly full bar do not', () => {
|
|
@@ -128,11 +127,11 @@ test('compactPlan: big idle sessions compact, small ones and a nearly full bar d
|
|
|
128
127
|
assert.deepEqual(compactPlan({ tokens: 210000, now, rateLimits: [{ kind: 'seven_day', percentUsed: 99 }] }), { action: 'skip' });
|
|
129
128
|
});
|
|
130
129
|
|
|
131
|
-
test('savingsText: calls on a lighter model,
|
|
130
|
+
test('savingsText: share of calls on a lighter model, holdout says so, nothing before the first call', () => {
|
|
132
131
|
const { savingsText } = _internal;
|
|
133
|
-
assert.equal(savingsText({ calls: 5, routed_calls: 5
|
|
134
|
-
assert.equal(savingsText({ calls:
|
|
135
|
-
assert.equal(savingsText({ calls: 9, routed_calls: 0
|
|
132
|
+
assert.equal(savingsText({ calls: 5, routed_calls: 5 }), '100% of this session ran on a lighter model');
|
|
133
|
+
assert.equal(savingsText({ calls: 1121, routed_calls: 622, trimmed_tokens: 35100000 }), '55% of this session ran on a lighter model');
|
|
134
|
+
assert.equal(savingsText({ calls: 9, routed_calls: 0 }), 'no calls moved to a lighter model yet');
|
|
136
135
|
assert.equal(savingsText({ calls: 3, routed_calls: 0 }, 'holdout'), 'comparison session: not routed, so Troxy can measure the difference');
|
|
137
136
|
assert.equal(savingsText({ calls: 0 }), undefined);
|
|
138
137
|
assert.equal(savingsText(null), undefined);
|