fix(telegram): allow fallback models in /model validation (#40105)

Merged via squash.

Prepared head SHA: de07585e03
Co-authored-by: avirweb <257412074+avirweb@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
This commit is contained in:
avirweb
2026-03-12 07:55:51 -05:00
committed by GitHub
parent 171d2df9e0
commit f2e28fc30f
12 changed files with 313 additions and 45 deletions

View File

@@ -1,3 +1,4 @@
import { rm } from "node:fs/promises";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js";
@@ -5,6 +6,7 @@ import {
listNativeCommandSpecs,
listNativeCommandSpecsForConfig,
} from "../auto-reply/commands-registry.js";
import { loadSessionStore } from "../config/sessions.js";
import { normalizeTelegramCommandName } from "../config/telegram-custom-commands.js";
import {
answerCallbackQuerySpy,
@@ -531,49 +533,127 @@ describe("createTelegramBot", () => {
it("routes compact model callbacks by inferring provider", async () => {
onSpy.mockClear();
replySpy.mockClear();
editMessageTextSpy.mockClear();
const modelId = "us.anthropic.claude-3-5-sonnet-20240620-v1:0";
const storePath = `/tmp/openclaw-telegram-model-compact-${process.pid}-${Date.now()}.json`;
createTelegramBot({
token: "tok",
config: {
agents: {
defaults: {
model: `bedrock/${modelId}`,
await rm(storePath, { force: true });
try {
createTelegramBot({
token: "tok",
config: {
agents: {
defaults: {
model: `bedrock/${modelId}`,
},
},
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
},
},
session: {
store: storePath,
},
},
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
});
const callbackHandler = onSpy.mock.calls.find(
(call) => call[0] === "callback_query",
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
expect(callbackHandler).toBeDefined();
await callbackHandler({
callbackQuery: {
id: "cbq-model-compact-1",
data: `mdl_sel/${modelId}`,
from: { id: 9, first_name: "Ada", username: "ada_bot" },
message: {
chat: { id: 1234, type: "private" },
date: 1736380800,
message_id: 14,
},
},
},
});
const callbackHandler = onSpy.mock.calls.find((call) => call[0] === "callback_query")?.[1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
expect(callbackHandler).toBeDefined();
me: { username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
await callbackHandler({
callbackQuery: {
id: "cbq-model-compact-1",
data: `mdl_sel/${modelId}`,
from: { id: 9, first_name: "Ada", username: "ada_bot" },
message: {
chat: { id: 1234, type: "private" },
date: 1736380800,
message_id: 14,
expect(replySpy).not.toHaveBeenCalled();
expect(editMessageTextSpy).toHaveBeenCalledTimes(1);
expect(editMessageTextSpy.mock.calls[0]?.[2]).toContain("✅ Model reset to default");
const entry = Object.values(loadSessionStore(storePath, { skipCache: true }))[0];
expect(entry?.providerOverride).toBeUndefined();
expect(entry?.modelOverride).toBeUndefined();
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-compact-1");
} finally {
await rm(storePath, { force: true });
}
});
it("resets overrides when selecting the configured default model", async () => {
onSpy.mockClear();
replySpy.mockClear();
editMessageTextSpy.mockClear();
const storePath = `/tmp/openclaw-telegram-model-default-${process.pid}-${Date.now()}.json`;
await rm(storePath, { force: true });
try {
createTelegramBot({
token: "tok",
config: {
agents: {
defaults: {
model: "claude-opus-4-6",
models: {
"anthropic/claude-opus-4-6": {},
},
},
},
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
},
},
session: {
store: storePath,
},
},
},
me: { username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
});
const callbackHandler = onSpy.mock.calls.find(
(call) => call[0] === "callback_query",
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
expect(callbackHandler).toBeDefined();
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0]?.[0];
expect(payload?.Body).toContain(`/model amazon-bedrock/${modelId}`);
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-compact-1");
await callbackHandler({
callbackQuery: {
id: "cbq-model-default-1",
data: "mdl_sel_anthropic/claude-opus-4-6",
from: { id: 9, first_name: "Ada", username: "ada_bot" },
message: {
chat: { id: 1234, type: "private" },
date: 1736380800,
message_id: 16,
},
},
me: { username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).not.toHaveBeenCalled();
expect(editMessageTextSpy).toHaveBeenCalledTimes(1);
expect(editMessageTextSpy.mock.calls[0]?.[2]).toContain("✅ Model reset to default");
const entry = Object.values(loadSessionStore(storePath, { skipCache: true }))[0];
expect(entry?.providerOverride).toBeUndefined();
expect(entry?.modelOverride).toBeUndefined();
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-default-1");
} finally {
await rm(storePath, { force: true });
}
});
it("rejects ambiguous compact model callbacks and returns provider list", async () => {