Skip to content

fix: resolve /model command misleading behavior when switching to model from different provider#5578

Open
pandyzhou wants to merge 3 commits intoAstrBotDevs:masterfrom
pandyzhou:fix/model-command-cross-provider-misleading
Open

fix: resolve /model command misleading behavior when switching to model from different provider#5578
pandyzhou wants to merge 3 commits intoAstrBotDevs:masterfrom
pandyzhou:fix/model-command-cross-provider-misleading

Conversation

@pandyzhou
Copy link

@pandyzhou pandyzhou commented Feb 28, 2026

Motivation / 动机

问题:使用 /model <模型名> 切换到属于其他提供商的模型时(例如当前为 deepseek,输入 gpt-4 属于另一提供商),命令会输出「切换模型到 gpt-4。」,看似成功。但会话的 provider_perf 仍指向原提供商,后续 LLM 请求仍发往 deepseek,导致 "Model Not Exist" 错误,用户难以定位问题。

解决:实现智能模型解析——输入模型名时自动在所有提供商中查找,若在其他提供商中找到则自动切换提供商并设置模型,避免误导性成功提示和请求失败。

Modifications / 改动点

  • 修改文件astrbot/builtin_stars/builtin_commands/commands/provider.py

  • 新增_find_provider_for_model() 辅助方法,在所有 LLM 提供商中查找包含指定模型的提供商

  • 修改model_ls 中字符串分支逻辑:

    • 模型在当前提供商:设置模型并显示成功(含提供商信息)
    • 模型在其他提供商:自动切换 Provider + 设置模型,并提示「检测到模型 X 属于提供商 Y,已自动切换提供商并设置模型」
    • 模型未找到:不设置,提示错误并建议使用 /provider
  • 更新:Tips 文案,说明可自动跨提供商查找并切换

  • 修复:整数分支异常处理中缺少 return,导致错误消息被成功消息覆盖

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

验证步骤

  1. 配置多个 LLM 提供商(如 deepseek 和另一包含 gpt-4 模型的提供商)
  2. 当前使用 deepseek 时,执行 /model gpt-4
  3. 预期:提示「检测到模型 [ep-20260228215018-s7nq4] 属于提供商 [xxx],已自动切换提供商并设置模型。」
  4. 发起对话,预期:请求成功,无 "Model Not Exist" 错误

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了「验证步骤」和「运行截图」。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

Summary by Sourcery

改进 /model 命令,以便在不同提供方之间正确解析模型,并在切换模型时避免产生具有误导性的成功消息。

Bug 修复:

  • 确保基于整数的模型选择在发生错误时立即返回,以防失败消息被成功消息覆盖。
  • 修复 /model 行为:当指定其他提供方的模型时,不再在提供方保持不变的情况下仍声称切换成功。

增强功能:

  • 添加跨提供方的模型解析功能,可按模型名称在所有已配置的提供方中搜索,并在需要时自动切换提供方。
  • 更新 /model 的提示文案,说明自动跨提供方查找和自动切换提供方的行为。
Original summary in English

Summary by Sourcery

Improve /model command to correctly resolve models across providers and avoid misleading success messages when switching models.

Bug Fixes:

  • Ensure integer-based model selection returns immediately on errors so failure messages are not overridden by success messages.
  • Fix /model behavior where specifying a model from another provider no longer leaves the provider unchanged while claiming a successful switch.

Enhancements:

  • Add cross-provider model resolution that searches all configured providers by model name and automatically switches provider when needed.
  • Update /model tips text to explain automatic cross-provider lookup and provider switching behavior.

@auto-assign auto-assign bot requested review from Raven95676 and anka-afk February 28, 2026 16:32
@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Feb 28, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在解决 /model 命令在跨提供商切换模型时存在的误导性行为和潜在的请求失败问题。通过引入智能模型解析机制,系统现在能够自动识别用户输入的模型所属的提供商,并在必要时自动切换提供商,从而提供更流畅、更直观的用户体验,避免了因提供商不匹配而导致的错误。

