Merge pull request #2338 from encyc/feat/whale-routes-2026

feat: whale-size route taxonomy for model + thinking-effort picker
This commit is contained in:
Hunter Bown
2026-05-30 23:01:08 -07:00
committed by GitHub
3 changed files with 486 additions and 54 deletions
+1
View File
@@ -71,6 +71,7 @@ mod ui_text;
pub mod user_input;
pub mod views;
pub mod vim_mode;
pub mod whale_routes;
pub mod widgets;
pub mod workspace_context;
+298 -54
View File
@@ -1,22 +1,16 @@
//! `/model` picker modal: pick a DeepSeek model and a thinking-effort tier
//! and apply both at once (#39).
//! `/model` picker modal: pick a model and thinking-effort tier (#39, #2026).
//!
//! Two side-by-side panes — Models on the left, Thinking effort on the
//! right. Tab swaps focus, ↑/↓ moves within the focused pane, Enter applies
//! both and closes the modal. Esc applies the last-highlighted choice and
//! closes.
//! For DeepSeek providers the picker shows whale-sized routes — model + effort
//! combinations sorted largest → fastest with friendly whale-species labels
//! (Blue Whale, Fin Whale, …, Beluga). A single ↑/↓ selection sets both
//! model and effort at once. The "auto" option is always available; custom
//! (unrecognised) model ids appear as a separate row.
//!
//! The effort pane intentionally only exposes `Off / High / Max`. Per
//! DeepSeek's [Thinking Mode docs](https://api-docs.deepseek.com/guides/reasoning_model),
//! `low`/`medium` are silently mapped to `high` server-side and `xhigh` is
//! mapped to `max`, so surfacing them as separate choices would be misleading.
//! The legacy variants remain valid in `~/.deepseek/settings.toml` for
//! back-compat — the picker just doesn't offer them.
//! For pass-through providers the picker falls back to the classic two-column
//! layout (Models | Thinking), with no whale labelling.
//!
//! On apply we emit a [`ViewEvent::ModelPickerApplied`] with the resolved
//! model id and effort tier; the UI handler updates `App` state, persists
//! the choice via `Settings`, and forwards `Op::SetModel` so the running
//! engine picks up the change without a restart.
//! model id and effort tier.
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
@@ -30,6 +24,7 @@ use ratatui::{
use crate::palette;
use crate::tui::app::{App, ReasoningEffort};
use crate::tui::views::{ModalKind, ModalView, ViewAction, ViewEvent};
use crate::tui::whale_routes::WHALE_ROUTES;
/// Models the picker exposes by default. Kept short on purpose — power
/// users can still type `/model <id>` for anything else.
@@ -69,12 +64,21 @@ pub struct ModelPickerView {
/// When true, hide DeepSeek-specific model rows (pass-through providers
/// like openai don't support them).
hide_deepseek_models: bool,
/// When true, show whale-sized routes instead of two-column model/effort.
show_whale_routes: bool,
/// Selected whale-route index (when show_whale_routes is true).
selected_route_idx: usize,
}
impl ModelPickerView {
#[must_use]
pub fn new(app: &App) -> Self {
let hide_deepseek_models = crate::config::provider_passes_model_through(app.api_provider);
// Whale routes are DeepSeek-specific — only official providers get them.
let show_whale_routes = matches!(
app.api_provider,
crate::config::ApiProvider::Deepseek | crate::config::ApiProvider::DeepseekCN
);
let initial_model = if app.auto_model {
"auto".to_string()
} else {
@@ -104,6 +108,34 @@ impl ModelPickerView {
.position(|e| *e == normalized)
.unwrap_or(2); // default to High if somehow unknown
// When showing whale routes, find the matching route by position in the array
// (not by sort_order, which happens to match today but is semantically wrong).
let (selected_route_idx, show_custom_model_row) = if show_whale_routes {
let idx = WHALE_ROUTES
.iter()
.position(|r| {
r.model.eq_ignore_ascii_case(&initial_model) && r.effort == normalized
})
.unwrap_or_else(|| {
// No matching whale route — key the fallback on whether the
// current model is actually "auto", not on show_custom_model_row.
// Otherwise a known DeepSeek model (e.g. v4-pro) paired with
// ReasoningEffort::Auto silently falls through to the "auto" row
// and replaces the explicit model on apply.
if initial_model.eq_ignore_ascii_case("auto") {
WHALE_ROUTES.len() // "auto" row
} else {
WHALE_ROUTES.len() + 1 // custom model row
}
});
// When the whale-route fallback selected the custom row, ensure it is
// visible so the user can see their current model in the picker.
let show_custom = show_custom_model_row || idx == WHALE_ROUTES.len() + 1;
(idx, show_custom)
} else {
(0, show_custom_model_row)
};
Self {
initial_model,
initial_effort,
@@ -113,6 +145,8 @@ impl ModelPickerView {
selection_touched: false,
show_custom_model_row,
hide_deepseek_models,
show_whale_routes,
selected_route_idx,
}
}
@@ -128,10 +162,11 @@ impl ModelPickerView {
self.visible_model_ids().len() + if self.show_custom_model_row { 1 } else { 0 }
}
/// Resolve the currently highlighted model row to a model id. If the
/// custom row is selected we return the original model from the App so
/// "Apply" doesn't blow away an unrecognised id.
/// Resolve the currently highlighted row to a model id.
fn resolved_model(&self) -> String {
if self.show_whale_routes {
return self.resolved_whale_model();
}
let visible = self.visible_model_ids();
if self.show_custom_model_row && self.selected_model_idx == visible.len() {
self.initial_model.clone()
@@ -143,13 +178,59 @@ impl ModelPickerView {
}
fn resolved_effort(&self) -> ReasoningEffort {
if self.show_whale_routes {
return self.resolved_whale_effort();
}
if self.resolved_model().trim().eq_ignore_ascii_case("auto") {
return ReasoningEffort::Auto;
}
PICKER_EFFORTS[self.selected_effort_idx]
}
/// Resolve model from the whale-route list.
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.selected_route_idx == WHALE_ROUTES.len() {
// First fallback row: always "auto".
"auto".to_string()
} else {
// Second fallback row: custom model.
self.initial_model.clone()
}
}
/// Resolve effort from the whale-route list.
fn resolved_whale_effort(&self) -> ReasoningEffort {
if self.selected_route_idx < WHALE_ROUTES.len() {
WHALE_ROUTES[self.selected_route_idx].effort
} else if self.selected_route_idx == WHALE_ROUTES.len() {
// First fallback row: "auto".
ReasoningEffort::Auto
} else {
// Second fallback row: custom model — keep the initial effort.
self.initial_effort
}
}
/// Number of rows in the whale-route list.
fn whale_route_row_count(&self) -> usize {
let base = WHALE_ROUTES.len() + 1; // routes + auto
if self.show_custom_model_row {
base + 1
} else {
base
}
}
fn move_up(&mut self) -> bool {
if self.show_whale_routes {
if self.selected_route_idx > 0 {
self.selected_route_idx -= 1;
return true;
}
return false;
}
match self.focus {
Pane::Model => {
if self.selected_model_idx > 0 {
@@ -168,6 +249,14 @@ impl ModelPickerView {
}
fn move_down(&mut self) -> bool {
if self.show_whale_routes {
let max = self.whale_route_row_count().saturating_sub(1);
if self.selected_route_idx < max {
self.selected_route_idx += 1;
return true;
}
return false;
}
match self.focus {
Pane::Model => {
let max = self.model_row_count().saturating_sub(1);
@@ -285,7 +374,9 @@ impl ModalView for ModelPickerView {
ViewAction::None
}
KeyCode::Tab | KeyCode::Right | KeyCode::Left | KeyCode::BackTab => {
self.toggle_focus();
if !self.show_whale_routes {
self.toggle_focus();
}
ViewAction::None
}
_ => ViewAction::None,
@@ -293,6 +384,88 @@ impl ModalView for ModelPickerView {
}
fn render(&self, area: Rect, buf: &mut Buffer) {
if self.show_whale_routes {
self.render_whale_routes(area, buf);
} else {
self.render_classic(area, buf);
}
}
}
impl ModelPickerView {
/// Single-column whale-route list for DeepSeek providers.
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 as u16 + 4)
.min(area.height.saturating_sub(4))
.max(8);
let popup_area = Rect {
x: area.x + (area.width.saturating_sub(popup_width)) / 2,
y: area.y + (area.height.saturating_sub(popup_height)) / 2,
width: popup_width,
height: popup_height,
};
Clear.render(popup_area, buf);
let outer = Block::default()
.title(Line::from(Span::styled(
" Whale Routes ",
Style::default()
.fg(palette::DEEPSEEK_SKY)
.add_modifier(Modifier::BOLD),
)))
.title_bottom(Line::from(vec![
Span::styled(" ↑↓ ", Style::default().fg(palette::TEXT_MUTED)),
Span::raw("choose "),
Span::styled(" Enter ", Style::default().fg(palette::TEXT_MUTED)),
Span::raw("apply "),
Span::styled(" Esc ", Style::default().fg(palette::TEXT_MUTED)),
Span::raw("apply "),
]))
.borders(Borders::ALL)
.border_style(Style::default().fg(palette::BORDER_COLOR))
.style(Style::default());
let inner = outer.inner(popup_area);
outer.render(popup_area, buf);
let mut rows: Vec<(String, String)> = WHALE_ROUTES
.iter()
.map(|r| {
(
format!("{}{}", r.label, r.hint),
r.description.to_string(),
)
})
.collect();
// Fallback row 1: always "auto".
rows.push((
"auto — select per turn".to_string(),
"Let CodeWhale pick the best model each turn".to_string(),
));
// Fallback row 2: custom model when the current model isn't recognized.
if self.show_custom_model_row {
rows.push((
format!("{} — custom", self.initial_model),
"Current model (not a standard route)".to_string(),
));
}
self.render_pane(
inner,
buf,
"Model & thinking",
rows,
self.selected_route_idx,
true,
);
}
/// Classic two-column layout for pass-through providers.
fn render_classic(&self, area: Rect, buf: &mut Buffer) {
let popup_width = 64.min(area.width.saturating_sub(4)).max(40);
let popup_height = 14.min(area.height.saturating_sub(4)).max(10);
let popup_area = Rect {
@@ -455,12 +628,7 @@ mod tests {
app.auto_model = true;
app.reasoning_effort = ReasoningEffort::Off;
let mut view = ModelPickerView::new(&app);
view.selected_model_idx = 0;
view.selected_effort_idx = PICKER_EFFORTS
.iter()
.position(|effort| *effort == ReasoningEffort::Max)
.expect("max effort row");
let view = ModelPickerView::new(&app);
assert_eq!(view.resolved_model(), "auto");
assert_eq!(view.resolved_effort(), ReasoningEffort::Auto);
@@ -505,53 +673,46 @@ mod tests {
}
#[test]
fn arrow_keys_move_within_focused_pane() {
fn arrow_keys_move_within_whale_routes() {
let (app, _lock) = create_test_app();
let mut view = ModelPickerView::new(&app);
// Default focus is Model; move down then up.
let initial = view.selected_model_idx;
assert!(view.show_whale_routes);
let initial = view.selected_route_idx;
view.handle_key(KeyEvent::new(
KeyCode::Down,
crossterm::event::KeyModifiers::NONE,
));
assert_eq!(view.selected_model_idx, initial + 1);
assert_eq!(view.selected_route_idx, initial + 1);
view.handle_key(KeyEvent::new(
KeyCode::Up,
crossterm::event::KeyModifiers::NONE,
));
assert_eq!(view.selected_model_idx, initial);
assert_eq!(view.selected_route_idx, initial);
}
#[test]
fn tab_switches_focus_and_arrow_now_moves_effort() {
let (mut app, _lock) = create_test_app();
// Default is Max; pin to Off so the Down arrow has
// somewhere to go.
app.reasoning_effort = ReasoningEffort::Off;
fn tab_is_noop_in_whale_route_mode() {
let (app, _lock) = create_test_app();
let mut view = ModelPickerView::new(&app);
let initial_effort_idx = view.selected_effort_idx;
assert!(view.show_whale_routes);
let before = view.selected_route_idx;
view.handle_key(KeyEvent::new(
KeyCode::Tab,
crossterm::event::KeyModifiers::NONE,
));
assert_eq!(view.focus, Pane::Effort);
view.handle_key(KeyEvent::new(
KeyCode::Down,
crossterm::event::KeyModifiers::NONE,
));
assert!(view.selected_effort_idx > initial_effort_idx);
assert_eq!(view.selected_route_idx, before);
}
#[test]
fn enter_emits_apply_event_with_selection() {
fn enter_with_whale_routes_emits_apply_event() {
let (mut app, _lock) = create_test_app();
app.reasoning_effort = ReasoningEffort::High;
app.model = "deepseek-v4-pro".to_string();
app.auto_model = false;
let mut view = ModelPickerView::new(&app);
view.handle_key(KeyEvent::new(
KeyCode::Tab,
crossterm::event::KeyModifiers::NONE,
));
// Initial route: Fin Whale (Pro + High, sort_order=1)
assert_eq!(view.selected_route_idx, 1);
// Move down to Sperm Whale (Pro + Off, sort_order=2)
view.handle_key(KeyEvent::new(
KeyCode::Down,
crossterm::event::KeyModifiers::NONE,
@@ -568,13 +729,91 @@ mod tests {
..
}) => {
assert_eq!(model, "deepseek-v4-pro");
assert_eq!(effort, ReasoningEffort::Max);
assert_eq!(effort, ReasoningEffort::Off);
assert_eq!(previous_effort, ReasoningEffort::High);
}
other => panic!("expected ModelPickerApplied EmitAndClose, got {other:?}"),
}
}
#[test]
fn whale_routes_initial_selection_matches_app_state() {
let (mut app, _lock) = create_test_app();
app.model = "deepseek-v4-flash".to_string();
app.auto_model = false;
app.reasoning_effort = ReasoningEffort::Max;
let view = ModelPickerView::new(&app);
// Humpback = Flash + Max, sort_order = 3
assert_eq!(view.selected_route_idx, 3);
assert_eq!(view.resolved_model(), "deepseek-v4-flash");
assert_eq!(view.resolved_effort(), ReasoningEffort::Max);
}
#[test]
fn whale_routes_known_model_auto_effort_does_not_fall_to_auto() {
// Regression: a known DeepSeek model paired with ReasoningEffort::Auto
// must NOT fall through to the "auto" row — that would silently replace
// the explicit model with "auto" on apply.
let (mut app, _lock) = create_test_app();
app.model = "deepseek-v4-pro".to_string();
app.auto_model = false;
app.reasoning_effort = ReasoningEffort::Auto;
let view = ModelPickerView::new(&app);
// Should fall to custom row (WHALE_ROUTES.len() + 1), not auto row.
assert_eq!(view.selected_route_idx, WHALE_ROUTES.len() + 1);
assert_eq!(view.resolved_model(), "deepseek-v4-pro");
assert_eq!(view.resolved_effort(), ReasoningEffort::Auto);
// The custom row must be visible so the user sees their current model.
assert!(view.show_custom_model_row);
}
#[test]
fn whale_routes_auto_effort_maps_to_fallback_row() {
let (mut app, _lock) = create_test_app();
app.model = "auto".to_string();
app.auto_model = true;
app.reasoning_effort = ReasoningEffort::Auto;
let view = ModelPickerView::new(&app);
// "auto" doesn't match any whale route, falls to fallback row
assert_eq!(view.selected_route_idx, WHALE_ROUTES.len());
assert_eq!(view.resolved_model(), "auto");
assert_eq!(view.resolved_effort(), ReasoningEffort::Auto);
}
#[test]
fn whale_routes_custom_model_falls_back() {
let (mut app, _lock) = create_test_app();
app.model = "deepseek-v4-pro-2026-04-XX".to_string();
app.auto_model = false;
app.reasoning_effort = ReasoningEffort::High;
let view = ModelPickerView::new(&app);
// Custom model → second fallback row (after "auto")
assert_eq!(view.selected_route_idx, WHALE_ROUTES.len() + 1);
assert_eq!(view.resolved_model(), "deepseek-v4-pro-2026-04-XX");
assert_eq!(view.resolved_effort(), ReasoningEffort::High);
// Row count includes routes + auto + custom
assert_eq!(view.whale_route_row_count(), WHALE_ROUTES.len() + 2);
}
#[test]
fn whale_routes_down_from_last_is_noop() {
let (app, _lock) = create_test_app();
let mut view = ModelPickerView::new(&app);
// Navigate to the last row
view.selected_route_idx = view.whale_route_row_count() - 1;
let result = view.move_down();
assert!(!result);
}
#[test]
fn whale_routes_up_from_first_is_noop() {
let (app, _lock) = create_test_app();
let mut view = ModelPickerView::new(&app);
view.selected_route_idx = 0;
let result = view.move_up();
assert!(!result);
}
#[test]
fn immediate_esc_applies_current_selection() {
let (app, _lock) = create_test_app();
@@ -592,9 +831,12 @@ mod tests {
}
#[test]
fn esc_after_selection_move_applies_highlighted_model() {
let (app, _lock) = create_test_app();
fn esc_after_selection_move_applies_highlighted_route() {
let (mut app, _lock) = create_test_app();
app.reasoning_effort = ReasoningEffort::High;
let mut view = ModelPickerView::new(&app);
// Initial: Fin Whale (Pro+High), previous_effort=High
// Down → Sperm Whale (Pro+Off)
view.handle_key(KeyEvent::new(
KeyCode::Down,
crossterm::event::KeyModifiers::NONE,
@@ -608,13 +850,15 @@ mod tests {
match action {
ViewAction::EmitAndClose(ViewEvent::ModelPickerApplied {
model,
previous_model,
effort,
previous_effort,
..
}) => {
assert_eq!(previous_model, "deepseek-v4-pro");
assert_eq!(model, "deepseek-v4-flash");
assert_eq!(model, "deepseek-v4-pro");
assert_eq!(effort, ReasoningEffort::Off);
assert_eq!(previous_effort, ReasoningEffort::High);
}
other => panic!("expected Esc to apply highlighted model, got {other:?}"),
other => panic!("expected Esc to apply highlighted route, got {other:?}"),
}
}
+187
View File
@@ -0,0 +1,187 @@
//! Whale-size route taxonomy for model + thinking-effort combinations (#2026).
//!
//! Maps each `(model, reasoning_effort)` pair to a friendly whale-species label,
//! sorted from largest/deepest to smallest/fastest. The labels share the same
//! species pool as sub-agent nicknames (#2016) but serve a different purpose:
//! route/tier names help users understand depth/cost/speed at a glance.
//!
//! ## Route ordering (size → speed)
//!
//! 1. Blue Whale — Pro + max thinking (largest, deepest)
//! 2. Fin Whale — Pro + high thinking
//! 3. Sperm Whale — Pro + no thinking
//! 4. Humpback — Flash + max thinking
//! 5. Minke Whale — Flash + high thinking
//! 6. Beluga — Flash + no thinking (smallest, fastest)
//!
//! Unknown or non-DeepSeek models fall back to the raw model id without
//! fake whale labeling.
use crate::tui::app::ReasoningEffort;
/// One whale-sized route: a model + thinking-effort combination with
/// a friendly label, sort order, and descriptive hint.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WhaleRoute {
/// Whale-species label, e.g. "Blue Whale".
pub label: &'static str,
/// Model id, e.g. "deepseek-v4-pro".
pub model: &'static str,
/// Reasoning effort tier.
pub effort: ReasoningEffort,
/// Sort index (0 = largest / deepest).
pub sort_order: usize,
/// Short inline hint, e.g. "Pro + max thinking".
pub hint: &'static str,
/// Longer description for tooltips / route receipts.
pub description: &'static str,
}
/// Six canonical routes, sorted largest → smallest.
pub const WHALE_ROUTES: &[WhaleRoute] = &[
WhaleRoute {
label: "Blue Whale",
model: "deepseek-v4-pro",
effort: ReasoningEffort::Max,
sort_order: 0,
hint: "Pro + max thinking",
description: "Flagship reasoning at maximum depth — architecture, debugging, security reviews",
},
WhaleRoute {
label: "Fin Whale",
model: "deepseek-v4-pro",
effort: ReasoningEffort::High,
sort_order: 1,
hint: "Pro + high thinking",
description: "Deep reasoning for complex tasks — multi-file refactors, careful planning",
},
WhaleRoute {
label: "Sperm Whale",
model: "deepseek-v4-pro",
effort: ReasoningEffort::Off,
sort_order: 2,
hint: "Pro + no thinking",
description: "Full model power without reasoning overhead — straightforward code generation",
},
WhaleRoute {
label: "Humpback",
model: "deepseek-v4-flash",
effort: ReasoningEffort::Max,
sort_order: 3,
hint: "Flash + max thinking",
description: "Fast model with reasoning depth — lightweight analysis, first-pass reviews",
},
WhaleRoute {
label: "Minke Whale",
model: "deepseek-v4-flash",
effort: ReasoningEffort::High,
sort_order: 4,
hint: "Flash + high thinking",
description: "Fast model, moderate reasoning — tool execution, read-only scouting",
},
WhaleRoute {
label: "Beluga",
model: "deepseek-v4-flash",
effort: ReasoningEffort::Off,
sort_order: 5,
hint: "Flash + no thinking",
description: "Fastest and cheapest — lookups, searches, simple edits",
},
];
impl WhaleRoute {
/// Look up the whale route for a given model id and reasoning effort.
/// Returns `None` for non-DeepSeek models or unrecognized combinations.
#[must_use]
#[allow(dead_code)]
pub fn for_model_effort(model: &str, effort: ReasoningEffort) -> Option<&'static WhaleRoute> {
WHALE_ROUTES
.iter()
.find(|r| r.model.eq_ignore_ascii_case(model) && r.effort == effort)
}
/// Look up a whale route by its sort-order index.
#[must_use]
#[allow(dead_code)]
pub fn by_sort_order(index: usize) -> Option<&'static WhaleRoute> {
WHALE_ROUTES.iter().find(|r| r.sort_order == index)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn routes_are_sorted_by_size() {
for window in WHALE_ROUTES.windows(2) {
assert!(
window[0].sort_order < window[1].sort_order,
"{} should sort before {}",
window[0].label,
window[1].label
);
}
}
#[test]
fn lookup_blue_whale_for_pro_max() {
let route = WhaleRoute::for_model_effort("deepseek-v4-pro", ReasoningEffort::Max)
.expect("blue whale route exists");
assert_eq!(route.label, "Blue Whale");
assert_eq!(route.model, "deepseek-v4-pro");
assert_eq!(route.effort, ReasoningEffort::Max);
assert_eq!(route.sort_order, 0);
}
#[test]
fn lookup_beluga_for_flash_off() {
let route = WhaleRoute::for_model_effort("deepseek-v4-flash", ReasoningEffort::Off)
.expect("beluga route exists");
assert_eq!(route.label, "Beluga");
assert_eq!(route.sort_order, 5);
}
#[test]
fn lookup_case_insensitive_model() {
let route = WhaleRoute::for_model_effort("DeepSeek-V4-Pro", ReasoningEffort::High)
.expect("case-insensitive match");
assert_eq!(route.label, "Fin Whale");
}
#[test]
fn unknown_model_returns_none() {
assert!(WhaleRoute::for_model_effort("gpt-4o", ReasoningEffort::High).is_none());
}
#[test]
fn unknown_effort_with_valid_model_returns_none() {
// ReasoningEffort::Auto is not in any whale route
assert!(WhaleRoute::for_model_effort("deepseek-v4-pro", ReasoningEffort::Auto).is_none());
}
#[test]
fn by_sort_order_finds_correct_routes() {
assert_eq!(WhaleRoute::by_sort_order(0).unwrap().label, "Blue Whale");
assert_eq!(WhaleRoute::by_sort_order(5).unwrap().label, "Beluga");
assert!(WhaleRoute::by_sort_order(99).is_none());
}
#[test]
fn every_route_has_unique_sort_order() {
let orders: Vec<usize> = WHALE_ROUTES.iter().map(|r| r.sort_order).collect();
let mut sorted = orders.clone();
sorted.sort();
sorted.dedup();
assert_eq!(orders.len(), sorted.len(), "duplicate sort orders detected");
}
#[test]
fn every_route_has_unique_label() {
let labels: Vec<&str> = WHALE_ROUTES.iter().map(|r| r.label).collect();
let mut sorted = labels.clone();
sorted.sort();
sorted.dedup();
assert_eq!(labels.len(), sorted.len(), "duplicate labels detected");
}
}