Skip to content

fix(tui): accept custom model IDs in /model for non-DeepSeek providers (#1572)#2280

Open
Hmbown wants to merge 1 commit into
mainfrom
fix/1572-custom-model-switch
Open

fix(tui): accept custom model IDs in /model for non-DeepSeek providers (#1572)#2280
Hmbown wants to merge 1 commit into
mainfrom
fix/1572-custom-model-switch

Conversation

@Hmbown
Copy link
Copy Markdown
Owner

@Hmbown Hmbown commented May 27, 2026

Summary

Closes #1572.

/model <id> validated every input through the DeepSeek-only normalizer, so users on OpenAI-compatible providers or custom base URLs could list their models with /models but couldn't switch to them — the validator rejected anything that didn't match a canonical DeepSeek alias.

Reproduction from the issue:

api_key = "sk-..."
base_url = "https://opencode.ai/zen/go/v1"
default_text_model = "opencode-go/glm-5.1"

[models]
"opencode-go/glm-5.1" = {}
"opencode-go/kimi-k2.6" = {}
# ...

/models lists everything, but /model opencode-go/glm-5.1 returns Invalid model 'opencode-go/glm-5.1'. Expected auto or a DeepSeek model ID. ....

Fix

  • Add normalize_custom_model_id — pass-through with non-empty + control-character guards.
  • Add Config::model_ids_pass_through() — true when the active provider passes models through (OpenAI-compatible) OR the active provider preserves custom-base-URL models.
  • Store model_ids_passthrough on App so commands and the model picker can read it without re-deriving from Config. Set at construction and refreshed in switch_provider.
  • commands/core::model branches on App::accepts_custom_model_ids(): pass-through validator for non-DeepSeek/custom-base, the existing DeepSeek normalizer otherwise. DeepSeek validation is unchanged for DeepSeek users.
  • ModelPickerView::new uses the same predicate, so the picker's "hide DeepSeek models" hint and the /model validator agree.

Tests

Two new unit tests in commands::core::tests:

  • test_model_change_accepts_custom_id_for_openai_compatible_providerApiProvider::Openai + pass-through flag.
  • test_model_change_accepts_custom_id_for_custom_base_url — pass-through flag only (custom base URL path).

The existing test_model_change_rejects_invalid_model is unchanged and still pins DeepSeek-side validation.

Testing

  • cargo fmt --all -- --check
  • cargo clippy -p codewhale-tui --all-targets — clean for the touched code. The two clippy errors clippy 1.94 emits on main (commands/config.rs:476 useless_format, runtime_log.rs:177 redundant_closure) pre-exist on origin/main; PR style: fix two clippy warnings #2237 already proposes the fix and is CLEAN to merge.
  • cargo test -p codewhale-tui --bin codewhale-tui commands::core::tests — 32 passed (5 model-change tests including 2 new ones).

Checklist

  • Updated docs or comments as needed (inline rustdoc on the new field / helpers)
  • Added or updated tests where relevant
  • Verified TUI behavior manually if UI changes — covered by unit tests; the runtime path goes through App::accepts_custom_model_ids().

Notes for review

#1572)

`/model <id>` validated every input through the DeepSeek-only normalizer,
so users on OpenAI-compatible providers or custom base URLs could list
their models with `/models` but couldn't switch to them — the validator
rejected anything that didn't match a canonical DeepSeek alias.

Reported in #1572: with `base_url = "https://opencode.ai/zen/go/v1"` and
`opencode-go/glm-5.1`-style model IDs declared in `[models]`, `/models`
listed everything but `/model opencode-go/glm-5.1` was rejected as
"Invalid model".

Adds `normalize_custom_model_id` (pass-through with non-empty + control
character guards), exposes `model_ids_passthrough` on `App`, and lets
the `/model` command branch on the active provider/base URL: pass-through
for OpenAI-compatible / custom-base, DeepSeek validation otherwise. The
model picker uses the same predicate so its "hide DeepSeek models" hint
matches the validator.

Tests cover both pass-through paths (OpenAI provider and custom base URL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 12:59
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Copy link
Copy Markdown
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 pull request introduces support for custom model IDs when using OpenAI-compatible providers or custom base URLs, bypassing client-side model validation when appropriate. A critical issue was identified in model_ids_pass_through() where client-side validation is bypassed even for official DeepSeek domains if a custom base_url is configured, which could lead to unexpected API errors. A fix was suggested to explicitly check and prevent bypassing validation for official domains.

Comment thread crates/tui/src/config.rs
Comment on lines +1819 to +1823
pub(crate) fn model_ids_pass_through(&self) -> bool {
let provider = self.api_provider();
provider_passes_model_through(provider)
|| self.active_provider_preserves_custom_base_url_model()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

When the active provider is Deepseek or DeepseekCN, but the user has configured a custom base_url that still points to an official DeepSeek domain (for example, https://api.deepseek.com to opt out of beta features), active_provider_preserves_custom_base_url_model() will return true because the URL differs from the default beta endpoint (https://api.deepseek.com/beta).

This causes model_ids_pass_through() to return true, which completely bypasses client-side model validation for official DeepSeek endpoints. As a result, invalid or typoed model IDs will be accepted by the TUI, only to fail later with a 400 Bad Request from the DeepSeek API.

We should explicitly check if the custom base URL still points to an official DeepSeek domain (api.deepseek.com or api.deepseeki.com) and prevent bypassing validation in those cases.

    pub(crate) fn model_ids_pass_through(&self) -> bool {
        let provider = self.api_provider();
        if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
            let base_url = self.deepseek_base_url();
            let is_official = ["api.deepseek.com", "api.deepseeki.com"]
                .iter()
                .any(|domain| base_url.contains(domain));
            if is_official {
                return false;
            }
        }
        provider_passes_model_through(provider)
            || self.active_provider_preserves_custom_base_url_model()
    }

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

使用 opencode-go 无法切换其他开源模型?

2 participants