troxy-cli 1.30.2 → 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
|
|
@@ -235,7 +230,7 @@ const REPORT_KEEP_ROWS = 1000; // kept for the next try while Troxy is unreachab
|
|
|
235
230
|
let pending = [];
|
|
236
231
|
let compacting = false;
|
|
237
232
|
|
|
238
|
-
function reportRow({ sid, usage, requested, arm, cold, score, compact, rateLimits, ts, effort, effortAsked }) {
|
|
233
|
+
function reportRow({ sid, usage, requested, arm, cold, score, compact, rateLimits, ts, effort, effortAsked, surfaces }) {
|
|
239
234
|
const rl = {};
|
|
240
235
|
for (const w of rateLimits || []) {
|
|
241
236
|
if (!WINDOW_LABEL[w.kind]) continue;
|
|
@@ -248,6 +243,7 @@ function reportRow({ sid, usage, requested, arm, cold, score, compact, rateLimit
|
|
|
248
243
|
compact: Boolean(compact), input_tokens: usage.input_tokens, output_tokens: usage.output_tokens,
|
|
249
244
|
cache_read_tokens: usage.cache_read_input_tokens, cache_creation_tokens: usage.cache_creation_input_tokens,
|
|
250
245
|
rate_limits: rl, ts, effort: effort || null, effort_requested: effortAsked || null,
|
|
246
|
+
surface: (surfaces || []).includes('desktop') ? 'desktop' : (surfaces || []).includes('terminal') ? 'terminal' : null,
|
|
251
247
|
};
|
|
252
248
|
}
|
|
253
249
|
|
|
@@ -256,7 +252,9 @@ async function noteStep($, result, meta) {
|
|
|
256
252
|
if (!usage || !usage.model) return;
|
|
257
253
|
let rateLimits = [];
|
|
258
254
|
try { rateLimits = (await $.session.usage()).rateLimits; } catch {}
|
|
259
|
-
|
|
255
|
+
let surfaces = [];
|
|
256
|
+
try { surfaces = await $.session.surfaces(); } catch {}
|
|
257
|
+
pending.push(reportRow({ ...meta, sid: await $.session.id(), usage, compact: compacting, rateLimits, surfaces, ts: await $.clock.now() }));
|
|
260
258
|
if (pending.length > REPORT_KEEP_ROWS) pending = pending.slice(-REPORT_KEEP_ROWS);
|
|
261
259
|
}
|
|
262
260
|
|
|
@@ -275,10 +273,9 @@ async function flushReport($) {
|
|
|
275
273
|
}
|
|
276
274
|
|
|
277
275
|
// /troxy status: everything about this session's Troxy in one place.
|
|
278
|
-
function statusText({ version, paused,
|
|
276
|
+
function statusText({ version, paused, savings, rateLimits = [], compactOff = false }) {
|
|
279
277
|
const lines = [`Troxy ${version || ''}`.trim() + ` · routing is ${paused ? 'off' : 'on'}`];
|
|
280
|
-
lines.push(
|
|
281
|
-
: 'This session: nothing moved to a cheaper model yet');
|
|
278
|
+
lines.push(`This session: ${savings || 'nothing moved to a lighter model yet'}`);
|
|
282
279
|
const meter = rateLimits.filter(w => WINDOW_LABEL[w.kind]).map(w => `${WINDOW_LABEL[w.kind]} ${Math.round(w.percentUsed)}%`);
|
|
283
280
|
if (meter.length) lines.push(`Plan usage now: ${meter.join(' · ')}`);
|
|
284
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.');
|
|
@@ -603,8 +600,7 @@ export function register(on) {
|
|
|
603
600
|
await $.store.set('paused', r.paused);
|
|
604
601
|
if (/^\s*(on|off)\s*$/i.test(String(e.args || ''))) return { text: r.text };
|
|
605
602
|
const manifest = JSON.parse(await $.fs.read(`${$.plugin.root}/.claude-plugin/plugin.json`).catch(() => '{}'));
|
|
606
|
-
|
|
607
|
-
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')) }) };
|
|
608
604
|
});
|
|
609
605
|
// Routing fails open: a scorer or hook error must never break the session,
|
|
610
606
|
// it just means the requested model is used as-is.
|
|
@@ -621,7 +617,7 @@ export function register(on) {
|
|
|
621
617
|
const { value: kept } = await $.state.get(SAVINGS);
|
|
622
618
|
if ((!line && !kept && !note) || e.props.hasSurvey) return next(e);
|
|
623
619
|
const { Text } = $.ui.resolve(e);
|
|
624
|
-
return h(Text, { color: TROXY_TEAL }, `Troxy ${[
|
|
620
|
+
return h(Text, { color: TROXY_TEAL }, `Troxy · ${[kept, line, note && note.text].filter(Boolean).join(' · ')}`);
|
|
625
621
|
});
|
|
626
622
|
// After the answer, so counting never delays a prompt.
|
|
627
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);
|
|
@@ -432,3 +431,11 @@ test('routeStep: with the effort swap on, an easy prompt goes out at low effort
|
|
|
432
431
|
assert.equal(pendingRows().at(-1).effort, 'low');
|
|
433
432
|
assert.equal(pendingRows().at(-1).effort_requested, 'high');
|
|
434
433
|
});
|
|
434
|
+
|
|
435
|
+
test('reportRow: surface is desktop when the app draws the session, terminal otherwise, null in -p', () => {
|
|
436
|
+
const { reportRow } = _internal;
|
|
437
|
+
const base = { sid: 's', usage: { model: 'm' } };
|
|
438
|
+
assert.equal(reportRow({ ...base, surfaces: ['terminal', 'desktop'] }).surface, 'desktop');
|
|
439
|
+
assert.equal(reportRow({ ...base, surfaces: ['terminal'] }).surface, 'terminal');
|
|
440
|
+
assert.equal(reportRow({ ...base, surfaces: [] }).surface, null);
|
|
441
|
+
});
|