troxy-cli 1.29.60 → 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.60",
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": {
@@ -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;
@@ -215,6 +215,18 @@ test('stepModel: warm switch at a new prompt only after the hold, and under the
215
215
  assert.equal(stepModel({ ...base, arm: 'holdout', newPrompt: true, heldPrompts: HOLD_PROMPTS }), M.opus);
216
216
  });
217
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
+
218
230
  test('pickModel: past the floor, stay on the warm model unless the prompt needs a stronger one', () => {
219
231
  const { pickModel, MODELS } = _internal;
220
232
  const big = 80000;