The hardcoded Codex catalog advertised `ultra` for gpt-5.6-sol and gpt-5.6-terra, so selecting it sent `reasoning.effort = "ultra"` and the Responses endpoint answered 400 with its real menu, which ends at `max`. The tier was kigi's invention: the enum documented it as codex-only, the catalog was its only producer, and the upstream model snapshot never listed it. Removing the variant as well as the catalog entries means `/effort ultra` is rejected at the TUI instead of parsing and failing on the wire; a persisted `ultra` degrades to the model default through `lenient_reasoning_effort_opt`, as designed for vocabulary changes.
82 lines
3.3 KiB
Rust
82 lines
3.3 KiB
Rust
//! Shared reasoning-effort dropdown levels for `/model` and `/effort`.
|
|
|
|
use kigi_shell::sampling::types::{ReasoningEffort, ReasoningEffortOption};
|
|
|
|
use crate::slash::command::ArgItem;
|
|
|
|
/// Effort levels in the built-in fallback menu (strongest first). `none`/`minimal`
|
|
/// are still accepted by `ReasoningEffort::from_str` for power users. `max` is
|
|
/// deliberately absent: it exists only where a model's server menu offers it
|
|
/// (e.g. Kimi K3) — the legacy fallback reproduces the historical rows, and
|
|
/// offering `max` on models that reject it would 400.
|
|
pub(crate) const EFFORT_LEVELS: &[ReasoningEffort] = &[
|
|
ReasoningEffort::Xhigh,
|
|
ReasoningEffort::High,
|
|
ReasoningEffort::Medium,
|
|
ReasoningEffort::Low,
|
|
];
|
|
|
|
pub(crate) fn effort_description(level: ReasoningEffort) -> &'static str {
|
|
match level {
|
|
ReasoningEffort::None => "No reasoning",
|
|
ReasoningEffort::Minimal => "Minimal reasoning",
|
|
ReasoningEffort::Low => "Faster, lighter reasoning",
|
|
ReasoningEffort::Medium => "Balanced reasoning",
|
|
ReasoningEffort::High => "Heavy reasoning",
|
|
ReasoningEffort::Xhigh => "Extra-heavy reasoning",
|
|
ReasoningEffort::Max => "Maximum reasoning",
|
|
}
|
|
}
|
|
|
|
/// The built-in menu used when the server sends no `reasoningEfforts`. Reproduces
|
|
/// the historical rows: labels are the lowercase level (via `Display`),
|
|
/// descriptions from `effort_description`. The active row is matched by value
|
|
/// against the session effort at render time, so `default` is left unset here.
|
|
pub(crate) fn legacy_effort_options() -> Vec<ReasoningEffortOption> {
|
|
EFFORT_LEVELS
|
|
.iter()
|
|
.map(|&level| ReasoningEffortOption {
|
|
id: level.as_str().to_string(),
|
|
value: level,
|
|
label: level.to_string(),
|
|
description: Some(effort_description(level).to_string()),
|
|
default: false,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Build effort rows for autocomplete from a per-model option list.
|
|
///
|
|
/// - `mark_active` + `current_effort` mark the current session effort with `(active)`.
|
|
/// - `insert_text_for` controls what is inserted on select:
|
|
/// - `/effort`: the option id (`"deep"`)
|
|
/// - `/model` chained phase: `"ModelName deep"`
|
|
///
|
|
/// `match_text` gets an `a `/`b `/…` sort prefix so the matcher's alphabetical
|
|
/// tiebreak preserves the option order.
|
|
pub(crate) fn build_effort_arg_items(
|
|
options: &[ReasoningEffortOption],
|
|
current_effort: Option<ReasoningEffort>,
|
|
mark_active: bool,
|
|
insert_text_for: impl Fn(&ReasoningEffortOption) -> String,
|
|
) -> Vec<ArgItem> {
|
|
options
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, option)| {
|
|
let active = mark_active && current_effort == Some(option.value);
|
|
let active_suffix = if active { " (active)" } else { "" };
|
|
let insert_text = insert_text_for(option);
|
|
// Sort-key prefix: 'a' for top row, 'b' for next, etc. Only
|
|
// affects matcher tiebreak ordering, never rendered.
|
|
let sort_prefix = char::from(b'a' + idx as u8);
|
|
ArgItem {
|
|
display: format!("{}{active_suffix}", option.label),
|
|
match_text: format!("{sort_prefix} {insert_text}"),
|
|
insert_text,
|
|
description: option.description.clone().unwrap_or_default(),
|
|
}
|
|
})
|
|
.collect()
|
|
}
|