Highlights

  • 新增跨提供商模型查找功能: 引入了 _find_provider_for_model 辅助方法,用于在所有已配置的 LLM 提供商中查找指定模型,支持排除当前提供商。
  • 优化 /model 命令行为: 重构了 /model 命令处理字符串(模型名)输入时的逻辑。现在,如果模型属于当前提供商,则直接设置;如果模型属于其他提供商,则自动切换提供商并设置模型;如果模型未找到,则给出明确的错误提示。
  • 更新用户提示信息: 修改了 /model 命令的提示文本,明确告知用户该命令现在支持自动跨提供商查找和切换模型。
  • 修复异常处理逻辑: 修正了 /model 命令处理整数输入时,在异常情况下缺少 return 语句的问题,确保错误消息能正确显示而不被后续成功消息覆盖。
Changelog
  • astrbot/builtin_stars/builtin_commands/commands/provider.py
    • Added TYPE_CHECKING import and Provider type hint for better type safety.
    • Introduced _find_provider_for_model asynchronous method to search for a given model across all available LLM providers.
    • Updated the model_ls command's string input handling to intelligently detect and switch providers if a model is found in a different provider.
    • Modified the success and error messages within model_ls to provide clearer feedback to the user regarding model and provider switching.
    • Corrected a logical flow in the integer input handling of model_ls to ensure error messages are properly returned after an exception.
Activity
  • 目前没有人类活动(评论、审查等)发生。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot bot added the area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. label Feb 28, 2026
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了两个问题,并留下了一些整体性的反馈:

  • _find_provider_for_model 中,对 p.get_models() 抛出的异常被静默忽略;建议至少记录提供商 ID 和错误详情,这样在跨提供商解析失败时,更容易诊断配置错误或 API 故障。
  • 新的跨提供商查找逻辑会在每次处理 /model <name> 字符串输入时,对所有提供商调用一次 get_models();如果 get_models() 调用成本较高或提供商数量较多,建议增加简单的缓存机制或对查找频率进行限制,避免产生不必要的重复查询。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_find_provider_for_model`, exceptions from `p.get_models()` are silently ignored; consider at least logging provider ID and error details so that misconfigured providers or API failures are easier to diagnose when cross-provider resolution fails.
- The new cross-provider lookup calls `get_models()` on all providers for every `/model <name>` string input; if `get_models()` is expensive or there are many providers, consider adding simple caching or a rate limit on lookups to avoid unnecessary repeated queries.

## Individual Comments

### Comment 1
<location path="astrbot/builtin_stars/builtin_commands/commands/provider.py" line_range="51-68" />
<code_context>
+            *[p.get_models() for p in all_providers],
+            return_exceptions=True,
+        )
+        for provider, result in zip(all_providers, results):
+            if isinstance(result, BaseException):
+                continue
+            provider_id = provider.meta().id
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Swallowing `BaseException` from `get_models()` may hide provider-specific issues.

Since every `BaseException` from `get_models()` is ignored, systemic issues (auth, networking, misconfig) can be silently skipped, making failures hard to diagnose. Please log these exceptions (ideally with the provider id), or at least log when all providers fail to return models so that configuration/runtime problems are visible.

```suggestion
    async def _find_provider_for_model(
        self, model_name: str, exclude_provider_id: str | None = None
    ) -> tuple["Provider" | None, str | None]:
        """在所有 LLM 提供商中查找包含指定模型的提供商。返回 (provider, provider_id) 或 (None, None)。"""
        all_providers = self.context.get_all_providers()
        results = await asyncio.gather(
            *[p.get_models() for p in all_providers],
            return_exceptions=True,
        )

        loop = asyncio.get_running_loop()
        failed_providers: list[str] = []

        for provider, result in zip(all_providers, results):
            if isinstance(result, BaseException):
                provider_id = provider.meta().id
                failed_providers.append(provider_id)
                loop.call_exception_handler(
                    {
                        "message": "Failed to get models from provider",
                        "exception": result,
                        "provider_id": provider_id,
                    }
                )
                continue

            provider_id = provider.meta().id
            if exclude_provider_id and provider_id == exclude_provider_id:
                continue
            if model_name in result:
                return provider, provider_id

        # 如果所有提供商都未能返回模型列表,则记录一条系统级错误,便于排查配置/运行时问题
        if failed_providers and len(failed_providers) == len(all_providers):
            loop.call_exception_handler(
                {
                    "message": "Failed to get models from all providers",
                    "providers": failed_providers,
                }
            )

        return None, None
