mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 08:31:10 +00:00
Telegram: support compact model callback fallback
This commit is contained in:
committed by
Peter Steinberger
parent
c582a54554
commit
54eb13893f
@@ -1261,11 +1261,29 @@ export const registerTelegramHandlers = ({
|
|||||||
|
|
||||||
if (modelCallback.type === "select") {
|
if (modelCallback.type === "select") {
|
||||||
const { provider, model } = modelCallback;
|
const { provider, model } = modelCallback;
|
||||||
|
let resolvedProvider = provider;
|
||||||
|
if (!resolvedProvider) {
|
||||||
|
const matchingProviders = providers.filter((id) => byProvider.get(id)?.has(model));
|
||||||
|
if (matchingProviders.length === 1) {
|
||||||
|
resolvedProvider = matchingProviders[0];
|
||||||
|
} else {
|
||||||
|
const providerInfos: ProviderInfo[] = providers.map((p) => ({
|
||||||
|
id: p,
|
||||||
|
count: byProvider.get(p)?.size ?? 0,
|
||||||
|
}));
|
||||||
|
const buttons = buildProviderKeyboard(providerInfos);
|
||||||
|
await editMessageWithButtons(
|
||||||
|
`Could not resolve model "${model}".\n\nSelect a provider:`,
|
||||||
|
buttons,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Process model selection as a synthetic message with /model command
|
// Process model selection as a synthetic message with /model command
|
||||||
const syntheticMessage = buildSyntheticTextMessage({
|
const syntheticMessage = buildSyntheticTextMessage({
|
||||||
base: callbackMessage,
|
base: callbackMessage,
|
||||||
from: callback.from,
|
from: callback.from,
|
||||||
text: `/model ${provider}/${model}`,
|
text: `/model ${resolvedProvider}/${model}`,
|
||||||
});
|
});
|
||||||
await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, {
|
await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, {
|
||||||
forceWasMentioned: true,
|
forceWasMentioned: true,
|
||||||
|
|||||||
@@ -319,6 +319,107 @@ describe("createTelegramBot", () => {
|
|||||||
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-4");
|
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-4");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes compact model callbacks by inferring provider", async () => {
|
||||||
|
onSpy.mockClear();
|
||||||
|
replySpy.mockClear();
|
||||||
|
|
||||||
|
const modelId = "us.anthropic.claude-3-5-sonnet-20240620-v1:0";
|
||||||
|
|
||||||
|
createTelegramBot({
|
||||||
|
token: "tok",
|
||||||
|
config: {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: `bedrock/${modelId}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
me: { username: "openclaw_bot" },
|
||||||
|
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects ambiguous compact model callbacks and returns provider list", async () => {
|
||||||
|
onSpy.mockClear();
|
||||||
|
replySpy.mockClear();
|
||||||
|
editMessageTextSpy.mockClear();
|
||||||
|
|
||||||
|
createTelegramBot({
|
||||||
|
token: "tok",
|
||||||
|
config: {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: "anthropic/shared-model",
|
||||||
|
models: {
|
||||||
|
"anthropic/shared-model": {},
|
||||||
|
"openai/shared-model": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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-2",
|
||||||
|
data: "mdl_sel/shared-model",
|
||||||
|
from: { id: 9, first_name: "Ada", username: "ada_bot" },
|
||||||
|
message: {
|
||||||
|
chat: { id: 1234, type: "private" },
|
||||||
|
date: 1736380800,
|
||||||
|
message_id: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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(
|
||||||
|
'Could not resolve model "shared-model".',
|
||||||
|
);
|
||||||
|
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-compact-2");
|
||||||
|
});
|
||||||
|
|
||||||
it("includes sender identity in group envelope headers", async () => {
|
it("includes sender identity in group envelope headers", async () => {
|
||||||
onSpy.mockClear();
|
onSpy.mockClear();
|
||||||
replySpy.mockClear();
|
replySpy.mockClear();
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ describe("parseModelCallbackData", () => {
|
|||||||
{ type: "select", provider: "anthropic", model: "claude-sonnet-4-5" },
|
{ type: "select", provider: "anthropic", model: "claude-sonnet-4-5" },
|
||||||
],
|
],
|
||||||
["mdl_sel_openai/gpt-4/turbo", { type: "select", provider: "openai", model: "gpt-4/turbo" }],
|
["mdl_sel_openai/gpt-4/turbo", { type: "select", provider: "openai", model: "gpt-4/turbo" }],
|
||||||
|
[
|
||||||
|
"mdl_sel/us.anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||||
|
{ type: "select", model: "us.anthropic.claude-3-5-sonnet-20240620-v1:0" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"mdl_sel/anthropic/claude-3-7-sonnet",
|
||||||
|
{ type: "select", model: "anthropic/claude-3-7-sonnet" },
|
||||||
|
],
|
||||||
[" mdl_prov ", { type: "providers" }],
|
[" mdl_prov ", { type: "providers" }],
|
||||||
] as const;
|
] as const;
|
||||||
for (const [input, expected] of cases) {
|
for (const [input, expected] of cases) {
|
||||||
@@ -36,6 +44,7 @@ describe("parseModelCallbackData", () => {
|
|||||||
"mdl_invalid",
|
"mdl_invalid",
|
||||||
"mdl_list_",
|
"mdl_list_",
|
||||||
"mdl_sel_noslash",
|
"mdl_sel_noslash",
|
||||||
|
"mdl_sel/",
|
||||||
];
|
];
|
||||||
for (const input of invalid) {
|
for (const input of invalid) {
|
||||||
expect(parseModelCallbackData(input), input).toBeNull();
|
expect(parseModelCallbackData(input), input).toBeNull();
|
||||||
@@ -209,6 +218,18 @@ describe("buildModelsKeyboard", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses compact selection callback when provider/model callback exceeds 64 bytes", () => {
|
||||||
|
const model = "us.anthropic.claude-3-5-sonnet-20240620-v1:0";
|
||||||
|
const result = buildModelsKeyboard({
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
models: [model],
|
||||||
|
currentPage: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result[0]?.[0]?.callback_data).toBe(`mdl_sel/${model}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildBrowseProvidersButton", () => {
|
describe("buildBrowseProvidersButton", () => {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
* Callback data patterns (max 64 bytes for Telegram):
|
* Callback data patterns (max 64 bytes for Telegram):
|
||||||
* - mdl_prov - show providers list
|
* - mdl_prov - show providers list
|
||||||
* - mdl_list_{prov}_{pg} - show models for provider (page N, 1-indexed)
|
* - mdl_list_{prov}_{pg} - show models for provider (page N, 1-indexed)
|
||||||
* - mdl_sel_{provider/id} - select model
|
* - mdl_sel_{provider/id} - select model (standard)
|
||||||
|
* - mdl_sel/{model} - select model (compact fallback when standard is >64 bytes)
|
||||||
* - mdl_back - back to providers list
|
* - mdl_back - back to providers list
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export type ButtonRow = Array<{ text: string; callback_data: string }>;
|
|||||||
export type ParsedModelCallback =
|
export type ParsedModelCallback =
|
||||||
| { type: "providers" }
|
| { type: "providers" }
|
||||||
| { type: "list"; provider: string; page: number }
|
| { type: "list"; provider: string; page: number }
|
||||||
| { type: "select"; provider: string; model: string }
|
| { type: "select"; provider?: string; model: string }
|
||||||
| { type: "back" };
|
| { type: "back" };
|
||||||
|
|
||||||
export type ProviderInfo = {
|
export type ProviderInfo = {
|
||||||
@@ -57,6 +58,18 @@ export function parseModelCallbackData(data: string): ParsedModelCallback | null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mdl_sel/{model} (compact fallback)
|
||||||
|
const compactSelMatch = trimmed.match(/^mdl_sel\/(.+)$/);
|
||||||
|
if (compactSelMatch) {
|
||||||
|
const modelRef = compactSelMatch[1];
|
||||||
|
if (modelRef) {
|
||||||
|
return {
|
||||||
|
type: "select",
|
||||||
|
model: modelRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// mdl_sel_{provider/model}
|
// mdl_sel_{provider/model}
|
||||||
const selMatch = trimmed.match(/^mdl_sel_(.+)$/);
|
const selMatch = trimmed.match(/^mdl_sel_(.+)$/);
|
||||||
if (selMatch) {
|
if (selMatch) {
|
||||||
@@ -133,8 +146,12 @@ export function buildModelsKeyboard(params: ModelsKeyboardParams): ButtonRow[] {
|
|||||||
: currentModel;
|
: currentModel;
|
||||||
|
|
||||||
for (const model of pageModels) {
|
for (const model of pageModels) {
|
||||||
const callbackData = `mdl_sel_${provider}/${model}`;
|
const fullCallbackData = `mdl_sel_${provider}/${model}`;
|
||||||
// Skip models that would exceed Telegram's callback_data limit
|
const callbackData =
|
||||||
|
Buffer.byteLength(fullCallbackData, "utf8") <= MAX_CALLBACK_DATA_BYTES
|
||||||
|
? fullCallbackData
|
||||||
|
: `mdl_sel/${model}`;
|
||||||
|
// Skip models that still exceed Telegram's callback_data limit
|
||||||
if (Buffer.byteLength(callbackData, "utf8") > MAX_CALLBACK_DATA_BYTES) {
|
if (Buffer.byteLength(callbackData, "utf8") > MAX_CALLBACK_DATA_BYTES) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user