fix(tui): accept custom model IDs in /model for non-DeepSeek providers (#1572)#2280
fix(tui): accept custom model IDs in /model for non-DeepSeek providers (#1572)#2280Hmbown wants to merge 1 commit into
Conversation
#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>
There was a problem hiding this comment.
Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
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.
| 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() | ||
| } |
There was a problem hiding this comment.
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()
}
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/modelsbut couldn't switch to them — the validator rejected anything that didn't match a canonical DeepSeek alias.Reproduction from the issue:
/modelslists everything, but/model opencode-go/glm-5.1returnsInvalid model 'opencode-go/glm-5.1'. Expected auto or a DeepSeek model ID. ....Fix
normalize_custom_model_id— pass-through with non-empty + control-character guards.Config::model_ids_pass_through()— true when the active provider passes models through (OpenAI-compatible) OR the active provider preserves custom-base-URL models.model_ids_passthroughonAppso commands and the model picker can read it without re-deriving fromConfig. Set at construction and refreshed inswitch_provider.commands/core::modelbranches onApp::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::newuses the same predicate, so the picker's "hide DeepSeek models" hint and the/modelvalidator agree.Tests
Two new unit tests in
commands::core::tests:test_model_change_accepts_custom_id_for_openai_compatible_provider—ApiProvider::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_modelis unchanged and still pins DeepSeek-side validation.Testing
cargo fmt --all -- --checkcargo clippy -p codewhale-tui --all-targets— clean for the touched code. The two clippy errors clippy 1.94 emits onmain(commands/config.rs:476useless_format,runtime_log.rs:177redundant_closure) pre-exist onorigin/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
App::accepts_custom_model_ids().Notes for review
accepts_custom_model_idshelper doesself.model_ids_passthrough || provider_passes_model_through(self.api_provider). The OR is intentional defense-in-depth in case a future code path mutatesapi_providerwithout going throughswitch_provider.accepts_custom_model_ids()predicate maps cleanly onto whatever per-provider machinery refactor: consolidate workspace crates — 14→11 (delete tui-core, merge hooks+agent) #2256 ends up shipping.