```
</issue_to_address>

### Comment 2
<location path="astrbot/builtin_stars/builtin_commands/commands/provider.py" line_range="56-57" />
<code_context>
+    ) -> tuple["Provider" | None, str | None]:
+        """在所有 LLM 提供商中查找包含指定模型的提供商。返回 (provider, provider_id) 或 (None, None)。"""
+        all_providers = self.context.get_all_providers()
+        results = await asyncio.gather(
+            *[p.get_models() for p in all_providers],
+            return_exceptions=True,
+        )
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using `asyncio.gather(..., return_exceptions=True)` without any aggregate check may mask global failures.

With `return_exceptions=True`, if every `get_models()` call fails, `_find_provider_for_model` will return `(None, None)` just like the “model not found” case. Consider detecting when all entries in `results` are exceptions and either raising/logging a clear error so callers can distinguish configuration/connectivity failures from a genuinely missing model.

Suggested implementation:

```python
        all_providers = self.context.get_all_providers()
        results = await asyncio.gather(
            *[p.get_models() for p in all_providers],
            return_exceptions=True,
        )

        # 如果所有调用都失败,则这是配置/连接性问题,而不是单纯的“模型不存在”
        if results and all(isinstance(r, Exception) for r in results):
            logger = getattr(self.context, "logger", None)
            msg = f"Failed to fetch models from all providers when searching for model '{model_name}'"
            if logger is not None:
                logger.error(msg, extra={"model_name": model_name, "provider_count": len(all_providers), "errors": results})
            raise RuntimeError(msg)

```

This change assumes that the rest of `_find_provider_for_model` continues to work with `results` as a list aligned with `all_providers`. If you later refactor to skip individual failing providers, you may want to:
1. Filter out `Exception` entries in `results` when iterating to find the provider for `model_name`.
2. Optionally log per-provider failures (e.g., inside a `for provider, result in zip(all_providers, results)` loop).
</issue_to_address>

Sourcery 对开源项目是免费的——如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • In _find_provider_for_model, exceptions from p.get_models() are silently ignored; consider at least logging provider ID and error details so that misconfigured providers or API failures are easier to diagnose when cross-provider resolution fails.
  • The new cross-provider lookup calls get_models() on all providers for every /model <name> string input; if get_models() is expensive or there are many providers, consider adding simple caching or a rate limit on lookups to avoid unnecessary repeated queries.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_find_provider_for_model`, exceptions from `p.get_models()` are silently ignored; consider at least logging provider ID and error details so that misconfigured providers or API failures are easier to diagnose when cross-provider resolution fails.
- The new cross-provider lookup calls `get_models()` on all providers for every `/model <name>` string input; if `get_models()` is expensive or there are many providers, consider adding simple caching or a rate limit on lookups to avoid unnecessary repeated queries.

## Individual Comments

### Comment 1
<location path="astrbot/builtin_stars/builtin_commands/commands/provider.py" line_range="51-68" />
<code_context>
+            *[p.get_models() for p in all_providers],
+            return_exceptions=True,
+        )
+        for provider, result in zip(all_providers, results):
+            if isinstance(result, BaseException):
+                continue
+            provider_id = provider.meta().id
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Swallowing `BaseException` from `get_models()` may hide provider-specific issues.

Since every `BaseException` from `get_models()` is ignored, systemic issues (auth, networking, misconfig) can be silently skipped, making failures hard to diagnose. Please log these exceptions (ideally with the provider id), or at least log when all providers fail to return models so that configuration/runtime problems are visible.

