From 202c554d09ad519742be7e928ff66a8ab2cc2b12 Mon Sep 17 00:00:00 2001 From: Ermenegildo Fiorito Date: Tue, 3 Feb 2026 19:15:22 +0100 Subject: [PATCH] Telegram: fix model button review issues - Add currentModel to callback handler for checkmark display - Add 64-byte callback_data limit protection (skip long model IDs) - Add tests for large model lists and callback_data limits --- src/telegram/bot-handlers.ts | 5 ++ src/telegram/model-buttons.test.ts | 97 +++++++++++++++++++++++++++++- src/telegram/model-buttons.ts | 9 ++- 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index 74decd5f9..6c0ccea55 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -474,9 +474,14 @@ export const registerTelegramHandlers = ({ const totalPages = calculateTotalPages(models.length, pageSize); const safePage = Math.max(1, Math.min(page, totalPages)); + // Get current model from config for checkmark display + const modelCfg = cfg.agents?.defaults?.model; + const currentModel = typeof modelCfg === "string" ? modelCfg : modelCfg?.primary; + const buttons = buildModelsKeyboard({ provider, models, + currentModel, currentPage: safePage, totalPages, pageSize, diff --git a/src/telegram/model-buttons.test.ts b/src/telegram/model-buttons.test.ts index 69f93273e..bff2ea17c 100644 --- a/src/telegram/model-buttons.test.ts +++ b/src/telegram/model-buttons.test.ts @@ -197,8 +197,10 @@ describe("buildModelsKeyboard", () => { expect(paginationRow?.[1]?.text).toBe("3/3"); }); - it("truncates long model IDs", () => { - const longModel = "this-is-a-very-long-model-name-that-exceeds-the-limit"; + it("truncates long model IDs for display", () => { + // Model ID that's long enough to truncate display but still fits in callback_data + // callback_data = "mdl_sel_anthropic/" (18) + model (<=46) = 64 max + const longModel = "claude-3-5-sonnet-20241022-with-suffix"; const result = buildModelsKeyboard({ provider: "anthropic", models: [longModel], @@ -206,8 +208,23 @@ describe("buildModelsKeyboard", () => { totalPages: 1, }); const text = result[0]?.[0]?.text; + // Model is 38 chars, fits exactly in 38-char display limit + expect(text).toBe(longModel); + }); + + it("truncates display text for very long model names", () => { + // Use short provider to allow longer model in callback_data (64 byte limit) + // "mdl_sel_a/" = 10 bytes, leaving 54 for model + const longModel = "this-model-name-is-long-enough-to-need-truncation-abcd"; + const result = buildModelsKeyboard({ + provider: "a", + models: [longModel], + currentPage: 1, + totalPages: 1, + }); + const text = result[0]?.[0]?.text; expect(text?.startsWith("…")).toBe(true); - expect(text?.length).toBeLessThanOrEqual(39); // 38 max + possible checkmark + expect(text?.length).toBeLessThanOrEqual(38); }); }); @@ -242,3 +259,77 @@ describe("calculateTotalPages", () => { expect(calculateTotalPages(11, 5)).toBe(3); }); }); + +describe("large model lists (OpenRouter-scale)", () => { + it("handles 100+ models with pagination", () => { + const models = Array.from({ length: 150 }, (_, i) => `model-${i}`); + const totalPages = calculateTotalPages(models.length); + expect(totalPages).toBe(19); // 150 / 8 = 18.75 -> 19 pages + + // Test first page + const firstPage = buildModelsKeyboard({ + provider: "openrouter", + models, + currentPage: 1, + totalPages, + }); + expect(firstPage.length).toBe(10); // 8 models + pagination + back + expect(firstPage[0]?.[0]?.text).toBe("model-0"); + expect(firstPage[7]?.[0]?.text).toBe("model-7"); + + // Test last page + const lastPage = buildModelsKeyboard({ + provider: "openrouter", + models, + currentPage: 19, + totalPages, + }); + // Last page has 150 - (18 * 8) = 6 models + expect(lastPage.length).toBe(8); // 6 models + pagination + back + expect(lastPage[0]?.[0]?.text).toBe("model-144"); + }); + + it("all callback_data stays within 64-byte limit", () => { + // Realistic OpenRouter model IDs + const models = [ + "anthropic/claude-3-5-sonnet-20241022", + "google/gemini-2.0-flash-thinking-exp:free", + "deepseek/deepseek-r1-distill-llama-70b", + "meta-llama/llama-3.3-70b-instruct:nitro", + "nousresearch/hermes-3-llama-3.1-405b:extended", + ]; + const result = buildModelsKeyboard({ + provider: "openrouter", + models, + currentPage: 1, + totalPages: 1, + }); + + for (const row of result) { + for (const button of row) { + const bytes = Buffer.byteLength(button.callback_data, "utf8"); + expect(bytes).toBeLessThanOrEqual(64); + } + } + }); + + it("skips models that would exceed callback_data limit", () => { + const models = [ + "short-model", + "this-is-an-extremely-long-model-name-that-definitely-exceeds-the-sixty-four-byte-limit", + "another-short", + ]; + const result = buildModelsKeyboard({ + provider: "openrouter", + models, + currentPage: 1, + totalPages: 1, + }); + + // Should have 2 model buttons (skipping the long one) + back + const modelButtons = result.filter((row) => !row[0]?.callback_data.startsWith("mdl_back")); + expect(modelButtons.length).toBe(2); + expect(modelButtons[0]?.[0]?.text).toBe("short-model"); + expect(modelButtons[1]?.[0]?.text).toBe("another-short"); + }); +}); diff --git a/src/telegram/model-buttons.ts b/src/telegram/model-buttons.ts index 84302de91..03f74dae9 100644 --- a/src/telegram/model-buttons.ts +++ b/src/telegram/model-buttons.ts @@ -31,6 +31,7 @@ export type ModelsKeyboardParams = { }; const MODELS_PAGE_SIZE = 8; +const MAX_CALLBACK_DATA_BYTES = 64; /** * Parse a model callback_data string into a structured object. @@ -132,6 +133,12 @@ export function buildModelsKeyboard(params: ModelsKeyboardParams): ButtonRow[] { : currentModel; for (const model of pageModels) { + const callbackData = `mdl_sel_${provider}/${model}`; + // Skip models that would exceed Telegram's callback_data limit + if (Buffer.byteLength(callbackData, "utf8") > MAX_CALLBACK_DATA_BYTES) { + continue; + } + const isCurrentModel = model === currentModelId; const displayText = truncateModelId(model, 38); const text = isCurrentModel ? `${displayText} ✓` : displayText; @@ -139,7 +146,7 @@ export function buildModelsKeyboard(params: ModelsKeyboardParams): ButtonRow[] { rows.push([ { text, - callback_data: `mdl_sel_${provider}/${model}`, + callback_data: callbackData, }, ]); }