{"repo":"NousResearch/hermes-agent","public_id":"1469","display_label":"Interactive /models command and model picker for Telegram and Discord","summary":"These issues and pull requests all concern model discovery and selection in the chat gateway, mainly through a /models slash command and an interactive provider-to-model picker for Telegram and Discord. Some members focus on listing models, others on an interactive selector, and one on improving Telegram's /model; the titles alone do not show their status.","subsystem":"gateway","counts":{"members":11,"open_issues":1,"open_issues_distinct":1,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":5742,"merged_at":"2026-04-07T06:00:05+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-04-07T06:00:05+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.15854545454545452},"fix_edges":[{"issue_number":1469,"pr_number":5742,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-07T06:00:05+00:00"}],"members":[{"kind":"issue","number":1469,"title":"[Feature]: Telegram /model improvement","url":null,"state":"closed"},{"kind":"issue","number":3500,"title":"[Feature]: /models slash command for listing available models","url":null,"state":"open"},{"kind":"issue","number":3710,"title":"feat: interactive model picker for gateway (Discord/Telegram) — provider → model selection","url":null,"state":"closed"},{"kind":"pr","number":1513,"title":"feat: add interactive /models selector for Telegram (#1469)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#5742","number":5742}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":2212,"title":"feat(telegram): interactive /models selector with live API probing","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":3502,"title":"feat: add /models slash command for listing available models","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":3503,"title":"feat(gateway): add /models slash command for model discovery","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":3718,"title":"feat: interactive model picker for gateway (Discord/Telegram)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#5742","number":5742}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":4793,"title":"feat: interactive model picker for gateway (Discord/Telegram)","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":5742,"title":"feat: interactive model picker for Telegram and Discord","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/discord.py","gateway/platforms/telegram.py","gateway/run.py"],"fix_hunks":[{"file":"gateway/platforms/discord.py","hunks":["@@ -2039,6 +2039,66 @@ async def send_update_prompt(\n         except Exception as e:\n             return SendResult(success=False, error=str(e))\n \n+    async def send_model_picker(\n+        self,\n+        chat_id: str,\n+        providers: list,\n+        current_model: str,\n+        current_provider: str,\n+        session_key: str,\n+        on_model_selected,\n+        metadata: Optional[Dict[str, Any]] = None,\n+    ) -> SendResult:\n+        \"\"\"Send an interactive select-menu model picker.\n+\n+        Two-step drill-down: provider dropdown → model dropdown.\n+        Uses Discord embeds + Select menus via ``ModelPickerView``.\n+        \"\"\"\n+        if not self._client or not DISCORD_AVAILABLE:\n+            return SendResult(success=False, error=\"Not connected\")\n+\n+        try:\n+            # Resolve target channel (use thread_id if present)\n+            target_id = chat_id\n+            if metadata and metadata.get(\"thread_id\"):\n+                target_id = metadata[\"thread_id\"]\n+\n+            channel = self._client.get_channel(int(target_id))\n+            if not channel:\n+                channel = await self._client.fetch_channel(int(target_id))\n+\n+            try:\n+                from hermes_cli.providers import get_label\n+                provider_label = get_label(current_provider)\n+            except Exception:\n+                provider_label = current_provider\n+\n+            embed = discord.Embed(\n+                title=\"⚙ Model Configuration\",\n+                description=(\n+                    f\"Current model: `{current_model or 'unknown'}`\\n\"\n+                    f\"Provider: {provider_label}\\n\\n\"\n+                    f\"Select a provider:\"\n+                ),\n+                color=discord.Color.blue(),\n+            )\n+\n+            view = ModelPickerView(\n+                providers=providers,\n+                current_model=current_model,\n+                current_provider=current_provider,\n+                session_key=session_key,\n+                on_model_selected=on_model_selected,\n+                allowed_user_ids=self._allowed_user_ids,\n+            )\n+\n+            msg = await channel.send(embed=embed, view=view)\n+            return SendResult(success=True, message_id=str(msg.id))\n+\n+        except Exception as e:\n+            logger.warning(\"[%s] send_model_picker failed: %s\", self.name, e)\n+            return SendResult(success=False, error=str(e))\n+\n     def _get_parent_channel_id(self, channel: Any) -> Optional[str]:\n         \"\"\"Return the parent channel ID for a Discord thread-like channel, if present.\"\"\"\n         parent = getattr(channel, \"parent\", None)","@@ -2530,3 +2590,219 @@ async def on_timeout(self):\n             self.resolved = True\n             for child in self.children:\n                 child.disabled = True\n+\n+    class ModelPickerView(discord.ui.View):\n+        \"\"\"Interactive select-menu view for model switching.\n+\n+        Two-step drill-down: provider dropdown → model dropdown.\n+        Edits the original message in-place as the user navigates.\n+        Times out after 2 minutes.\n+        \"\"\"\n+\n+        def __init__(\n+            self,\n+            providers: list,\n+            current_model: str,\n+            current_provider: str,\n+            session_key: str,\n+            on_model_selected,\n+            allowed_user_ids: set,\n+        ):\n+            super().__init__(timeout=120)\n+            self.providers = providers\n+            self.current_model = current_model\n+            self.current_provider = current_provider\n+            self.session_key = session_key\n+            self.on_model_selected = on_model_selected\n+            self.allowed_user_ids = allowed_user_ids\n+            self.resolved = False\n+            self._selected_provider: str = \"\"\n+\n+            self._build_provider_select()\n+\n+        def _check_auth(self, interaction: discord.Interaction) -> bool:\n+            if not self.allowed_user_ids:\n+                return True\n+            return str(interaction.user.id) in self.allowed_user_ids\n+\n+        def _build_provider_select(self):\n+            \"\"\"Build the provider dropdown menu.\"\"\"\n+            self.clear_items()\n+            options = []\n+            for p in self.providers:\n+                count = p.get(\"total_models\", len(p.get(\"models\", [])))\n+                label = f\"{p['name']} ({count} models)\"\n+                desc = \"current\" if p.get(\"is_current\") else None\n+                options.append(\n+                    discord.SelectOption(\n+                        label=label[:100],\n+                        value=p[\"slug\"],\n+                        default=bool(p.get(\"is_current\")),\n+                        description=desc,\n+                    )\n+                )\n+            if not options:\n+                return\n+\n+            select = discord.ui.Select(\n+                placeholder=\"Choose a provider...\",\n+                options=options[:25],\n+                custom_id=\"model_provider_select\",\n+            )\n+            select.callback = self._on_provider_selected\n+            self.add_item(select)\n+\n+            cancel_btn = discord.ui.Button(\n+                label=\"Cancel\", style=discord.ButtonStyle.red, custom_id=\"model_cancel\"\n+            )\n+            cancel_btn.callback = self._on_cancel\n+            self.add_item(cancel_btn)\n+\n+        def _build_model_select(self, provider_slug: str):\n+            \"\"\"Build the model dropdown for a specific provider.\"\"\"\n+            self.clear_items()\n+            provider = next(\n+                (p for p in self.providers if p[\"slug\"] == provider_slug), None\n+            )\n+            if not provider:\n+                return\n+\n+            models = provider.get(\"models\", [])\n+            options = []\n+            for model_id in models[:25]:\n+                short = model_id.split(\"/\")[-1] if \"/\" in model_id else model_id\n+                options.append(\n+                    discord.SelectOption(\n+                        label=short[:100],\n+                        value=model_id[:100],\n+                    )\n+                )\n+            if not options:\n+                return\n+\n+            select = discord.ui.Select(\n+                placeholder=f\"Choose a model from {provider.get('name', provider_slug)}...\",\n+                options=options,\n+                custom_id=\"model_model_select\",\n+            )\n+            select.callback = self._on_model_selected\n+            self.add_item(select)\n+\n+            back_btn = discord.ui.Button(\n+                label=\"◀ Back\", style=discord.ButtonStyle.grey, custom_id=\"model_back\"\n+            )\n+            back_btn.callback = self._on_back\n+            self.add_item(back_btn)\n+\n+            cancel_btn = discord.ui.Button(\n+                label=\"Cancel\", style=discord.ButtonStyle.red, custom_id=\"model_cancel2\"\n+            )\n+            cancel_btn.callback = self._on_cancel\n+            self.add_item(cancel_btn)\n+\n+        async def _on_provider_selected(self, interaction: discord.Interaction):\n+            if not self._check_auth(interaction):\n+                await interaction.response.send_message(\n+                    \"You're not authorized~\", ephemeral=True\n+                )\n+                return\n+\n+            provider_slug = interaction.data[\"values\"][0]\n+            self._selected_provider = provider_slug\n+            provider = next(\n+                (p for p in self.providers if p[\"slug\"] == provider_slug), None\n+            )\n+            pname = provider.get(\"name\", provider_slug) if provider else provider_slug\n+\n+            self._build_model_select(provider_slug)\n+\n+            total = provider.get(\"total_models\", 0) if provider else 0\n+            shown = min(len(provider.get(\"models\", [])), 25) if provider else 0\n+            extra = f\"\\n*{total - shown} more available — type `/model <name>` directly*\" if total > shown else \"\"\n+\n+            await interaction.response.edit_message(\n+                embed=discord.Embed(\n+                    title=\"⚙ Model Configuration\",\n+                    description=f\"Provider: **{pname}**\\nSelect a model:{extra}\",\n+                    color=discord.Color.blue(),\n+                ),\n+                view=self,\n+            )\n+\n+        async def _on_model_selected(self, interaction: discord.Interaction):\n+            if self.resolved:\n+                await interaction.response.send_message(\n+                    \"Already resolved~\", ephemeral=True\n+                )\n+                return\n+            if not self._check_auth(interaction):\n+                await interaction.response.send_message(\n+                    \"You're not authorized~\", ephemeral=True\n+                )\n+                return\n+\n+            self.resolved = True\n+            model_id = interaction.data[\"values\"][0]\n+\n+            try:\n+                result_text = await self.on_model_selected(\n+                    str(interaction.channel_id),\n+                    model_id,\n+                    self._selected_provider,\n+                )\n+            except Exception as exc:\n+                result_text = f\"Error switching model: {exc}\"\n+\n+            self.clear_items()\n+            await interaction.response.edit_message(\n+                embed=discord.Embed(\n+                    title=\"⚙ Model Switched\",\n+                    description=result_text,\n+                    color=discord.Color.green(),\n+                ),\n+                view=self,\n+            )\n+\n+        async def _on_back(self, interaction: discord.Interaction):\n+            if not self._check_auth(interaction):\n+                await interaction.response.send_message(\n+                    \"You're not authorized~\", ephemeral=True\n+                )\n+                return\n+\n+            self._build_provider_select()\n+\n+            try:\n+                from hermes_cli.providers import get_label\n+                provider_label = get_label(self.current_provider)\n+            except Exception:\n+                provider_label = self.current_provider\n+\n+            await interaction.response.edit_message(\n+                embed=discord.Embed(\n+                    title=\"⚙ Model Configuration\",\n+                    description=(\n+                        f\"Current model: `{self.current_model or 'unknown'}`\\n\"\n+                        f\"Provider: {provider_label}\\n\\n\"\n+                        f\"Select a provider:\"\n+                    ),\n+                    color=discord.Color.blue(),\n+                ),\n+                view=self,\n+            )\n+\n+        async def _on_cancel(self, interaction: discord.Interaction):\n+            self.resolved = True\n+            self.clear_items()\n+            await interaction.response.edit_message(\n+                embed=discord.Embed(\n+                    title=\"⚙ Model Configuration\",\n+                    description=\"Model selection cancelled.\",\n+                    color=discord.Color.greyple(),\n+                ),\n+                view=self,\n+            )\n+\n+        async def on_timeout(self):\n+            self.resolved = True\n+            self.clear_items()"]},{"file":"gateway/platforms/telegram.py","hunks":["@@ -151,6 +151,8 @@ def __init__(self, config: PlatformConfig):\n         self._dm_topics: Dict[str, int] = {}\n         # DM Topics config from extra.dm_topics\n         self._dm_topics_config: List[Dict[str, Any]] = self.config.extra.get(\"dm_topics\", [])\n+        # Interactive model picker state per chat\n+        self._model_picker_state: Dict[str, dict] = {}\n \n     def _fallback_ips(self) -> list[str]:\n         \"\"\"Return validated fallback IPs from config (populated by _apply_env_overrides).\"\"\"","@@ -1008,14 +1010,252 @@ async def send_update_prompt(\n             logger.warning(\"[%s] send_update_prompt failed: %s\", self.name, e)\n             return SendResult(success=False, error=str(e))\n \n+    async def send_model_picker(\n+        self,\n+        chat_id: str,\n+        providers: list,\n+        current_model: str,\n+        current_provider: str,\n+        session_key: str,\n+        on_model_selected,\n+        metadata: Optional[Dict[str, Any]] = None,\n+    ) -> SendResult:\n+        \"\"\"Send an interactive inline-keyboard model picker.\n+\n+        Two-step drill-down: provider selection → model selection.\n+        Edits the same message in-place as the user navigates.\n+        \"\"\"\n+        if not self._bot:\n+            return SendResult(success=False, error=\"Not connected\")\n+\n+        try:\n+            from hermes_cli.providers import get_label\n+        except ImportError:\n+            def get_label(slug):\n+                return slug\n+\n+        try:\n+            # Build provider buttons — 2 per row\n+            buttons: list = []\n+            for p in providers:\n+                count = p.get(\"total_models\", len(p.get(\"models\", [])))\n+                label = f\"{p['name']} ({count})\"\n+                if p.get(\"is_current\"):\n+                    label = f\"✓ {label}\"\n+                # Compact callback data: mp:<slug>  (max 64 bytes)\n+                buttons.append(\n+                    InlineKeyboardButton(label, callback_data=f\"mp:{p['slug']}\")\n+                )\n+\n+            rows = [buttons[i : i + 2] for i in range(0, len(buttons), 2)]\n+            rows.append([InlineKeyboardButton(\"✗ Cancel\", callback_data=\"mx\")])\n+            keyboard = InlineKeyboardMarkup(rows)\n+\n+            provider_label = get_label(current_provider)\n+            text = (\n+                f\"⚙ *Model Configuration*\\n\\n\"\n+                f\"Current model: `{current_model or 'unknown'}`\\n\"\n+                f\"Provider: {provider_label}\\n\\n\"\n+                f\"Select a provider:\"\n+            )\n+\n+            thread_id = metadata.get(\"thread_id\") if metadata else None\n+            msg = await self._bot.send_message(\n+                chat_id=int(chat_id),\n+                text=text,\n+                parse_mode=ParseMode.MARKDOWN,\n+                reply_markup=keyboard,\n+                message_thread_id=int(thread_id) if thread_id else None,\n+            )\n+\n+            # Store picker state keyed by chat_id\n+            self._model_picker_state[str(chat_id)] = {\n+                \"msg_id\": msg.message_id,\n+                \"providers\": providers,\n+                \"session_key\": session_key,\n+                \"on_model_selected\": on_model_selected,\n+                \"current_model\": current_model,\n+                \"current_provider\": current_provider,\n+            }\n+\n+            return SendResult(success=True, message_id=str(msg.message_id))\n+        except Exception as e:\n+            logger.warning(\"[%s] send_model_picker failed: %s\", self.name, e)\n+            return SendResult(success=False, error=str(e))\n+\n+    async def _handle_model_picker_callback(\n+        self, query, data: str, chat_id: str\n+    ) -> None:\n+        \"\"\"Handle model picker inline keyboard callbacks (mp:/mm:/mb:/mx:).\"\"\"\n+        state = self._model_picker_state.get(chat_id)\n+        if not state:\n+            await query.answer(text=\"Picker expired — use /model again.\")\n+            return\n+\n+        try:\n+            from hermes_cli.providers import get_label\n+        except ImportError:\n+            def get_label(slug):\n+                return slug\n+\n+        if data.startswith(\"mp:\"):\n+            # --- Provider selected: show model buttons ---\n+            provider_slug = data[3:]\n+            provider = next(\n+                (p for p in state[\"providers\"] if p[\"slug\"] == provider_slug),\n+                None,\n+            )\n+            if not provider:\n+                await query.answer(text=\"Provider not found.\")\n+                return\n+\n+            models = provider.get(\"models\", [])\n+            state[\"selected_provider\"] = provider_slug\n+            state[\"selected_provider_name\"] = provider.get(\"name\", provider_slug)\n+            state[\"model_list\"] = models\n+\n+            buttons: list = []\n+            for i, model_id in enumerate(models):\n+                # Short display label: strip vendor prefix\n+                short = model_id.split(\"/\")[-1] if \"/\" in model_id else model_id\n+                # Truncate long model names for button label (max ~40 chars)\n+                if len(short) > 38:\n+                    short = short[:35] + \"...\"\n+                buttons.append(\n+                    InlineKeyboardButton(short, callback_data=f\"mm:{i}\")\n+                )\n+\n+            rows = [buttons[i : i + 2] for i in range(0, len(buttons), 2)]\n+            rows.append([\n+                InlineKeyboardButton(\"◀ Back\", callback_data=\"mb\"),\n+                InlineKeyboardButton(\"✗ Cancel\", callback_data=\"mx\"),\n+            ])\n+            keyboard = InlineKeyboardMarkup(rows)\n+\n+            pname = provider.get(\"name\", provider_slug)\n+            total = provider.get(\"total_models\", len(models))\n+            shown = len(models)\n+            extra = f\"\\n_{total - shown} more available — type `/model <name>` directly_\" if total > shown else \"\"\n+\n+            await query.edit_message_text(\n+                text=(\n+                    f\"⚙ *Model Configuration*\\n\\n\"\n+                    f\"Provider: *{pname}*\\n\"\n+                    f\"Select a model:{extra}\"\n+                ),\n+                parse_mode=ParseMode.MARKDOWN,\n+                reply_markup=keyboard,\n+            )\n+            await query.answer()\n+\n+        elif data.startswith(\"mm:\"):\n+            # --- Model selected: perform the switch ---\n+            try:\n+                idx = int(data[3:])\n+            except ValueError:\n+                await query.answer(text=\"Invalid selection.\")\n+                return\n+\n+            model_list = state.get(\"model_list\", [])\n+            if idx < 0 or idx >= len(model_list):\n+                await query.answer(text=\"Invalid model index.\")\n+                return\n+\n+            model_id = model_list[idx]\n+            provider_slug = state.get(\"selected_provider\", \"\")\n+            callback = state.get(\"on_model_selected\")\n+\n+            if not callback:\n+                await query.answer(text=\"Picker expired.\")\n+                return\n+\n+            try:\n+                result_text = await callback(chat_id, model_id, provider_slug)\n+            except Exception as exc:\n+                logger.error(\"Model picker switch failed: %s\", exc)\n+                result_text = f\"Error switching model: {exc}\"\n+\n+            # Edit message to show confirmation, remove buttons\n+            try:\n+                await query.edit_message_text(\n+                    text=result_text,\n+                    parse_mode=ParseMode.MARKDOWN,\n+                    reply_markup=None,\n+                )\n+            except Exception:\n+                # Markdown parse failure — retry as plain text\n+                try:\n+                    await query.edit_message_text(\n+                        text=result_text,\n+                        parse_mode=None,\n+                        reply_markup=None,\n+                    )\n+                except Exception:\n+                    pass\n+            await query.answer(text=\"Model switched!\")\n+\n+            # Clean up state\n+            self._model_picker_state.pop(chat_id, None)\n+\n+        elif data == \"mb\":\n+            # --- Back to provider list ---\n+            buttons = []\n+            for p in state[\"providers\"]:\n+                count = p.get(\"total_models\", len(p.get(\"models\", [])))\n+                label = f\"{p['name']} ({count})\"\n+                if p.get(\"is_current\"):\n+                    label = f\"✓ {label}\"\n+                buttons.append(\n+                    InlineKeyboardButton(label, callback_data=f\"mp:{p['slug']}\")\n+                )\n+\n+            rows = [buttons[i : i + 2] for i in range(0, len(buttons), 2)]\n+            rows.append([InlineKeyboardButton(\"✗ Cancel\", callback_data=\"mx\")])\n+            keyboard = InlineKeyboardMarkup(rows)\n+\n+            try:\n+                provider_label = get_label(state[\"current_provider\"])\n+            except Exception:\n+                provider_label = state[\"current_provider\"]\n+\n+            await query.edit_message_text(\n+                text=(\n+                    f\"⚙ *Model Configuration*\\n\\n\"\n+                    f\"Current model: `{state['current_model'] or 'unknown'}`\\n\"\n+                    f\"Provider: {provider_label}\\n\\n\"\n+                    f\"Select a provider:\"\n+                ),\n+                parse_mode=ParseMode.MARKDOWN,\n+                reply_markup=keyboard,\n+            )\n+            await query.answer()\n+\n+        elif data == \"mx\":\n+            # --- Cancel ---\n+            self._model_picker_state.pop(chat_id, None)\n+            await query.edit_message_text(\n+                text=\"Model selection cancelled.\",\n+                reply_markup=None,\n+            )\n+            await query.answer()\n+\n     async def _handle_callback_query(\n         self, update: \"Update\", context: \"ContextTypes.DEFAULT_TYPE\"\n     ) -> None:\n-        \"\"\"Handle inline keyboard button clicks (update prompts).\"\"\"\n+        \"\"\"Handle inline keyboard button clicks.\"\"\"\n         query = update.callback_query\n         if not query or not query.data:\n             return\n         data = query.data\n+\n+        # --- Model picker callbacks ---\n+        if data.startswith((\"mp:\", \"mm:\", \"mb\", \"mx\")):\n+            chat_id = str(query.message.chat_id) if query.message else None\n+            if chat_id:\n+                await self._handle_model_picker_callback(query, data, chat_id)\n+            return\n+\n+        # --- Update prompt callbacks ---\n         if not data.startswith(\"update_prompt:\"):\n             return\n         answer = data.split(\":\", 1)[1]  # \"y\" or \"n\""]},{"file":"gateway/run.py","hunks":["@@ -3464,11 +3464,11 @@ async def _handle_commands_command(self, event: MessageEvent) -> str:\n             lines.append(f\"_(Requested page {requested_page} was out of range, showing page {page}.)_\")\n         return \"\\n\".join(lines)\n     \n-    async def _handle_model_command(self, event: MessageEvent) -> str:\n+    async def _handle_model_command(self, event: MessageEvent) -> Optional[str]:\n         \"\"\"Handle /model command — switch model for this session.\n \n         Supports:\n-          /model                              — show current model info\n+          /model                              — interactive picker (Telegram/Discord) or text list\n           /model <name>                       — switch for this session only\n           /model <name> --global              — switch and persist to config.yaml\n           /model <name> --provider <provider> — switch provider + model","@@ -3516,8 +3516,118 @@ async def _handle_model_command(self, event: MessageEvent) -> str:\n             current_base_url = override.get(\"base_url\", current_base_url)\n             current_api_key = override.get(\"api_key\", current_api_key)\n \n-        # No args: show authenticated providers with models\n+        # No args: show interactive picker (Telegram/Discord) or text list\n         if not model_input and not explicit_provider:\n+            # Try interactive picker if the platform supports it\n+            adapter = self.adapters.get(source.platform)\n+            has_picker = (\n+                adapter is not None\n+                and getattr(type(adapter), \"send_model_picker\", None) is not None\n+            )\n+\n+            if has_picker:\n+                try:\n+                    providers = list_authenticated_providers(\n+                        current_provider=current_provider,\n+                        user_providers=user_provs,\n+                        max_models=8,\n+                    )\n+                except Exception:\n+                    providers = []\n+\n+                if providers:\n+                    # Build a callback closure for when the user picks a model.\n+                    # Captures self + locals needed for the switch logic.\n+                    _self = self\n+                    _session_key = session_key\n+                    _cur_model = current_model\n+                    _cur_provider = current_provider\n+                    _cur_base_url = current_base_url\n+                    _cur_api_key = [redacted:secret-assignment]\n+\n+                    async def _on_model_selected(\n+                        _chat_id: str, model_id: str, provider_slug: str\n+                    ) -> str:\n+                        \"\"\"Perform the model switch and return confirmation text.\"\"\"\n+                        result = _switch_model(\n+                            raw_input=model_id,\n+                            current_provider=_cur_provider,\n+                            current_model=_cur_model,\n+                            current_base_url=_cur_base_url,\n+                            current_api_key=[redacted:secret-assignment]\n+                            is_global=False,\n+                            explicit_provider=provider_slug,\n+                        )\n+                        if not result.success:\n+                            return f\"Error: {result.error_message}\"\n+\n+                        # Update cached agent in-place\n+                        cached_entry = None\n+                        _cache_lock = getattr(_self, \"_agent_cache_lock\", None)\n+                        _cache = getattr(_self, \"_agent_cache\", None)\n+                        if _cache_lock and _cache is not None:\n+                            with _cache_lock:\n+                                cached_entry = _cache.get(_session_key)\n+                        if cached_entry and cached_entry[0] is not None:\n+                            try:\n+                                cached_entry[0].switch_model(\n+                                    new_model=result.new_model,\n+                                    new_provider=result.target_provider,\n+                                    api_key=[redacted:secret-assignment]\n+                                    base_url=result.base_url,\n+                                    api_mode=result.api_mode,\n+                                )\n+                            except Exception as exc:\n+                                logger.warning(\"Picker model switch failed for cached agent: %s\", exc)\n+\n+                        # Store model note + session override\n+                        if not hasattr(_self, \"_pending_model_notes\"):\n+                            _self._pending_model_notes = {}\n+                        _self._pending_model_notes[_session_key] = (\n+                            f\"[Note: model was just switched from {_cur_model} to {result.new_model} \"\n+                            f\"via {result.provider_label or result.target_provider}. \"\n+                            f\"Adjust your self-identification accordingly.]\"\n+                        )\n+                        if not hasattr(_self, \"_session_model_overrides\"):\n+                            _self._session_model_overrides = {}\n+                        _self._session_model_overrides[_session_key] = {\n+                            \"model\": result.new_model,\n+                            \"provider\": result.target_provider,\n+                            \"api_key\": [redacted:secret-assignment]\n+                            \"base_url\": result.base_url,\n+                            \"api_mode\": result.api_mode,\n+                        }\n+\n+                        # Build confirmation text\n+                        plabel = result.provider_label or result.target_provider\n+                        lines = [f\"Model switched to `{result.new_model}`\"]\n+                        lines.append(f\"Provider: {plabel}\")\n+                        mi = result.model_info\n+                        if mi:\n+                            if mi.context_window:\n+                                lines.append(f\"Context: {mi.context_window:,} tokens\")\n+                            if mi.max_output:\n+                                lines.append(f\"Max output: {mi.max_output:,} tokens\")\n+                            if mi.has_cost_data():\n+                                lines.append(f\"Cost: {mi.format_cost()}\")\n+                            lines.append(f\"Capabilities: {mi.format_capabilities()}\")\n+                        lines.append(\"_(session only — use `/model <name> --global` to persist)_\")\n+                        return \"\\n\".join(lines)\n+\n+                    metadata = {\"thread_id\": source.thread_id} if source.thread_id else None\n+                    result = await adapter.send_model_picker(\n+                        chat_id=source.chat_id,\n+                        providers=providers,\n+                        current_model=current_model,\n+                        current_provider=current_provider,\n+                        session_key=session_key,\n+                        on_model_selected=_on_model_selected,\n+                        metadata=metadata,\n+                    )\n+                    if result.success:\n+                        return None  # Picker sent — adapter handles the response\n+\n+            # Fallback: text list (for platforms without picker or if picker failed)\n             provider_label = get_label(current_provider)\n             lines = [f\"Current: `{current_model or 'unknown'}` on {provider_label}\", \"\"]\n \n"]}]},{"kind":"pr","number":129845,"title":"feat(gateway): add /models slash command for listing a provider models","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"be6379218c9e","number":0},{"kind":"pr","value":"#118518","number":118518}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}