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",
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
- // This session: savings under measurement · 5h: 19% used · 7d: 12% used
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 ['This session: savings under measurement',
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
- // 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.
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
- const kept = ((s && s.compressed_tokens) || 0) + ((s && s.trimmed_tokens) || 0);
191
- const k = n => n >= 1e6 ? `${Math.round(n / 1e5) / 10}M`.replace('.0M', 'M') : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
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;
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, totals, rateLimits = [], compactOff = false }) {
276
+ function statusText({ version, paused, savings, rateLimits = [], compactOff = false }) {
282
277
  const lines = [`Troxy ${version || ''}`.trim() + ` · routing is ${paused ? 'off' : 'on'}`];
283
- lines.push(sessionLine(totals) ? `This session: ${sessionLine(totals).replace(/^.*?This session: /, '')}`
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
- const { value: totals } = await $.state.get(SAVED);
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 ${[line, kept, note && note.text].filter(Boolean).join(' · ')}`);
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
- 'This session: savings under measurement · 5h: 19% used · 7d: 12% used');
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
- 'This session: savings under measurement · 5h: 12% used');
73
- assert.equal(savedLine({ cost_paid_usd: 1.64, dollars_saved_usd: 4.53 }, []), 'This session: savings under measurement');
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), 'This session: savings under measurement · 5h: 3% used · 7d: 2% used');
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), /savings under measurement · 5h: 3% used/);
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
- const totals = { paid: 0.03, full: 0.12, meter: {} };
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: savings under measurement\n'
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 cheaper model yet\n/);
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, kept tokens only when there are some, holdout says so', () => {
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, 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');
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);