troxy-cli 1.30.0 → 1.30.1

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.0",
3
+ "version": "1.30.1",
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": {
@@ -181,13 +181,19 @@ function sessionLine(t) {
181
181
  Object.entries(t.meter).map(([kind, percentUsed]) => ({ kind, percentUsed })));
182
182
  }
183
183
 
184
- // Tokens Troxy kept out of the requests of this session (compression + trimming), counted
185
- // by the server. undefined until something was saved.
186
- function savingsText(s) {
187
- const c = (s && s.compressed_tokens) || 0, t = (s && s.trimmed_tokens) || 0;
184
+ // This session's Troxy work, from the server's count of its calls: how many ran on a
185
+ // lighter model than asked, plus tokens kept out of requests (compression + trimming,
186
+ // API-key setups through the proxy only). A holdout session says so: it is never routed.
187
+ // undefined until there is something to say.
188
+ function savingsText(s, arm) {
189
+ const calls = (s && s.calls) || 0, routed = (s && s.routed_calls) || 0;
190
+ const kept = ((s && s.compressed_tokens) || 0) + ((s && s.trimmed_tokens) || 0);
188
191
  const k = n => n >= 1e6 ? `${Math.round(n / 1e5) / 10}M`.replace('.0M', 'M') : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
189
- // Always show, even if 0, so the band is transparent: 0 means no trimming/compression on this turn
190
- return `${k(c + t)} tokens not re-sent, thanks to compression and trimming`;
192
+ if (arm === 'holdout') return calls ? 'comparison session: not routed, so Troxy can measure the difference' : undefined;
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;
191
197
  }
192
198
 
193
199
  // This session's savings from the server, or null when there is no key, no session id or
@@ -229,7 +235,7 @@ const REPORT_KEEP_ROWS = 1000; // kept for the next try while Troxy is unreachab
229
235
  let pending = [];
230
236
  let compacting = false;
231
237
 
232
- function reportRow({ sid, usage, requested, arm, cold, score, compact, rateLimits }) {
238
+ function reportRow({ sid, usage, requested, arm, cold, score, compact, rateLimits, ts }) {
233
239
  const rl = {};
234
240
  for (const w of rateLimits || []) {
235
241
  if (!WINDOW_LABEL[w.kind]) continue;
@@ -241,7 +247,7 @@ function reportRow({ sid, usage, requested, arm, cold, score, compact, rateLimit
241
247
  arm: arm || null, switch: arm ? (cold ? 'cold' : 'warm') : null, score: score == null ? null : score,
242
248
  compact: Boolean(compact), input_tokens: usage.input_tokens, output_tokens: usage.output_tokens,
243
249
  cache_read_tokens: usage.cache_read_input_tokens, cache_creation_tokens: usage.cache_creation_input_tokens,
244
- rate_limits: rl,
250
+ rate_limits: rl, ts,
245
251
  };
246
252
  }
247
253
 
@@ -250,7 +256,7 @@ async function noteStep($, result, meta) {
250
256
  if (!usage || !usage.model) return;
251
257
  let rateLimits = [];
252
258
  try { rateLimits = (await $.session.usage()).rateLimits; } catch {}
253
- pending.push(reportRow({ ...meta, sid: await $.session.id(), usage, compact: compacting, rateLimits }));
259
+ pending.push(reportRow({ ...meta, sid: await $.session.id(), usage, compact: compacting, rateLimits, ts: await $.clock.now() }));
254
260
  if (pending.length > REPORT_KEEP_ROWS) pending = pending.slice(-REPORT_KEEP_ROWS);
255
261
  }
256
262
 
@@ -601,7 +607,7 @@ export function register(on) {
601
607
  let savings = null;
602
608
  try {
603
609
  savings = await fetchSavings($);
604
- if (savings) await $.state.set(SAVINGS, savingsText(savings));
610
+ if (savings) await $.state.set(SAVINGS, savingsText(savings, (await $.state.get(ARM)).value) || null);
605
611
  } catch {} // a missed fetch only leaves the band as it was
606
612
  if (turn) {
607
613
  const { start, ratio } = turn;
@@ -128,14 +128,14 @@ test('compactPlan: big idle sessions compact, small ones and a nearly full bar d
128
128
  assert.deepEqual(compactPlan({ tokens: 210000, now, rateLimits: [{ kind: 'seven_day', percentUsed: 99 }] }), { action: 'skip' });
129
129
  });
130
130
 
131
- test('savingsText: shows compression + trimming together, 0 when nothing was saved', () => {
131
+ test('savingsText: calls on a lighter model, kept tokens only when there are some, holdout says so', () => {
132
132
  const { savingsText } = _internal;
133
- assert.equal(savingsText({ compressed_tokens: 5200, trimmed_tokens: 40100 }), '45k tokens not re-sent, thanks to compression and trimming');
134
- assert.equal(savingsText({ compressed_tokens: 300, trimmed_tokens: 0 }), '300 tokens not re-sent, thanks to compression and trimming');
135
- assert.equal(savingsText({ compressed_tokens: 0, trimmed_tokens: 0 }), '0 tokens not re-sent, thanks to compression and trimming');
136
- assert.equal(savingsText({ compressed_tokens: 123277, trimmed_tokens: 154512861 }), '154.6M tokens not re-sent, thanks to compression and trimming');
137
- assert.equal(savingsText({ compressed_tokens: 0, trimmed_tokens: 2000000 }), '2M tokens not re-sent, thanks to compression and trimming');
138
- assert.equal(savingsText(null), '0 tokens not re-sent, thanks to compression and trimming');
133
+ assert.equal(savingsText({ calls: 5, routed_calls: 5, compressed_tokens: 0, trimmed_tokens: 0 }), '5 of 5 calls on a lighter model');
134
+ assert.equal(savingsText({ calls: 4, routed_calls: 1, compressed_tokens: 5200, trimmed_tokens: 40100 }), '1 of 4 calls on a lighter model · 45k tokens not re-sent');
135
+ assert.equal(savingsText({ calls: 9, routed_calls: 0, compressed_tokens: 123277, trimmed_tokens: 154512861 }), '0 of 9 calls on a lighter model · 154.6M tokens not re-sent');
136
+ assert.equal(savingsText({ calls: 3, routed_calls: 0 }, 'holdout'), 'comparison session: not routed, so Troxy can measure the difference');
137
+ assert.equal(savingsText({ calls: 0 }), undefined);
138
+ assert.equal(savingsText(null), undefined);
139
139
  });
140
140
 
141
141
  test('fetchSavings: sends the saved key and the session id; null without a key or on an error', async () => {
@@ -364,6 +364,7 @@ test('usage report: each request is queued with model, tokens, bar and arm, then
364
364
  const row = pendingRows().at(-1);
365
365
  assert.equal(row.model, M.sonnet); assert.equal(row.model_requested, M.opus);
366
366
  assert.equal(row.arm, 'treated'); assert.equal(row.switch, 'cold'); assert.equal(row.cache_read_tokens, 900);
367
+ assert.equal(typeof row.ts, 'number'); // when the request ran, not when the turn ended
367
368
  assert.deepEqual(row.rate_limits, { five_hour: { percent_used: 42, resets_at: '2026-10-20T10:00:00Z' } });
368
369
  assert.equal(JSON.stringify(row).includes('reply only ok'), false); // never prompt text
369
370
  await flushReport($);