```suggestion
    async def _find_provider_for_model(
        self, model_name: str, exclude_provider_id: str | None = None
    ) -> tuple["Provider" | None, str | None]:
        """在所有 LLM 提供商中查找包含指定模型的提供商。返回 (provider, provider_id) 或 (None, None)。"""
        all_providers = self.context.get_all_providers()
        results = await asyncio.gather(
            *[p.get_models() for p in all_providers],
            return_exceptions=True,
        )

        loop = asyncio.get_running_loop()
        failed_providers: list[str] = []

        for provider, result in zip(all_providers, results):
            if isinstance(result, BaseException):
                provider_id = provider.meta().id
                failed_providers.append(provider_id)
                loop.call_exception_handler(
                    {
                        "message": "Failed to get models from provider",
                        "exception": result,
                        "provider_id": provider_id,
                    }
                )
                continue

            provider_id = provider.meta().id
            if exclude_provider_id and provider_id == exclude_provider_id:
                continue
            if model_name in result:
                return provider, provider_id

        # 如果所有提供商都未能返回模型列表,则记录一条系统级错误,便于排查配置/运行时问题
        if failed_providers and len(failed_providers) == len(all_providers):
            loop.call_exception_handler(
                {
                    "message": "Failed to get models from all providers",
                    "providers": failed_providers,
                }
            )

        return None, None
```
</issue_to_address>

### Comment 2
<location path="astrbot/builtin_stars/builtin_commands/commands/provider.py" line_range="56-57" />
<code_context>
+    ) -> tuple["Provider" | None, str | None]:
+        """在所有 LLM 提供商中查找包含指定模型的提供商。返回 (provider, provider_id) 或 (None, None)。"""
+        all_providers = self.context.get_all_providers()
+        results = await asyncio.gather(
+            *[p.get_models() for p in all_providers],
+            return_exceptions=True,
+        )
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using `asyncio.gather(..., return_exceptions=True)` without any aggregate check may mask global failures.

With `return_exceptions=True`, if every `get_models()` call fails, `_find_provider_for_model` will return `(None, None)` just like the “model not found” case. Consider detecting when all entries in `results` are exceptions and either raising/logging a clear error so callers can distinguish configuration/connectivity failures from a genuinely missing model.

Suggested implementation:

```python
        all_providers = self.context.get_all_providers()
        results = await asyncio.gather(
            *[p.get_models() for p in all_providers],
            return_exceptions=True,
        )

        # 如果所有调用都失败,则这是配置/连接性问题,而不是单纯的“模型不存在”
        if results and all(isinstance(r, Exception) for r in results):
            logger = getattr(self.context, "logger", None)
            msg = f"Failed to fetch models from all providers when searching for model '{model_name}'"
            if logger is not None:
                logger.error(msg, extra={"model_name": model_name, "provider_count": len(all_providers), "errors": results})
            raise RuntimeError(msg)

```

This change assumes that the rest of `_find_provider_for_model` continues to work with `results` as a list aligned with `all_providers`. If you later refactor to skip individual failing providers, you may want to:
1. Filter out `Exception` entries in `results` when iterating to find the provider for `model_name`.
2. Optionally log per-provider failures (e.g., inside a `for provider, result in zip(all_providers, results)` loop).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@dosubot
Copy link

dosubot bot commented Feb 28, 2026

Related Documentation

Checked 1 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This PR addresses the misleading behavior of the /model command when switching models across different providers and introduces automatic cross-provider model lookup and switching, which is a significant improvement. However, it introduces a critical security vulnerability: model selection is not properly isolated between user sessions. Modifying the shared provider instance's state allows one user to change the model for all other users sharing that provider, potentially leading to service disruption or unexpected costs. Additionally, I've provided some refactoring suggestions to further simplify the code logic, enhance efficiency, and improve readability, particularly by integrating model lookup into the _find_provider_for_model method to avoid redundant get_models() calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant