switchroom 0.16.5 → 0.16.6
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/dist/cli/switchroom.js +3 -3
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +32 -15
- package/telegram-plugin/gateway/gateway.ts +6 -0
- package/telegram-plugin/gateway/model-command.ts +32 -12
- package/telegram-plugin/tests/model-command.test.ts +31 -3
package/dist/cli/switchroom.js
CHANGED
|
@@ -51674,8 +51674,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
51674
51674
|
import { dirname, join } from "node:path";
|
|
51675
51675
|
|
|
51676
51676
|
// src/build-info.ts
|
|
51677
|
-
var VERSION = "0.16.
|
|
51678
|
-
var COMMIT_SHA = "
|
|
51677
|
+
var VERSION = "0.16.6";
|
|
51678
|
+
var COMMIT_SHA = "925f5798";
|
|
51679
51679
|
|
|
51680
51680
|
// src/cli/resolve-version.ts
|
|
51681
51681
|
function readPackageVersion() {
|
|
@@ -69841,7 +69841,7 @@ x-litellm-tags: service:hindsight`);
|
|
|
69841
69841
|
`--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
|
|
69842
69842
|
`--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
|
|
69843
69843
|
"--health-cmd",
|
|
69844
|
-
HINDSIGHT_HEALTHCHECK_CMD,
|
|
69844
|
+
litellm ? `python3 -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:${apiPort}/health",timeout=4).getcode()==200 else 1)'` : HINDSIGHT_HEALTHCHECK_CMD,
|
|
69845
69845
|
"--health-interval",
|
|
69846
69846
|
"30s",
|
|
69847
69847
|
"--health-timeout",
|
|
@@ -22587,7 +22587,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:f
|
|
|
22587
22587
|
import { dirname as dirname4, join as join2 } from "node:path";
|
|
22588
22588
|
|
|
22589
22589
|
// src/build-info.ts
|
|
22590
|
-
var VERSION = "0.16.
|
|
22590
|
+
var VERSION = "0.16.6";
|
|
22591
22591
|
|
|
22592
22592
|
// src/cli/resolve-version.ts
|
|
22593
22593
|
function readPackageVersion() {
|
package/package.json
CHANGED
|
@@ -46101,6 +46101,7 @@ var MODEL_CALLBACK_PREFIX = "mdl:";
|
|
|
46101
46101
|
var MODEL_CALLBACK_SELECT = "mdl:s:";
|
|
46102
46102
|
var MODEL_CALLBACK_REFRESH = "mdl:r";
|
|
46103
46103
|
var MODEL_CALLBACK_SR = "mdl:sr:";
|
|
46104
|
+
var MODEL_CALLBACK_HEADER = "mdl:h";
|
|
46104
46105
|
var SR_MODEL_LABELS = {
|
|
46105
46106
|
"sr-gemini-2.5-pro": "Gemini 2.5 Pro",
|
|
46106
46107
|
"sr-gemini-2.5-flash": "Gemini 2.5 Flash",
|
|
@@ -46126,20 +46127,28 @@ function busyReply(deps) {
|
|
|
46126
46127
|
html: true
|
|
46127
46128
|
};
|
|
46128
46129
|
}
|
|
46130
|
+
function headerRow(label) {
|
|
46131
|
+
return [{ text: label, callback_data: MODEL_CALLBACK_HEADER }];
|
|
46132
|
+
}
|
|
46129
46133
|
function menuKeyboard(claudeOptions, srOptions) {
|
|
46130
|
-
const
|
|
46131
|
-
|
|
46134
|
+
const hasBothGroups = claudeOptions.length > 0 && srOptions.length > 0;
|
|
46135
|
+
const rows = [];
|
|
46136
|
+
if (hasBothGroups)
|
|
46137
|
+
rows.push(headerRow("\u2500\u2500 Claude (Max / Pro subscription) \u2500\u2500"));
|
|
46138
|
+
for (const o of claudeOptions) {
|
|
46139
|
+
rows.push([{
|
|
46132
46140
|
text: o.current ? `\u2705 ${o.label}` : o.label,
|
|
46133
46141
|
callback_data: modelSelectCallbackData(o.label)
|
|
46134
|
-
}
|
|
46135
|
-
|
|
46136
|
-
|
|
46137
|
-
rows.push(
|
|
46138
|
-
|
|
46142
|
+
}]);
|
|
46143
|
+
}
|
|
46144
|
+
if (srOptions.length > 0) {
|
|
46145
|
+
rows.push(headerRow("\u2500\u2500 OpenRouter / external \u2500\u2500"));
|
|
46146
|
+
for (const o of srOptions) {
|
|
46147
|
+
rows.push([{
|
|
46139
46148
|
text: `\uD83C\uDF10 ${srFriendlyLabel(o.label)}`,
|
|
46140
46149
|
callback_data: `${MODEL_CALLBACK_SR}${o.label}`
|
|
46141
|
-
}
|
|
46142
|
-
|
|
46150
|
+
}]);
|
|
46151
|
+
}
|
|
46143
46152
|
}
|
|
46144
46153
|
rows.push([{ text: "\uD83D\uDD04 Refresh", callback_data: MODEL_CALLBACK_REFRESH }]);
|
|
46145
46154
|
return rows;
|
|
@@ -46174,8 +46183,9 @@ async function buildModelMenu(deps) {
|
|
|
46174
46183
|
if (quota)
|
|
46175
46184
|
lines.push(`Quota: ${deps.escapeHtml(quota)}`);
|
|
46176
46185
|
lines.push("", "Tap a model to switch the <b>live session</b>:");
|
|
46177
|
-
if (srOptions.length > 0)
|
|
46178
|
-
lines.push("\uD83C\uDF10
|
|
46186
|
+
if (srOptions.length > 0) {
|
|
46187
|
+
lines.push("Claude models use your Max/Pro subscription. \uD83C\uDF10 models are billed separately via OpenRouter.");
|
|
46188
|
+
}
|
|
46179
46189
|
lines.push(PERSIST_NOTE);
|
|
46180
46190
|
return { text: lines.join(`
|
|
46181
46191
|
`), html: true, keyboard: menuKeyboard(claudeOptions, srOptions) };
|
|
@@ -46184,6 +46194,9 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
46184
46194
|
if (data === MODEL_CALLBACK_REFRESH) {
|
|
46185
46195
|
return { answer: "Refreshed", reply: await buildModelMenu(deps) };
|
|
46186
46196
|
}
|
|
46197
|
+
if (data === MODEL_CALLBACK_HEADER) {
|
|
46198
|
+
return { answer: "Tap a model in this section to switch", reply: { text: "", html: true }, toastOnly: true };
|
|
46199
|
+
}
|
|
46187
46200
|
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
46188
46201
|
const srName = data.slice(MODEL_CALLBACK_SR.length);
|
|
46189
46202
|
if (!isValidModelArg(srName)) {
|
|
@@ -55998,10 +56011,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
55998
56011
|
}
|
|
55999
56012
|
|
|
56000
56013
|
// ../src/build-info.ts
|
|
56001
|
-
var VERSION = "0.16.
|
|
56002
|
-
var COMMIT_SHA = "
|
|
56003
|
-
var COMMIT_DATE = "2026-06-
|
|
56004
|
-
var LATEST_PR =
|
|
56014
|
+
var VERSION = "0.16.6";
|
|
56015
|
+
var COMMIT_SHA = "925f5798";
|
|
56016
|
+
var COMMIT_DATE = "2026-06-28T03:53:56Z";
|
|
56017
|
+
var LATEST_PR = 2611;
|
|
56005
56018
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
56006
56019
|
|
|
56007
56020
|
// gateway/boot-version.ts
|
|
@@ -68141,6 +68154,10 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
68141
68154
|
await ctx.answerCallbackQuery({ text: "\u23F3 Agent is mid-turn \u2014 tap again when it\u2019s idle", show_alert: false }).catch(() => {});
|
|
68142
68155
|
return;
|
|
68143
68156
|
}
|
|
68157
|
+
if (data === MODEL_CALLBACK_HEADER) {
|
|
68158
|
+
await ctx.answerCallbackQuery({ text: "Tap a model in this section to switch" }).catch(() => {});
|
|
68159
|
+
return;
|
|
68160
|
+
}
|
|
68144
68161
|
await ctx.answerCallbackQuery({ text: "Switching\u2026" }).catch(() => {});
|
|
68145
68162
|
try {
|
|
68146
68163
|
const outcome = await handleModelMenuCallback(data, modelDeps);
|
|
@@ -276,6 +276,7 @@ import {
|
|
|
276
276
|
buildModelMenu,
|
|
277
277
|
handleModelMenuCallback,
|
|
278
278
|
MODEL_CALLBACK_PREFIX,
|
|
279
|
+
MODEL_CALLBACK_HEADER,
|
|
279
280
|
type ModelMenuDeps,
|
|
280
281
|
type ModelCommandDeps,
|
|
281
282
|
type ModelMenuReply,
|
|
@@ -20721,6 +20722,11 @@ bot.on('callback_query:data', async ctx => {
|
|
|
20721
20722
|
// drive behind the pane lock. A callback can only be answered once, so
|
|
20722
20723
|
// the rich result (what was set / why it failed) is conveyed by the
|
|
20723
20724
|
// message edit — which now ALWAYS keeps the menu buttons.
|
|
20725
|
+
// Header rows are section labels — informational, no model switch.
|
|
20726
|
+
if (data === MODEL_CALLBACK_HEADER) {
|
|
20727
|
+
await ctx.answerCallbackQuery({ text: 'Tap a model in this section to switch' }).catch(() => {})
|
|
20728
|
+
return
|
|
20729
|
+
}
|
|
20724
20730
|
await ctx.answerCallbackQuery({ text: 'Switching…' }).catch(() => {})
|
|
20725
20731
|
try {
|
|
20726
20732
|
const outcome = await handleModelMenuCallback(data, modelDeps)
|
|
@@ -235,6 +235,8 @@ const MODEL_CALLBACK_SELECT = 'mdl:s:'
|
|
|
235
235
|
export const MODEL_CALLBACK_REFRESH = 'mdl:r'
|
|
236
236
|
/** Callback prefix for sr-* (LiteLLM non-Anthropic) model selection. */
|
|
237
237
|
export const MODEL_CALLBACK_SR = 'mdl:sr:'
|
|
238
|
+
/** Callback for section-header rows — shows an informational toast, no action. */
|
|
239
|
+
export const MODEL_CALLBACK_HEADER = 'mdl:h'
|
|
238
240
|
|
|
239
241
|
/**
|
|
240
242
|
* Friendly display names for sr-* synthetic model names. An sr-* model in
|
|
@@ -296,29 +298,38 @@ function busyReply(deps: Pick<ModelMenuDeps, 'escapeHtml'>): ModelMenuReply {
|
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
|
|
301
|
+
function headerRow(label: string): ModelMenuKeyboardButton[] {
|
|
302
|
+
return [{ text: label, callback_data: MODEL_CALLBACK_HEADER }]
|
|
303
|
+
}
|
|
304
|
+
|
|
299
305
|
function menuKeyboard(
|
|
300
306
|
claudeOptions: ModelPickerOption[],
|
|
301
307
|
srOptions: ModelPickerOption[],
|
|
302
308
|
): ModelMenuKeyboardButton[][] {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
309
|
+
const hasBothGroups = claudeOptions.length > 0 && srOptions.length > 0
|
|
310
|
+
const rows: ModelMenuKeyboardButton[][] = []
|
|
311
|
+
|
|
312
|
+
if (hasBothGroups) rows.push(headerRow('── Claude (Max / Pro subscription) ──'))
|
|
313
|
+
for (const o of claudeOptions) {
|
|
314
|
+
rows.push([{
|
|
307
315
|
text: o.current ? `✅ ${o.label}` : o.label,
|
|
308
316
|
callback_data: modelSelectCallbackData(o.label),
|
|
309
|
-
}
|
|
310
|
-
|
|
317
|
+
}])
|
|
318
|
+
}
|
|
319
|
+
|
|
311
320
|
// sr-* models are non-Anthropic (routed via LiteLLM → OpenRouter).
|
|
312
321
|
// Selection uses text-inject rather than cursor-nav — more reliable
|
|
313
322
|
// when the picker has many models (GATEWAY_MODEL_DISCOVERY=1).
|
|
314
|
-
|
|
315
|
-
rows.push(
|
|
316
|
-
|
|
323
|
+
if (srOptions.length > 0) {
|
|
324
|
+
rows.push(headerRow('── OpenRouter / external ──'))
|
|
325
|
+
for (const o of srOptions) {
|
|
326
|
+
rows.push([{
|
|
317
327
|
text: `🌐 ${srFriendlyLabel(o.label)}`,
|
|
318
328
|
callback_data: `${MODEL_CALLBACK_SR}${o.label}`,
|
|
319
|
-
}
|
|
320
|
-
|
|
329
|
+
}])
|
|
330
|
+
}
|
|
321
331
|
}
|
|
332
|
+
|
|
322
333
|
rows.push([{ text: '🔄 Refresh', callback_data: MODEL_CALLBACK_REFRESH }])
|
|
323
334
|
return rows
|
|
324
335
|
}
|
|
@@ -367,7 +378,9 @@ export async function buildModelMenu(
|
|
|
367
378
|
}
|
|
368
379
|
if (quota) lines.push(`Quota: ${deps.escapeHtml(quota)}`)
|
|
369
380
|
lines.push('', 'Tap a model to switch the <b>live session</b>:')
|
|
370
|
-
if (srOptions.length > 0)
|
|
381
|
+
if (srOptions.length > 0) {
|
|
382
|
+
lines.push('Claude models use your Max/Pro subscription. 🌐 models are billed separately via OpenRouter.')
|
|
383
|
+
}
|
|
371
384
|
lines.push(PERSIST_NOTE)
|
|
372
385
|
|
|
373
386
|
return { text: lines.join('\n'), html: true, keyboard: menuKeyboard(claudeOptions, srOptions) }
|
|
@@ -410,6 +423,13 @@ export async function handleModelMenuCallback(
|
|
|
410
423
|
return { answer: 'Refreshed', reply: await buildModelMenu(deps) }
|
|
411
424
|
}
|
|
412
425
|
|
|
426
|
+
if (data === MODEL_CALLBACK_HEADER) {
|
|
427
|
+
// Section-header row — the gateway handles this with a direct answerCallbackQuery
|
|
428
|
+
// before calling this function, so this branch is dead in practice. Guard
|
|
429
|
+
// for callers that skip gateway.ts (tests, future refactors).
|
|
430
|
+
return { answer: 'Tap a model in this section to switch', reply: { text: '', html: true }, toastOnly: true }
|
|
431
|
+
}
|
|
432
|
+
|
|
413
433
|
// sr-* model tap: text-inject `/model sr-<name>` rather than cursor-nav.
|
|
414
434
|
// Text-inject is more reliable when the picker has many models; sr-* names
|
|
415
435
|
// are safe (no entry in model_group_settings → no OAuth forwarding). See I6.
|
|
@@ -255,6 +255,7 @@ import {
|
|
|
255
255
|
sessionModelFromConfirmation,
|
|
256
256
|
classifyDiscoveredOptions,
|
|
257
257
|
MODEL_CALLBACK_REFRESH,
|
|
258
|
+
MODEL_CALLBACK_HEADER,
|
|
258
259
|
MODEL_CALLBACK_SR,
|
|
259
260
|
SR_MODEL_LABELS,
|
|
260
261
|
type ModelMenuDeps,
|
|
@@ -506,16 +507,43 @@ describe("buildModelMenu — with sr-* models", () => {
|
|
|
506
507
|
expect(srButton?.callback_data).toBe(`${MODEL_CALLBACK_SR}sr-gemini-2.5-pro`);
|
|
507
508
|
});
|
|
508
509
|
|
|
509
|
-
it("shows
|
|
510
|
+
it("shows section header rows when both claude and sr-* models present", async () => {
|
|
510
511
|
const { deps } = makeMenuDepsWithSr();
|
|
511
512
|
const menu = await buildModelMenu(deps);
|
|
512
|
-
|
|
513
|
+
const allButtons = menu.keyboard!.flat();
|
|
514
|
+
const headers = allButtons.filter((b) => b.callback_data === MODEL_CALLBACK_HEADER);
|
|
515
|
+
expect(headers.length).toBe(2);
|
|
516
|
+
expect(headers[0].text).toContain("Claude");
|
|
517
|
+
expect(headers[1].text).toContain("OpenRouter");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("no section headers when only claude models (no sr-*)", async () => {
|
|
521
|
+
const { deps } = makeMenuDeps();
|
|
522
|
+
const menu = await buildModelMenu(deps);
|
|
523
|
+
const allButtons = (menu.keyboard ?? []).flat();
|
|
524
|
+
const headers = allButtons.filter((b) => b.callback_data === MODEL_CALLBACK_HEADER);
|
|
525
|
+
expect(headers.length).toBe(0);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("header-row tap returns toastOnly without inject or model change", async () => {
|
|
529
|
+
const { deps, injectCalls } = makeMenuDepsWithSr();
|
|
530
|
+
const out = await handleModelMenuCallback(MODEL_CALLBACK_HEADER, deps);
|
|
531
|
+
expect(out.toastOnly).toBe(true);
|
|
532
|
+
expect(out.selectedModel).toBeUndefined();
|
|
533
|
+
expect(injectCalls).toHaveLength(0);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("shows subscription/OpenRouter legend when sr-* models are present", async () => {
|
|
537
|
+
const { deps } = makeMenuDepsWithSr();
|
|
538
|
+
const menu = await buildModelMenu(deps);
|
|
539
|
+
expect(menu.text).toContain("Max/Pro subscription");
|
|
540
|
+
expect(menu.text).toContain("OpenRouter");
|
|
513
541
|
});
|
|
514
542
|
|
|
515
543
|
it("no legend when no sr-* models in picker", async () => {
|
|
516
544
|
const { deps } = makeMenuDeps();
|
|
517
545
|
const menu = await buildModelMenu(deps);
|
|
518
|
-
expect(menu.text).not.toContain("
|
|
546
|
+
expect(menu.text).not.toContain("OpenRouter");
|
|
519
547
|
});
|
|
520
548
|
});
|
|
521
549
|
|