troxy-cli 1.29.59 → 1.29.61

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.29.59",
3
+ "version": "1.29.61",
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": {
@@ -188,9 +188,9 @@ function sessionLine(t) {
188
188
  // by the server. undefined until something was saved.
189
189
  function savingsText(s) {
190
190
  const c = (s && s.compressed_tokens) || 0, t = (s && s.trimmed_tokens) || 0;
191
- const k = n => n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
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
192
  // Always show, even if 0, so the band is transparent: 0 means no trimming/compression on this turn
193
- return `${k(c + t)} tokens saved by compression and trimming`;
193
+ return `${k(c + t)} tokens not re-sent, thanks to compression and trimming`;
194
194
  }
195
195
 
196
196
  // This session's savings from the server, or null when there is no key, no session id or
@@ -440,13 +440,17 @@ const held = new Map();
440
440
 
441
441
  // Which model a step runs on. At a cold moment (first step, 55+ min idle, after a
442
442
  // compaction) any model is free to pick. While warm, hold the model, except at the
443
- // first step of a new prompt once it has held HOLD_PROMPTS prompts: then re-pick by
444
- // score, down or up. The user's own /model always wins and becomes the held model.
443
+ // first step of a new prompt: up at once, down once it has held HOLD_PROMPTS prompts,
444
+ // re-picking by score. The user's own /model always wins and becomes the held model.
445
445
  function stepModel({ arm, requested, switched, cold, score, warm, newPrompt, heldPrompts, tokens }) {
446
446
  if (arm !== 'treated' || switched) return requested;
447
447
  if (cold) return pickModel(score, requested, 0, null);
448
- if (newPrompt && score !== null && heldPrompts >= HOLD_PROMPTS && tokens <= WARM_SWITCH_MAX_TOKENS) {
449
- return pickModel(score, requested, 0, null);
448
+ if (newPrompt && score !== null && tokens <= WARM_SWITCH_MAX_TOKENS) {
449
+ const pick = pickModel(score, requested, 0, null);
450
+ // Up is immediate: a cheap model chosen for an easy first prompt must not keep
451
+ // running a harder one (the 2026-10-15 A/B: Haiku held a whole coding session and
452
+ // made 2-4x the calls). Down still waits HOLD_PROMPTS, since it re-writes the cache.
453
+ if (RANK[tierOf(pick)] > RANK[tierOf(warm)] || heldPrompts >= HOLD_PROMPTS) return pick;
450
454
  }
451
455
  const w = tierOf(warm), rq = tierOf(requested);
452
456
  return w !== null && rq !== null && RANK[w] <= RANK[rq] ? warm : requested;
@@ -134,10 +134,12 @@ test('compactPlan: big idle sessions compact, small ones and a nearly full bar d
134
134
 
135
135
  test('savingsText: shows compression + trimming together, 0 when nothing was saved', () => {
136
136
  const { savingsText } = _internal;
137
- assert.equal(savingsText({ compressed_tokens: 5200, trimmed_tokens: 40100 }), '45k tokens saved by compression and trimming');
138
- assert.equal(savingsText({ compressed_tokens: 300, trimmed_tokens: 0 }), '300 tokens saved by compression and trimming');
139
- assert.equal(savingsText({ compressed_tokens: 0, trimmed_tokens: 0 }), '0 tokens saved by compression and trimming');
140
- assert.equal(savingsText(null), '0 tokens saved by compression and trimming');
137
+ assert.equal(savingsText({ compressed_tokens: 5200, trimmed_tokens: 40100 }), '45k tokens not re-sent, thanks to compression and trimming');
138
+ assert.equal(savingsText({ compressed_tokens: 300, trimmed_tokens: 0 }), '300 tokens not re-sent, thanks to compression and trimming');
139
+ assert.equal(savingsText({ compressed_tokens: 0, trimmed_tokens: 0 }), '0 tokens not re-sent, thanks to compression and trimming');
140
+ assert.equal(savingsText({ compressed_tokens: 123277, trimmed_tokens: 154512861 }), '154.6M tokens not re-sent, thanks to compression and trimming');
141
+ assert.equal(savingsText({ compressed_tokens: 0, trimmed_tokens: 2000000 }), '2M tokens not re-sent, thanks to compression and trimming');
142
+ assert.equal(savingsText(null), '0 tokens not re-sent, thanks to compression and trimming');
141
143
  });
142
144
 
143
145
  test('fetchSavings: sends the saved key and the session id; null without a key or on an error', async () => {
@@ -213,6 +215,18 @@ test('stepModel: warm switch at a new prompt only after the hold, and under the
213
215
  assert.equal(stepModel({ ...base, arm: 'holdout', newPrompt: true, heldPrompts: HOLD_PROMPTS }), M.opus);
214
216
  });
215
217
 
218
+ test('stepModel: a harder prompt upgrades a cheap warm session at once, a lighter one waits', () => {
219
+ const { stepModel, MODELS: M, HOLD_PROMPTS } = _internal;
220
+ const base = { arm: 'treated', requested: M.opus, switched: false, cold: false, warm: M.haiku, newPrompt: true, tokens: 5000 };
221
+ assert.equal(stepModel({ ...base, score: 0.9, heldPrompts: 0 }), M.opus); // hard prompt on haiku: up now
222
+ assert.equal(stepModel({ ...base, score: 0.5, heldPrompts: 0 }), M.sonnet);
223
+ assert.equal(stepModel({ ...base, score: 0.1, heldPrompts: 0 }), M.haiku); // still easy: stay
224
+ assert.equal(stepModel({ ...base, warm: M.opus, score: 0.1, heldPrompts: 0 }), M.opus); // down waits
225
+ assert.equal(stepModel({ ...base, warm: M.opus, score: 0.1, heldPrompts: HOLD_PROMPTS }), M.haiku); // ...for the hold
226
+ assert.equal(stepModel({ ...base, score: 0.9, heldPrompts: 0, tokens: 300000 }), M.haiku); // too big to re-write
227
+ assert.equal(stepModel({ ...base, score: 0.9, heldPrompts: 0, newPrompt: false }), M.haiku); // mid-turn: hold
228
+ });
229
+
216
230
  test('pickModel: past the floor, stay on the warm model unless the prompt needs a stronger one', () => {
217
231
  const { pickModel, MODELS } = _internal;
218
232
  const big = 80000;