troxy-cli 1.29.44 → 1.29.45
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.
|
|
3
|
+
"version": "1.29.45",
|
|
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": {
|
|
@@ -309,12 +309,20 @@ async function scorePrompt($, e, next) {
|
|
|
309
309
|
return next(e);
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
// The model the session last asked for, per session, so a /model switch is seen.
|
|
313
|
+
const lastAsked = new Map();
|
|
314
|
+
|
|
312
315
|
async function* routeStep($, e, next) {
|
|
313
316
|
// Main loop only: a subagent keeps whatever model it was given.
|
|
314
317
|
if (!decision || e.agentId) return yield* next(e);
|
|
318
|
+
// The user picked a different model since the last turn: their choice wins
|
|
319
|
+
// for this turn (the proxy does the same, route_reason 'user_switch').
|
|
320
|
+
const sid = await $.session.id();
|
|
321
|
+
const prev = lastAsked.get(sid);
|
|
322
|
+
lastAsked.set(sid, e.model);
|
|
323
|
+
if (prev && prev !== e.model) { decision = null; return yield* next(e); }
|
|
315
324
|
const model = pickModel(decision.score, e.model, decision.tokens);
|
|
316
325
|
if (model !== e.model) {
|
|
317
|
-
const sid = await $.session.id();
|
|
318
326
|
const file = modRouteFile($.plugin.root, sid);
|
|
319
327
|
const key = `${sid}:${e.model}>${model}`;
|
|
320
328
|
if (file && lastNote !== key) {
|
|
@@ -388,4 +396,4 @@ export function register(on) {
|
|
|
388
396
|
}
|
|
389
397
|
|
|
390
398
|
// Exported for tests only - not part of the mod's runtime contract.
|
|
391
|
-
export const _internal = { toDomainAndFields, guardBash, failClosed, parseScore, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, statusText, compactPlan, savingsText, fetchSavings };
|
|
399
|
+
export const _internal = { toDomainAndFields, guardBash, failClosed, parseScore, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, statusText, compactPlan, savingsText, fetchSavings, scorePrompt, routeStep };
|
|
@@ -142,3 +142,36 @@ test('fetchSavings: sends the saved key and the session id; null without a key o
|
|
|
142
142
|
assert.equal(await fetchSavings(mk({ env: { HOME: '/h' }, config: {} })), null);
|
|
143
143
|
assert.equal(await fetchSavings({ ...mk(), session: { id: async () => 'x' } }), null);
|
|
144
144
|
});
|
|
145
|
+
|
|
146
|
+
// A fake $ just big enough for scorePrompt + routeStep: the scorer says "easy"
|
|
147
|
+
// (score 0.1) and the context is small, so the mod would route down to haiku.
|
|
148
|
+
function routingApi() {
|
|
149
|
+
const kv = new Map();
|
|
150
|
+
return {
|
|
151
|
+
state: { get: async (k) => ({ value: kv.get(k.key) }), set: async (k, v) => kv.set(k.key, v) },
|
|
152
|
+
store: { get: async () => undefined },
|
|
153
|
+
session: { id: async () => 'sess-switch-test', usage: async () => ({ context: { tokens: 1000 }, rateLimits: [] }) },
|
|
154
|
+
plugin: { root: '/tmp/not-installed' },
|
|
155
|
+
model: { complete: async () => ({ isAnswered: true, text: '0.1' }) },
|
|
156
|
+
ui: { log: () => {} },
|
|
157
|
+
clock: { now: async () => 0 },
|
|
158
|
+
fs: { write: async () => {} },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runTurn($, model) {
|
|
163
|
+
const { scorePrompt, routeStep } = _internal;
|
|
164
|
+
await scorePrompt($, { text: 'reply only ok', origin: { kind: 'composer' } }, async (e) => e);
|
|
165
|
+
let sent;
|
|
166
|
+
const gen = routeStep($, { model }, async function* (e) { sent = e.model; });
|
|
167
|
+
for await (const _ of gen) { /* drain */ }
|
|
168
|
+
return sent;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
test('routeStep: a /model switch is honored for that turn, then routing resumes', async () => {
|
|
172
|
+
const $ = routingApi();
|
|
173
|
+
const opus = _internal.MODELS.opus, sonnet = _internal.MODELS.sonnet, haiku = _internal.MODELS.haiku;
|
|
174
|
+
assert.equal(await runTurn($, opus), haiku); // first turn: routed down as before
|
|
175
|
+
assert.equal(await runTurn($, sonnet), sonnet); // user picked sonnet: kept
|
|
176
|
+
assert.equal(await runTurn($, sonnet), haiku); // same choice again: routing resumes
|
|
177
|
+
});
|