Files
Kigi-CLI/crates/codegen/kigi-tui/src/slash/commands/effort_levels.rs
T
ZacharyZhang-NY 649c5f5641 fix(models): drop the ultra reasoning effort, which no backend accepts
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.
2026-07-27 12:12:14 -04:00

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()
}