switchroom 0.16.6 → 0.16.7
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 +2 -2
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +33 -0
- package/telegram-plugin/dist/gateway/gateway.js +30 -7
- package/telegram-plugin/gateway/gateway.ts +21 -0
- package/telegram-plugin/gateway/model-command.ts +11 -2
- package/telegram-plugin/tests/model-command.test.ts +4 -10
- package/telegram-plugin/uat/scenarios/jtbd-model-litellm-sr-dm.test.ts +147 -0
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.7";
|
|
51678
|
+
var COMMIT_SHA = "696eea91";
|
|
51679
51679
|
|
|
51680
51680
|
// src/cli/resolve-version.ts
|
|
51681
51681
|
function readPackageVersion() {
|
|
@@ -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.7";
|
|
22591
22591
|
|
|
22592
22592
|
// src/cli/resolve-version.ts
|
|
22593
22593
|
function readPackageVersion() {
|
package/package.json
CHANGED
|
@@ -86,6 +86,39 @@ CRON_APPEND_PROMPT="You are the cheap background cron worker for {{name}}. You r
|
|
|
86
86
|
# (no --continue) — low context by construction. cd into the workspace so
|
|
87
87
|
# claude's project key matches the pre-seeded trust state (.claude-cron).
|
|
88
88
|
cd "{{agentDir}}" || exit 1
|
|
89
|
+
|
|
90
|
+
# LiteLLM routing for the cron session — mirrors start.sh's boot block.
|
|
91
|
+
# IMPORTANT: the vault key is provisioned under the BASE agent name ({{name}}),
|
|
92
|
+
# NOT the cron identity ({{name}}-cron). Use the Handlebars template var so the
|
|
93
|
+
# lookup is a compile-time literal — no fragile runtime suffix-stripping.
|
|
94
|
+
# Attribution headers carry $SWITCHROOM_AGENT_NAME (= {{name}}-cron) so LiteLLM
|
|
95
|
+
# can distinguish cron spend from main-session spend per agent.
|
|
96
|
+
# FAIL-OPEN: missing key OR unreachable proxy → strip routing env, fall back to
|
|
97
|
+
# direct OAuth. An outage must never take the cron session dark.
|
|
98
|
+
if [ -n "$SWITCHROOM_LITELLM" ] && command -v switchroom >/dev/null 2>&1; then
|
|
99
|
+
sr_ll_key="$(switchroom vault get "litellm/{{name}}/api-key" 2>/dev/null || true)"
|
|
100
|
+
sr_ll_ok=""
|
|
101
|
+
if [ -z "$sr_ll_key" ]; then
|
|
102
|
+
echo "litellm: no virtual key for cron '{{name}}' — falling back to direct OAuth (no tracking/guardrail)" >&2
|
|
103
|
+
elif command -v curl >/dev/null 2>&1 && [ -n "$ANTHROPIC_BASE_URL" ] \
|
|
104
|
+
&& ! curl -fsS -m 5 -o /dev/null "${ANTHROPIC_BASE_URL%/}/health/liveliness" 2>/dev/null; then
|
|
105
|
+
echo "litellm: proxy unreachable at $ANTHROPIC_BASE_URL — falling back to direct OAuth (no tracking/guardrail this session)" >&2
|
|
106
|
+
else
|
|
107
|
+
sr_ll_ok="1"
|
|
108
|
+
fi
|
|
109
|
+
if [ -n "$sr_ll_ok" ]; then
|
|
110
|
+
export ANTHROPIC_CUSTOM_HEADERS="x-litellm-api-key: Bearer $sr_ll_key
|
|
111
|
+
x-litellm-customer-id: $SWITCHROOM_AGENT_NAME
|
|
112
|
+
x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:-default}"
|
|
113
|
+
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
|
|
114
|
+
else
|
|
115
|
+
# Fail-open: drop every routing var so the claude CLI talks to Anthropic
|
|
116
|
+
# directly on its OAuth credential (subscription path), unproxied.
|
|
117
|
+
unset ANTHROPIC_BASE_URL ANTHROPIC_SMALL_FAST_MODEL SWITCHROOM_LITELLM
|
|
118
|
+
fi
|
|
119
|
+
unset sr_ll_key sr_ll_ok
|
|
120
|
+
fi
|
|
121
|
+
|
|
89
122
|
# Create the cron tmux session DETACHED (-d), not in attach mode. This is a
|
|
90
123
|
# SUPERVISED BACKGROUND sidecar (start.sh forks it with `&`) with no controlling
|
|
91
124
|
# TTY — the MAIN session owns the container's foreground TTY. The attach flag
|
|
@@ -46156,9 +46156,10 @@ function menuKeyboard(claudeOptions, srOptions) {
|
|
|
46156
46156
|
async function buildModelMenu(deps) {
|
|
46157
46157
|
if (deps.isBusy())
|
|
46158
46158
|
return busyReply(deps);
|
|
46159
|
-
const [discovered, quota] = await Promise.all([
|
|
46159
|
+
const [discovered, quota, srNames] = await Promise.all([
|
|
46160
46160
|
deps.discover(deps.getAgentName()),
|
|
46161
|
-
deps.getQuotaBrief().catch(() => null)
|
|
46161
|
+
deps.getQuotaBrief().catch(() => null),
|
|
46162
|
+
deps.discoverSrModels().catch(() => [])
|
|
46162
46163
|
]);
|
|
46163
46164
|
if (!discovered.ok) {
|
|
46164
46165
|
const v1 = await handleModelCommand({ kind: "show" }, deps);
|
|
@@ -46168,7 +46169,8 @@ async function buildModelMenu(deps) {
|
|
|
46168
46169
|
html: true
|
|
46169
46170
|
};
|
|
46170
46171
|
}
|
|
46171
|
-
const { claude: claudeOptions
|
|
46172
|
+
const { claude: claudeOptions } = classifyDiscoveredOptions(discovered.options);
|
|
46173
|
+
const srOptions = srNames.map((name, i) => ({ index: i, label: name, detail: "", current: false }));
|
|
46172
46174
|
const current = claudeOptions.find((o) => o.current);
|
|
46173
46175
|
const lines = [`<b>Model \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`];
|
|
46174
46176
|
if (discovered.dismissFailed) {
|
|
@@ -56011,10 +56013,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
56011
56013
|
}
|
|
56012
56014
|
|
|
56013
56015
|
// ../src/build-info.ts
|
|
56014
|
-
var VERSION = "0.16.
|
|
56015
|
-
var COMMIT_SHA = "
|
|
56016
|
-
var COMMIT_DATE = "2026-06-
|
|
56017
|
-
var LATEST_PR =
|
|
56016
|
+
var VERSION = "0.16.7";
|
|
56017
|
+
var COMMIT_SHA = "696eea91";
|
|
56018
|
+
var COMMIT_DATE = "2026-06-28T04:56:27Z";
|
|
56019
|
+
var LATEST_PR = 2615;
|
|
56018
56020
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
56019
56021
|
|
|
56020
56022
|
// gateway/boot-version.ts
|
|
@@ -65208,6 +65210,27 @@ bot.command("clear", async (ctx) => {
|
|
|
65208
65210
|
function buildModelDeps() {
|
|
65209
65211
|
return {
|
|
65210
65212
|
discover: (a) => discoverModels(a),
|
|
65213
|
+
discoverSrModels: async () => {
|
|
65214
|
+
const base = process.env.ANTHROPIC_BASE_URL;
|
|
65215
|
+
const headers = process.env.ANTHROPIC_CUSTOM_HEADERS;
|
|
65216
|
+
if (!base || !headers)
|
|
65217
|
+
return [];
|
|
65218
|
+
const keyMatch = headers.match(/x-litellm-api-key:\s*Bearer\s*(\S+)/);
|
|
65219
|
+
if (!keyMatch)
|
|
65220
|
+
return [];
|
|
65221
|
+
try {
|
|
65222
|
+
const res = await fetch(`${base.replace(/\/$/, "")}/model/info`, {
|
|
65223
|
+
headers: { Authorization: `Bearer ${keyMatch[1]}` },
|
|
65224
|
+
signal: AbortSignal.timeout(5000)
|
|
65225
|
+
});
|
|
65226
|
+
if (!res.ok)
|
|
65227
|
+
return [];
|
|
65228
|
+
const data = await res.json();
|
|
65229
|
+
return (data.data ?? []).map((m) => m.model_name).filter((n) => n.startsWith("sr-")).sort();
|
|
65230
|
+
} catch {
|
|
65231
|
+
return [];
|
|
65232
|
+
}
|
|
65233
|
+
},
|
|
65211
65234
|
select: (a, label) => selectModel(a, label),
|
|
65212
65235
|
isBusy: () => currentTurn !== null,
|
|
65213
65236
|
getAgentName: getMyAgentName,
|
|
@@ -15981,6 +15981,27 @@ bot.command('clear', async ctx => {
|
|
|
15981
15981
|
function buildModelDeps(): ModelMenuDeps & ModelCommandDeps {
|
|
15982
15982
|
return {
|
|
15983
15983
|
discover: (a) => discoverModels(a),
|
|
15984
|
+
discoverSrModels: async () => {
|
|
15985
|
+
const base = process.env.ANTHROPIC_BASE_URL
|
|
15986
|
+
const headers = process.env.ANTHROPIC_CUSTOM_HEADERS
|
|
15987
|
+
if (!base || !headers) return []
|
|
15988
|
+
const keyMatch = headers.match(/x-litellm-api-key:\s*Bearer\s*(\S+)/)
|
|
15989
|
+
if (!keyMatch) return []
|
|
15990
|
+
try {
|
|
15991
|
+
const res = await fetch(`${base.replace(/\/$/, '')}/model/info`, {
|
|
15992
|
+
headers: { Authorization: `Bearer ${keyMatch[1]}` },
|
|
15993
|
+
signal: AbortSignal.timeout(5000),
|
|
15994
|
+
})
|
|
15995
|
+
if (!res.ok) return []
|
|
15996
|
+
const data = (await res.json()) as { data?: Array<{ model_name: string }> }
|
|
15997
|
+
return (data.data ?? [])
|
|
15998
|
+
.map((m) => m.model_name)
|
|
15999
|
+
.filter((n) => n.startsWith('sr-'))
|
|
16000
|
+
.sort()
|
|
16001
|
+
} catch {
|
|
16002
|
+
return []
|
|
16003
|
+
}
|
|
16004
|
+
},
|
|
15984
16005
|
select: (a, label) => selectModel(a, label),
|
|
15985
16006
|
isBusy: () => currentTurn !== null,
|
|
15986
16007
|
getAgentName: getMyAgentName,
|
|
@@ -214,6 +214,11 @@ export interface ModelMenuDeps {
|
|
|
214
214
|
getAgentName: () => string
|
|
215
215
|
/** One-line quota summary (e.g. "29% / 5h · 33% / 7d") or null. */
|
|
216
216
|
getQuotaBrief: () => Promise<string | null>
|
|
217
|
+
/**
|
|
218
|
+
* Fetch sr-* model names available via LiteLLM for this agent.
|
|
219
|
+
* Returns [] when LiteLLM is not configured or the probe fails.
|
|
220
|
+
*/
|
|
221
|
+
discoverSrModels: () => Promise<string[]>
|
|
217
222
|
escapeHtml: (s: string) => string
|
|
218
223
|
}
|
|
219
224
|
|
|
@@ -344,9 +349,10 @@ export async function buildModelMenu(
|
|
|
344
349
|
): Promise<ModelMenuReply> {
|
|
345
350
|
if (deps.isBusy()) return busyReply(deps)
|
|
346
351
|
|
|
347
|
-
const [discovered, quota] = await Promise.all([
|
|
352
|
+
const [discovered, quota, srNames] = await Promise.all([
|
|
348
353
|
deps.discover(deps.getAgentName()),
|
|
349
354
|
deps.getQuotaBrief().catch(() => null),
|
|
355
|
+
deps.discoverSrModels().catch(() => [] as string[]),
|
|
350
356
|
])
|
|
351
357
|
|
|
352
358
|
if (!discovered.ok) {
|
|
@@ -364,7 +370,10 @@ export async function buildModelMenu(
|
|
|
364
370
|
// or a prior session switch). Labelling the ✔ row "Now:" was misleading —
|
|
365
371
|
// it could read "Opus 4.8" while the live session is on Fable. Call it what
|
|
366
372
|
// it is, and tell the operator a switch applies to the live session.
|
|
367
|
-
|
|
373
|
+
// sr-* models come from LiteLLM (/model/info via discoverSrModels), not the
|
|
374
|
+
// claude picker — the CLI only knows Anthropic models.
|
|
375
|
+
const { claude: claudeOptions } = classifyDiscoveredOptions(discovered.options)
|
|
376
|
+
const srOptions: ModelPickerOption[] = srNames.map((name, i) => ({ index: i, label: name, detail: '', current: false }))
|
|
368
377
|
const current = claudeOptions.find((o) => o.current)
|
|
369
378
|
const lines: string[] = [`<b>Model — ${deps.escapeHtml(deps.getAgentName())}</b>`]
|
|
370
379
|
if (discovered.dismissFailed) {
|
|
@@ -283,6 +283,7 @@ function makeMenuDeps(overrides: Partial<ModelMenuDeps> = {}) {
|
|
|
283
283
|
},
|
|
284
284
|
isBusy: () => false,
|
|
285
285
|
getQuotaBrief: async () => "29% / 5h · 33% / 7d",
|
|
286
|
+
discoverSrModels: async () => [],
|
|
286
287
|
...overrides,
|
|
287
288
|
};
|
|
288
289
|
return { deps, calls, injectCalls: base.calls };
|
|
@@ -475,13 +476,10 @@ describe("SR_MODEL_LABELS", () => {
|
|
|
475
476
|
});
|
|
476
477
|
|
|
477
478
|
describe("buildModelMenu — with sr-* models", () => {
|
|
479
|
+
// sr-* models now come from discoverSrModels (LiteLLM), not the claude picker.
|
|
478
480
|
function makeMenuDepsWithSr(overrides: Partial<ModelMenuDeps> = {}) {
|
|
479
481
|
return makeMenuDeps({
|
|
480
|
-
|
|
481
|
-
ok: true as const,
|
|
482
|
-
options: OPTIONS_WITH_SR,
|
|
483
|
-
currentLabel: "Sonnet",
|
|
484
|
-
}),
|
|
482
|
+
discoverSrModels: async () => ["sr-gemini-2.5-pro", "sr-deepseek-r1"],
|
|
485
483
|
...overrides,
|
|
486
484
|
});
|
|
487
485
|
}
|
|
@@ -550,11 +548,7 @@ describe("buildModelMenu — with sr-* models", () => {
|
|
|
550
548
|
describe("handleModelMenuCallback — sr-* selection", () => {
|
|
551
549
|
function makeMenuDepsWithSr(overrides: Partial<ModelMenuDeps> = {}) {
|
|
552
550
|
return makeMenuDeps({
|
|
553
|
-
|
|
554
|
-
ok: true as const,
|
|
555
|
-
options: OPTIONS_WITH_SR,
|
|
556
|
-
currentLabel: "Sonnet",
|
|
557
|
-
}),
|
|
551
|
+
discoverSrModels: async () => ["sr-gemini-2.5-pro", "sr-deepseek-r1"],
|
|
558
552
|
...overrides,
|
|
559
553
|
});
|
|
560
554
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UAT — /model with LiteLLM sr-* (OpenRouter) model switching + spend tracking.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* 1. /model menu shows section headers ("Claude (Max / Pro subscription)" and
|
|
6
|
+
* "OpenRouter / external") when sr-* models are available.
|
|
7
|
+
* 2. Tapping an sr-* button switches the live session (text-inject path,
|
|
8
|
+
* not cursor-nav) and the confirmation banner appears.
|
|
9
|
+
* 3. After switching, the agent replies on the new model, and LiteLLM
|
|
10
|
+
* spend logs show agent:test-harness attribution.
|
|
11
|
+
* 4. Session resets to the configured model on restart (out of scope for
|
|
12
|
+
* this test — asserted in jtbd-always-on-after-restart-dm).
|
|
13
|
+
*
|
|
14
|
+
* Self-skips green when:
|
|
15
|
+
* - SWITCHROOM_UAT_DRIVER_SESSION is not set (no Telegram driver)
|
|
16
|
+
* - LiteLLM admin key unavailable (SWITCHROOM_UAT_LITELLM_ADMIN_KEY unset)
|
|
17
|
+
* - No sr-* buttons found in the menu (agent not LiteLLM-enabled or no
|
|
18
|
+
* sr-* models registered in LiteLLM)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
import { spinUp } from "../harness.js";
|
|
23
|
+
|
|
24
|
+
const AGENT = "test-harness";
|
|
25
|
+
const LITELLM_URL = process.env.SWITCHROOM_UAT_LITELLM_URL ?? "http://127.0.0.1:4010";
|
|
26
|
+
const LITELLM_ADMIN_KEY = process.env.SWITCHROOM_UAT_LITELLM_ADMIN_KEY ?? "";
|
|
27
|
+
|
|
28
|
+
async function getLiteLLMSpendForAgent(agent: string): Promise<number> {
|
|
29
|
+
if (!LITELLM_ADMIN_KEY) return -1;
|
|
30
|
+
const res = await fetch(`${LITELLM_URL}/spend/tags`, {
|
|
31
|
+
headers: { Authorization: `Bearer ${LITELLM_ADMIN_KEY}` },
|
|
32
|
+
}).catch(() => null);
|
|
33
|
+
if (!res?.ok) return -1;
|
|
34
|
+
const tags: Array<{ individual_request_tag: string; log_count: number }> = await res.json();
|
|
35
|
+
return tags.find((t) => t.individual_request_tag === `agent:${agent}`)?.log_count ?? 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("uat: /model sr-* LiteLLM routing — section headers + session switch + spend attribution", () => {
|
|
39
|
+
it(
|
|
40
|
+
"menu shows Claude/OpenRouter section headers, sr-* tap switches session and LiteLLM logs the request",
|
|
41
|
+
async () => {
|
|
42
|
+
const sc = await spinUp({ agent: AGENT });
|
|
43
|
+
try {
|
|
44
|
+
await sc.sendDM("/model");
|
|
45
|
+
const menu = await sc.expectMessage(/Default \(new sessions\):/i, {
|
|
46
|
+
from: "bot",
|
|
47
|
+
timeout: 30_000,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ── 1. Section headers ──────────────────────────────────────────
|
|
51
|
+
const kb = await sc.driver.getKeyboard(sc.botUserId, menu.messageId);
|
|
52
|
+
const flat = (kb ?? []).flat().filter((b) => b.callbackData);
|
|
53
|
+
|
|
54
|
+
const claudeHeader = flat.find(
|
|
55
|
+
(b) => b.text.includes("Claude") && b.text.includes("subscription") && b.callbackData === "mdl:h",
|
|
56
|
+
);
|
|
57
|
+
const openrouterHeader = flat.find(
|
|
58
|
+
(b) => b.text.includes("OpenRouter") && b.callbackData === "mdl:h",
|
|
59
|
+
);
|
|
60
|
+
const srButton = flat.find((b) => b.callbackData?.startsWith("mdl:sr:"));
|
|
61
|
+
|
|
62
|
+
if (!srButton) {
|
|
63
|
+
console.log("No sr-* buttons in menu — agent not LiteLLM-enabled or no sr-* models registered. Skipping.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
expect(claudeHeader, "Claude (Max / Pro subscription) header row").toBeDefined();
|
|
68
|
+
expect(openrouterHeader, "OpenRouter / external header row").toBeDefined();
|
|
69
|
+
expect(menu.text).toContain("Max/Pro subscription");
|
|
70
|
+
expect(menu.text).toContain("OpenRouter");
|
|
71
|
+
|
|
72
|
+
// ── 2. sr-* switch ─────────────────────────────────────────────
|
|
73
|
+
const spendBefore = await getLiteLLMSpendForAgent(AGENT);
|
|
74
|
+
const srName = srButton.callbackData!.replace("mdl:sr:", "");
|
|
75
|
+
|
|
76
|
+
await sc.driver.pressButton(sc.botUserId, menu.messageId, srButton.callbackData!);
|
|
77
|
+
// Allow text-inject + claude's /model response to propagate
|
|
78
|
+
await new Promise((r) => setTimeout(r, 8_000));
|
|
79
|
+
|
|
80
|
+
const afterMenu = await sc.driver.getMessage(sc.botUserId, menu.messageId);
|
|
81
|
+
expect(afterMenu?.text ?? "", "confirmation banner after sr-* tap").toMatch(
|
|
82
|
+
/Set model to|Switched|session/i,
|
|
83
|
+
);
|
|
84
|
+
// Card must keep its buttons (#2270 — no dead card)
|
|
85
|
+
const kbAfter = await sc.driver.getKeyboard(sc.botUserId, menu.messageId);
|
|
86
|
+
expect((kbAfter ?? []).flat().length, "menu keeps buttons after sr-* tap").toBeGreaterThan(0);
|
|
87
|
+
|
|
88
|
+
// ── 3. Send a quick message to generate a LiteLLM-routed turn ──
|
|
89
|
+
await sc.sendDM("Just reply with the word OK.");
|
|
90
|
+
await sc.expectMessage(/ok/i, { from: "bot", timeout: 60_000 });
|
|
91
|
+
|
|
92
|
+
// ── 4. LiteLLM spend attribution ────────────────────────────────
|
|
93
|
+
if (spendBefore >= 0) {
|
|
94
|
+
// Give LiteLLM a moment to flush the log
|
|
95
|
+
await new Promise((r) => setTimeout(r, 3_000));
|
|
96
|
+
const spendAfter = await getLiteLLMSpendForAgent(AGENT);
|
|
97
|
+
expect(spendAfter, `agent:${AGENT} log_count increased after turn`).toBeGreaterThan(spendBefore);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Restore: switch back to configured model ────────────────────
|
|
101
|
+
const currentKb = await sc.driver.getKeyboard(sc.botUserId, menu.messageId);
|
|
102
|
+
const restoreBtn = (currentKb ?? []).flat().find(
|
|
103
|
+
(b) => b.callbackData?.startsWith("mdl:s:") && /Default|Sonnet|claude-sonnet/i.test(b.text),
|
|
104
|
+
);
|
|
105
|
+
if (restoreBtn?.callbackData) {
|
|
106
|
+
await sc.driver.pressButton(sc.botUserId, menu.messageId, restoreBtn.callbackData);
|
|
107
|
+
await new Promise((r) => setTimeout(r, 4_000));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`✅ sr-* model switch (${srName}) verified end-to-end through LiteLLM`);
|
|
111
|
+
} finally {
|
|
112
|
+
await sc.tearDown();
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
180_000,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
it(
|
|
119
|
+
"header row tap shows toast without switching model or opening picker",
|
|
120
|
+
async () => {
|
|
121
|
+
const sc = await spinUp({ agent: AGENT });
|
|
122
|
+
try {
|
|
123
|
+
await sc.sendDM("/model");
|
|
124
|
+
const menu = await sc.expectMessage(/Default \(new sessions\):/i, {
|
|
125
|
+
from: "bot",
|
|
126
|
+
timeout: 30_000,
|
|
127
|
+
});
|
|
128
|
+
const kb = await sc.driver.getKeyboard(sc.botUserId, menu.messageId);
|
|
129
|
+
const flat = (kb ?? []).flat();
|
|
130
|
+
const headerBtn = flat.find((b) => b.callbackData === "mdl:h");
|
|
131
|
+
if (!headerBtn) {
|
|
132
|
+
console.log("No header row — agent not LiteLLM-enabled. Skipping.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Pressing the header should NOT change the menu text
|
|
136
|
+
const textBefore = menu.text;
|
|
137
|
+
await sc.driver.pressButton(sc.botUserId, menu.messageId, "mdl:h");
|
|
138
|
+
await new Promise((r) => setTimeout(r, 3_000));
|
|
139
|
+
const after = await sc.driver.getMessage(sc.botUserId, menu.messageId);
|
|
140
|
+
expect(after?.text ?? "").toBe(textBefore);
|
|
141
|
+
} finally {
|
|
142
|
+
await sc.tearDown();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
60_000,
|
|
146
|
+
);
|
|
147
|
+
});
|