troxy-cli 1.30.1 → 1.30.2
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.2",
|
|
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": {
|
|
@@ -235,7 +235,7 @@ const REPORT_KEEP_ROWS = 1000; // kept for the next try while Troxy is unreachab
|
|
|
235
235
|
let pending = [];
|
|
236
236
|
let compacting = false;
|
|
237
237
|
|
|
238
|
-
function reportRow({ sid, usage, requested, arm, cold, score, compact, rateLimits, ts }) {
|
|
238
|
+
function reportRow({ sid, usage, requested, arm, cold, score, compact, rateLimits, ts, effort, effortAsked }) {
|
|
239
239
|
const rl = {};
|
|
240
240
|
for (const w of rateLimits || []) {
|
|
241
241
|
if (!WINDOW_LABEL[w.kind]) continue;
|
|
@@ -247,7 +247,7 @@ function reportRow({ sid, usage, requested, arm, cold, score, compact, rateLimit
|
|
|
247
247
|
arm: arm || null, switch: arm ? (cold ? 'cold' : 'warm') : null, score: score == null ? null : score,
|
|
248
248
|
compact: Boolean(compact), input_tokens: usage.input_tokens, output_tokens: usage.output_tokens,
|
|
249
249
|
cache_read_tokens: usage.cache_read_input_tokens, cache_creation_tokens: usage.cache_creation_input_tokens,
|
|
250
|
-
rate_limits: rl, ts,
|
|
250
|
+
rate_limits: rl, ts, effort: effort || null, effort_requested: effortAsked || null,
|
|
251
251
|
};
|
|
252
252
|
}
|
|
253
253
|
|
|
@@ -510,6 +510,24 @@ function stepModel({ arm, requested, switched, cold, score, warm }) {
|
|
|
510
510
|
return w !== null && rq !== null && RANK[w] <= RANK[rq] ? warm : requested;
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
+
// Effort swap: an easy prompt (scorer) runs at lower effort, so the model thinks and
|
|
514
|
+
// writes less and makes fewer tool calls. Never raised. Claude Code sends the change
|
|
515
|
+
// per message, which keeps the prompt cache (tested 2026-10-20: about 65 cache tokens
|
|
516
|
+
// written on a changed turn, same as an unchanged one). Off unless switched on
|
|
517
|
+
// (config effortSwap, or TROXY_EFFORT_SWAP=1 for an A/B run) until it is measured.
|
|
518
|
+
const EFFORT_RANK = { low: 0, medium: 1, high: 2, xhigh: 3, max: 4 };
|
|
519
|
+
const EFFORT_LOW_BELOW = 0.4;
|
|
520
|
+
const EFFORT_MEDIUM_BELOW = 0.85;
|
|
521
|
+
function stepEffort({ arm, requested, score, on }) {
|
|
522
|
+
if (!on || arm !== 'treated' || score == null || !(requested in EFFORT_RANK)) return requested;
|
|
523
|
+
const cap = score < EFFORT_LOW_BELOW ? 'low' : score < EFFORT_MEDIUM_BELOW ? 'medium' : null;
|
|
524
|
+
return cap && EFFORT_RANK[cap] < EFFORT_RANK[requested] ? cap : requested;
|
|
525
|
+
}
|
|
526
|
+
async function effortSwapOn($) {
|
|
527
|
+
if ((await $.env.get('TROXY_EFFORT_SWAP')) === '1') return true;
|
|
528
|
+
return Boolean((await troxyConfig($)).effortSwap);
|
|
529
|
+
}
|
|
530
|
+
|
|
513
531
|
async function* routeStep($, e, next) {
|
|
514
532
|
// Main loop only: a subagent keeps whatever model it was given.
|
|
515
533
|
if (e.agentId) {
|
|
@@ -552,8 +570,13 @@ async function* routeStep($, e, next) {
|
|
|
552
570
|
}
|
|
553
571
|
try { await rememberAsked($, sid, want, model); } catch {} // a missed write only loses this on resume
|
|
554
572
|
if (model !== want && turn) turn.ratio = PRICE[tierOf(want)] / PRICE[tierOf(model)];
|
|
555
|
-
|
|
556
|
-
|
|
573
|
+
let effort = e.effort;
|
|
574
|
+
if (!(await $.store.get('paused'))) {
|
|
575
|
+
try { effort = stepEffort({ arm, requested: e.effort, score: decision ? decision.score : null, on: await effortSwapOn($) }); } catch {}
|
|
576
|
+
}
|
|
577
|
+
const sent = model === e.model && effort === e.effort ? e : { ...e, model, ...(effort === undefined ? {} : { effort }) };
|
|
578
|
+
const result = yield* next(sent);
|
|
579
|
+
try { await noteStep($, result, { requested: want, arm, cold: isCold, score: decision ? decision.score : null, effort: sent.effort, effortAsked: e.effort }); } catch {} // a missed row only thins the dashboard
|
|
557
580
|
return result;
|
|
558
581
|
}
|
|
559
582
|
|
|
@@ -629,4 +652,4 @@ export function register(on) {
|
|
|
629
652
|
}
|
|
630
653
|
|
|
631
654
|
// Exported for tests only - not part of the mod's runtime contract.
|
|
632
|
-
export const _internal = { reportRow, noteStep, flushReport, pendingRows: () => pending, troxyConfig, askedFor, rememberAsked, asked, armCompact, cancelCompact, toDomainAndFields, guardBash, failClosed, parseScore, asRequest, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, withKept, statusText, compactPlan, savingsText, fetchSavings, scorePrompt, routeStep, stepModel, lastSent };
|
|
655
|
+
export const _internal = { stepEffort, reportRow, noteStep, flushReport, pendingRows: () => pending, troxyConfig, askedFor, rememberAsked, asked, armCompact, cancelCompact, toDomainAndFields, guardBash, failClosed, parseScore, asRequest, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, withKept, statusText, compactPlan, savingsText, fetchSavings, scorePrompt, routeStep, stepModel, lastSent };
|
|
@@ -403,3 +403,32 @@ test('usage report: a server error keeps the rows for the next turn', async () =
|
|
|
403
403
|
await flushReport($);
|
|
404
404
|
assert.equal(pendingRows().length, 0);
|
|
405
405
|
});
|
|
406
|
+
|
|
407
|
+
test('stepEffort: easy prompts drop to low, middle ones to medium, never raised, off unless on', () => {
|
|
408
|
+
const { stepEffort } = _internal;
|
|
409
|
+
const on = { arm: 'treated', on: true };
|
|
410
|
+
assert.equal(stepEffort({ ...on, requested: 'high', score: 0.1 }), 'low');
|
|
411
|
+
assert.equal(stepEffort({ ...on, requested: 'xhigh', score: 0.6 }), 'medium');
|
|
412
|
+
assert.equal(stepEffort({ ...on, requested: 'high', score: 0.9 }), 'high'); // hard: kept
|
|
413
|
+
assert.equal(stepEffort({ ...on, requested: 'low', score: 0.6 }), 'low'); // never raised
|
|
414
|
+
assert.equal(stepEffort({ ...on, requested: 'medium', score: 0.6 }), 'medium');
|
|
415
|
+
assert.equal(stepEffort({ ...on, requested: 'high', score: null }), 'high'); // no score: no change
|
|
416
|
+
assert.equal(stepEffort({ ...on, requested: undefined, score: 0.1 }), undefined); // model without effort
|
|
417
|
+
assert.equal(stepEffort({ ...on, requested: 16000, score: 0.1 }), 16000); // a numeric budget: left alone
|
|
418
|
+
assert.equal(stepEffort({ arm: 'holdout', on: true, requested: 'high', score: 0.1 }), 'high');
|
|
419
|
+
assert.equal(stepEffort({ arm: 'treated', on: false, requested: 'high', score: 0.1 }), 'high');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('routeStep: with the effort swap on, an easy prompt goes out at low effort and the row records it', async () => {
|
|
423
|
+
const { MODELS: M, routeStep, scorePrompt, pendingRows } = _internal;
|
|
424
|
+
const $ = { ...routingApi(),
|
|
425
|
+
env: { get: async (k) => (k === 'TROXY_EFFORT_SWAP' ? '1' : undefined) },
|
|
426
|
+
fs: { write: async () => {}, read: async () => { throw new Error('no config'); } } };
|
|
427
|
+
await scorePrompt($, { text: 'reply only ok', origin: { kind: 'composer' } }, async (e) => e);
|
|
428
|
+
let sent;
|
|
429
|
+
const gen = routeStep($, { model: M.opus, effort: 'high' }, async function* (e) { sent = e; return { usage: { model: e.model, output_tokens: 1 } }; });
|
|
430
|
+
for await (const _ of gen) { /* drain */ }
|
|
431
|
+
assert.equal(sent.effort, 'low');
|
|
432
|
+
assert.equal(pendingRows().at(-1).effort, 'low');
|
|
433
|
+
assert.equal(pendingRows().at(-1).effort_requested, 'high');
|
|
434
|
+
});
|