feat: whale-size route taxonomy for model + thinking-effort picker#2338
Conversation
…mbown#2026) Adds a central whale-route taxonomy that maps each (model, reasoning_effort) pair to a friendly whale-species label sorted from largest/deepest to smallest/fastest: Blue Whale — Pro + max thinking Fin Whale — Pro + high thinking Sperm Whale — Pro + no thinking Humpback — Flash + max thinking Minke Whale — Flash + high thinking Porpoise — Flash + no thinking For DeepSeek providers the /model picker now shows a single-column whale-route list instead of the two-column Model|Thinking layout. Each route sets both model and effort at once. Pass-through providers retain the classic layout. New whale_routes module: - WhaleRoute struct with label, model, effort, sort_order, hint, description - WHALE_ROUTES const array (6 routes) - for_model_effort() and by_sort_order() lookups - 8 unit tests Model picker changes: - show_whale_routes flag activates on DeepSeek providers - Selected route maps to model + effort simultaneously - Fallback row for "auto" and custom models - Updated test suite for whale-route behavior (7 new/updated tests) Refs: Hmbown#1676, Hmbown#2026
There was a problem hiding this comment.
Code Review
This pull request introduces 'whale routes' for DeepSeek providers in the model picker modal, mapping model and thinking-effort combinations to friendly whale-species labels sorted from largest to fastest. For other providers, the picker falls back to the classic two-column layout. Feedback on the changes highlights a compilation error in model_picker.rs due to a type mismatch in popup_height calculation, as well as a correctness bug in resolved_whale_model() where selecting the fallback row can incorrectly resolve to the initial model instead of 'auto'.
| fn render_whale_routes(&self, area: Rect, buf: &mut Buffer) { | ||
| let popup_width = 62.min(area.width.saturating_sub(4)).max(44); | ||
| let row_count = self.whale_route_row_count(); | ||
| let popup_height = (row_count + 4).min(area.height.saturating_sub(4)).max(8) as u16; |
There was a problem hiding this comment.
This line will fail to compile because row_count + 4 is of type usize while area.height.saturating_sub(4) is of type u16. In Rust, the .min() method requires both arguments to be of the same type. Casting the u16 value to usize resolves this type mismatch.
| let popup_height = (row_count + 4).min(area.height.saturating_sub(4)).max(8) as u16; | |
| let popup_height = (row_count + 4).min(area.height.saturating_sub(4) as usize).max(8) as u16; |
| fn resolved_whale_model(&self) -> String { | ||
| if self.selected_route_idx < WHALE_ROUTES.len() { | ||
| WHALE_ROUTES[self.selected_route_idx].model.to_string() | ||
| } else { | ||
| // Past the last whale route: "auto" or custom. | ||
| self.initial_model.clone() | ||
| } | ||
| } |
There was a problem hiding this comment.
There is a correctness bug when resolving the model from the fallback row. If initial_model is a standard model (e.g., "deepseek-v4-pro") and the user selects the fallback row (which is rendered as "auto — select per turn" because show_custom_model_row is false), resolved_whale_model() incorrectly returns self.initial_model.clone() (i.e., "deepseek-v4-pro") instead of "auto". Checking self.show_custom_model_row ensures that we correctly resolve to "auto" when the fallback row represents the auto-select option.
| fn resolved_whale_model(&self) -> String { | |
| if self.selected_route_idx < WHALE_ROUTES.len() { | |
| WHALE_ROUTES[self.selected_route_idx].model.to_string() | |
| } else { | |
| // Past the last whale route: "auto" or custom. | |
| self.initial_model.clone() | |
| } | |
| } | |
| fn resolved_whale_model(&self) -> String { | |
| if self.selected_route_idx < WHALE_ROUTES.len() { | |
| WHALE_ROUTES[self.selected_route_idx].model.to_string() | |
| } else if self.show_custom_model_row { | |
| self.initial_model.clone() | |
| } else { | |
| "auto".to_string() | |
| } | |
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ddac40909
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else { | ||
| // Past the last whale route: "auto" or custom. | ||
| self.initial_model.clone() |
There was a problem hiding this comment.
Return auto for the standard fallback row
When the current model is one of the standard whale routes, the picker appends a fallback row that is rendered as auto — select per turn, but selecting that row reaches this branch and returns initial_model instead of auto. In that scenario, choosing the displayed auto row applies the current model/effort unchanged rather than enabling per-turn auto selection; only the custom-model fallback should preserve initial_model.
Useful? React with 👍 / 👎.
| #[must_use] | ||
| pub fn new(app: &App) -> Self { | ||
| let hide_deepseek_models = crate::config::provider_passes_model_through(app.api_provider); | ||
| let show_whale_routes = !hide_deepseek_models; |
There was a problem hiding this comment.
Limit whale routes to official DeepSeek providers
For non-pass-through compatible providers such as NVIDIA NIM, OpenRouter, Novita, Fireworks, SGLang, and vLLM, provider_passes_model_through is also false, so this enables whale-route mode even though those providers use provider-specific model IDs elsewhere (for example deepseek-ai/deepseek-v4-pro for NIM). In that context the current provider-specific model is treated as a custom fallback, and selecting any whale route applies the bare DeepSeek ID directly, bypassing the provider remapping used by /model <id> and config normalization.
Useful? React with 👍 / 👎.
| // Fallback row: "auto" if not set, otherwise the current custom model. | ||
| let fallback_label = if self.initial_model == "auto" { | ||
| "auto — select per turn".to_string() | ||
| } else if self.show_custom_model_row { | ||
| format!("{} — custom", self.initial_model) |
There was a problem hiding this comment.
Keep auto selectable for custom models
When the current model is an unrecognized/custom DeepSeek ID, the single fallback row is rendered as the custom model instead of adding both the custom row and the documented auto option. That means a user on a snapshot/custom model can no longer switch back to per-turn auto from /model, whereas the previous two-column picker always kept the auto row alongside the custom model.
Useful? React with 👍 / 👎.
… array index - Fix usize/u16 type mismatch in popup_height (row_count as u16 + 4) - Fallback rows: always show "auto" first, then custom model if applicable - Limit whale routes to official DeepSeek/DeepSeekCN providers only - Use array position (not sort_order) for selected_route_idx lookup - Update tests for new fallback row indices
These two methods were accidentally placed inside the ModalView trait implementation block, which caused E0407 compilation errors on CI. They are now in a separate impl ModelPickerView block.
…fort - model_picker.rs no longer directly references WhaleRoute (uses WHALE_ROUTES.iter().position() instead of WhaleRoute::for_model_effort) - whale_routes.rs: for_model_effort is only used in tests; add #[allow(dead_code)] to suppress -D warnings in release builds
|
CI status: macOS ✅, ubuntu ✅, lint ✅. The Windows failure is a pre-existing flaky test in |
|
haha I like this - I am working on something similar with a whale pod mode - will use this to help! |
|
Still like the whale-route idea. I am leaving this open because review found one real model-picker bug: a known DeepSeek model paired with Please key that fallback on whether the model is actually |
|
Thanks @encyc, I still like this direction a lot. I tested the merged PR locally against current I would hold merge for one correctness fix: a known DeepSeek model paired with One small taxonomy note too: #2016 excludes porpoises from the user-facing whale pool, so the final Flash/off tier should use a small whale name from the shared taxonomy instead of Porpoise. After that, this looks harvestable as the model-picker slice; I would keep #2026 open for receipts/footer/agent-card integration. |
…ugh to auto row The whale-route fallback in the picker constructor used show_custom_model_row as the gate for selecting the 'auto' vs custom row, but a known DeepSeek model (e.g. v4-pro) paired with ReasoningEffort::Auto would not match any whale route yet still have show_custom_model_row=false — silently landing on the auto row and replacing the explicit model with 'auto' on apply. Key the fallback on whether the initial model is actually 'auto' instead. When a whale-route fallback selects the custom row, ensure show_custom_model_row is set to true so the row is visible in the picker UI. Also: - Add regression test: known-model + Auto effort must not fall to auto row. - Clean up picker_auto_model_forces_auto_effort_on_apply: remove manual mutations of selected_model_idx / selected_effort_idx which whale-route mode never reads. - Rename Porpoise → Beluga per Hmbown#2016, which excludes porpoises from the user-facing whale pool.
|
@Hmbown thanks for the careful review. Pushed fixes for all three points: 1. Auto effort fallthrough bug (the real one) Fixed by keying the fallback on Regression test added: 2. Cleaned up test 3. Porpoise → Beluga All 28 model_picker / whale_routes tests pass locally. |
Refs #2026. Related: #1676.
Summary
Adds a central whale-route taxonomy that maps each
(model, reasoning_effort)pair to a friendly whale-species label, sorted from largest/deepest to smallest/fastest.For DeepSeek providers, the
/modelpicker now shows a single-column whale-route list instead of the two-column Model | Thinking layout. Pass-through providers retain the classic layout.Routes (largest → fastest)
New module:
whale_routes.rsWhaleRoutestruct with label, model, effort, sort_order, hint, descriptionWHALE_ROUTESconst array (6 routes)for_model_effort()— look up route by model + effortby_sort_order()— look up by sort indexModel picker changes
show_whale_routesflag activates for DeepSeek providersFiles
3 files, +436 −48
Greptile Summary
This PR introduces a whale-route taxonomy that maps
(model, reasoning_effort)pairs to friendly whale-species labels (Blue Whale → Beluga) and wires them into the/modelpicker for DeepSeek providers, replacing the two-column model/effort layout with a single-column route list. Pass-through providers retain the classic layout.whale_routes.rsdefines the 6-entryWHALE_ROUTESconst,WhaleRoutestruct, and two lookup helpers (for_model_effort,by_sort_order), all covered by 9 unit tests.model_picker.rsaddsshow_whale_routes/selected_route_idxfields, routes selection logic, render helpers, and 10 updated/new tests; fallback rows handle "auto" and unrecognised custom model IDs correctly, andselected_route_idxis initialised from.position()(array index) rather thansort_order, avoiding a future fragility.Confidence Score: 5/5
Safe to merge — the three compile-blocking and logic issues from the previous review round are all resolved.
The impl-block placement, popup-height type cast, and array-index-vs-sort_order initialisation flagged in earlier rounds are all correctly addressed. Fallback-row logic is consistent across new(), resolved_whale_model/effort, whale_route_row_count, and render. The 19 total tests cover the key paths well.
No files require special attention.
Important Files Changed
Reviews (5): Last reviewed commit: "fix(#2338): prevent known model + Auto e..." | Re-trigger Greptile