fix(telegram): paginate model provider picker

This commit is contained in:
helix4u
2026-07-03 15:29:07 -06:00
committed by kshitij
parent 5445e42b87
commit 7a648a8bff
2 changed files with 143 additions and 9 deletions
+67 -9
View File
@@ -4429,7 +4429,7 @@ class TelegramAdapter(BasePlatformAdapter):
try:
# Build provider buttons — folds provider groups (display only).
keyboard = self._build_provider_keyboard(providers)
keyboard, provider_page_info = self._build_provider_keyboard(providers, 0)
provider_label = get_label(current_provider)
text = self.format_message(
@@ -4437,7 +4437,7 @@ class TelegramAdapter(BasePlatformAdapter):
f"⚙ *Model Configuration*\n\n"
f"Current model: `{current_model or 'unknown'}`\n"
f"Provider: {provider_label}\n\n"
f"Select a provider:"
f"Select a provider:{provider_page_info}"
)
)
@@ -4467,6 +4467,7 @@ class TelegramAdapter(BasePlatformAdapter):
"on_model_selected": on_model_selected,
"current_model": current_model,
"current_provider": current_provider,
"provider_page": 0,
}
return SendResult(success=True, message_id=str(msg.message_id))
@@ -4474,10 +4475,11 @@ class TelegramAdapter(BasePlatformAdapter):
logger.warning("[%s] send_model_picker failed: %s", self.name, e)
return SendResult(success=False, error=str(e))
_PROVIDER_PAGE_SIZE = 10
_MODEL_PAGE_SIZE = 8
def _build_provider_keyboard(self, providers: list):
"""Build the top-level provider keyboard, folding provider groups.
def _build_provider_keyboard(self, providers: list, page: int = 0) -> tuple:
"""Build the paginated top-level provider keyboard, folding groups.
Provider families (Kimi/Moonshot, MiniMax, xAI Grok, ...) collapse to
a single ``mpg:<gid>`` button; tapping it drills into a member
@@ -4522,9 +4524,30 @@ class TelegramAdapter(BasePlatformAdapter):
for p in providers:
buttons.append(_provider_button(p))
rows = [buttons[i : i + 2] for i in range(0, len(buttons), 2)]
page_size = self._PROVIDER_PAGE_SIZE
total = len(buttons)
total_pages = max(1, (total + page_size - 1) // page_size)
page = max(0, min(page, total_pages - 1))
start = page * page_size
end = min(start + page_size, total)
page_buttons = buttons[start:end]
rows = [page_buttons[i : i + 2] for i in range(0, len(page_buttons), 2)]
if total_pages > 1:
nav: list = []
if page > 0:
nav.append(InlineKeyboardButton("◀ Prev", callback_data=f"mpv:{page - 1}"))
nav.append(InlineKeyboardButton(f"{page + 1}/{total_pages}", callback_data="mx:noop"))
if page < total_pages - 1:
nav.append(InlineKeyboardButton("Next ▶", callback_data=f"mpv:{page + 1}"))
rows.append(nav)
rows.append([InlineKeyboardButton("✗ Cancel", callback_data="mx")])
return InlineKeyboardMarkup(rows)
page_info = f" ({start + 1}–{end} of {total})" if total_pages > 1 else ""
return InlineKeyboardMarkup(rows), page_info
def _build_model_keyboard(self, models: list, page: int) -> tuple:
"""Build paginated model buttons. Returns (keyboard, page_info_text)."""
@@ -4655,6 +4678,38 @@ class TelegramAdapter(BasePlatformAdapter):
)
await query.answer()
elif data.startswith("mpv:"):
# --- Provider page navigation ---
try:
page = int(data[4:])
except ValueError:
await query.answer(text="Invalid page.")
return
state["provider_page"] = page
keyboard, provider_page_info = self._build_provider_keyboard(
state["providers"], page
)
try:
provider_label = get_label(state["current_provider"])
except Exception:
provider_label = state["current_provider"]
await query.edit_message_text(
text=self.format_message(
(
f"⚙ *Model Configuration*\n\n"
f"Current model: `{state['current_model'] or 'unknown'}`\n"
f"Provider: {provider_label}\n\n"
f"Select a provider:{provider_page_info}"
)
),
parse_mode=ParseMode.MARKDOWN_V2,
reply_markup=keyboard,
)
await query.answer()
elif data.startswith("mc:"):
# --- Expensive model confirmed: perform the switch ---
try:
@@ -4833,7 +4888,10 @@ class TelegramAdapter(BasePlatformAdapter):
elif data == "mb":
# --- Back to provider list (folds groups) ---
keyboard = self._build_provider_keyboard(state["providers"])
page = int(state.get("provider_page", 0) or 0)
keyboard, provider_page_info = self._build_provider_keyboard(
state["providers"], page
)
try:
provider_label = get_label(state["current_provider"])
@@ -4846,7 +4904,7 @@ class TelegramAdapter(BasePlatformAdapter):
f"⚙ *Model Configuration*\n\n"
f"Current model: `{state['current_model'] or 'unknown'}`\n"
f"Provider: {provider_label}\n\n"
f"Select a provider:"
f"Select a provider:{provider_page_info}"
)
),
parse_mode=ParseMode.MARKDOWN_V2,
@@ -4908,7 +4966,7 @@ class TelegramAdapter(BasePlatformAdapter):
query_user_name = getattr(query.from_user, "first_name", None)
# --- Model picker callbacks ---
if data.startswith(("mp:", "mpg:", "mm:", "mc:", "mb", "mx", "mg:")):
if data.startswith(("mp:", "mpg:", "mpv:", "mm:", "mc:", "mb", "mx", "mg:")):
chat_id = str(query.message.chat_id) if query.message else None
if chat_id:
await self._handle_model_picker_callback(query, data, chat_id)
@@ -205,6 +205,82 @@ class TestTelegramModelPicker:
assert "mp:minimax-cn" in built
assert "mb" in built
@pytest.mark.asyncio
async def test_provider_picker_paginates_past_first_ten(self, monkeypatch):
import plugins.platforms.telegram.adapter as tg
class _RecordingButton:
def __init__(self, text, callback_data=None, **kw):
self.text = text
self.callback_data = callback_data
class _RecordingMarkup:
def __init__(self, rows):
self.inline_keyboard = rows
monkeypatch.setattr(tg, "InlineKeyboardButton", _RecordingButton)
monkeypatch.setattr(tg, "InlineKeyboardMarkup", _RecordingMarkup)
adapter = _make_adapter()
sent = {}
async def mock_send_message(**kwargs):
sent.update(kwargs)
return SimpleNamespace(message_id=101)
adapter._bot.send_message = AsyncMock(side_effect=mock_send_message)
providers = [
{"slug": f"provider-{i}", "name": f"Provider {i}", "total_models": 1}
for i in range(10)
]
providers.append({
"slug": "zai",
"name": "Z.AI / GLM",
"models": ["glm-5.2"],
"total_models": 1,
})
await adapter.send_model_picker(
chat_id="12345",
providers=providers,
current_model="model_1",
current_provider="provider-0",
session_key="s",
on_model_selected=AsyncMock(),
metadata=None,
)
def _callbacks(markup):
return [
button.callback_data
for row in markup.inline_keyboard
for button in row
]
first_page = _callbacks(sent["reply_markup"])
assert "mp:zai" not in first_page
assert "mpv:1" in first_page
query = AsyncMock()
query.message = MagicMock()
query.message.chat_id = 12345
query.answer = AsyncMock()
query.edit_message_text = AsyncMock()
await adapter._handle_model_picker_callback(query, "mpv:1", "12345")
second_page = _callbacks(query.edit_message_text.call_args[1]["reply_markup"])
assert "mp:zai" in second_page
assert "mpv:0" in second_page
await adapter._handle_model_picker_callback(query, "mp:zai", "12345")
assert adapter._model_picker_state["12345"]["selected_provider"] == "zai"
await adapter._handle_model_picker_callback(query, "mb", "12345")
back_page = _callbacks(query.edit_message_text.call_args[1]["reply_markup"])
assert "mp:zai" in back_page
@pytest.mark.asyncio
async def test_expensive_model_requires_confirmation(self, monkeypatch):
adapter = _make_adapter()