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.
This commit is contained in:
2026-07-27 12:12:14 -04:00
parent ad3840f9ec
commit 649c5f5641
5 changed files with 22 additions and 48 deletions
+6 -6
View File
@@ -1574,7 +1574,7 @@ impl PlatformId {
/// `models_cache.json`, the `visibility=="list"` AND `supported_in_api==true`
/// set) because OpenAI exposes no stable public models endpoint for the
/// ChatGPT Codex backend. Each entry carries context window + per-model
/// selectable reasoning efforts (incl. the codex-only `xhigh`/`max`/`ultra`
/// selectable reasoning efforts (incl. the codex-only `xhigh`/`max`
/// tiers), so the fetch path maps them through the SAME
/// `platform_wire_model_to_entry` output as a live listing — no new type.
pub fn hardcoded_catalog(self) -> Option<Vec<WireModel>> {
@@ -1617,13 +1617,13 @@ fn openai_codex_wire_models() -> Vec<WireModel> {
codex_wire_model(
"gpt-5.6-sol",
"GPT-5.6-Sol",
&["low", "medium", "high", "xhigh", "max", "ultra"],
&["low", "medium", "high", "xhigh", "max"],
"low",
),
codex_wire_model(
"gpt-5.6-terra",
"GPT-5.6-Terra",
&["low", "medium", "high", "xhigh", "max", "ultra"],
&["low", "medium", "high", "xhigh", "max"],
"medium",
),
codex_wire_model(
@@ -2655,7 +2655,7 @@ mod tests {
/// The HARDCODED openai-codex catalog is exactly the 4 supported+listed
/// models, keyed by slug, ctx 272000, each exposing its exact supported
/// efforts (incl. the codex-only `xhigh`/`max`/`ultra` tiers). The
/// efforts (incl. the codex-only `xhigh`/`max` tiers). The
/// list-but-broken / hidden models are absent. Every other platform serves
/// NO hardcoded catalog (its models come from the live wire).
#[test]
@@ -2702,11 +2702,11 @@ mod tests {
};
assert_eq!(
efforts("gpt-5.6-sol"),
["low", "medium", "high", "xhigh", "max", "ultra"]
["low", "medium", "high", "xhigh", "max"]
);
assert_eq!(
efforts("gpt-5.6-terra"),
["low", "medium", "high", "xhigh", "max", "ultra"]
["low", "medium", "high", "xhigh", "max"]
);
assert_eq!(
efforts("gpt-5.6-luna"),
+12 -36
View File
@@ -911,11 +911,6 @@ pub enum ReasoningEffort {
/// Messages both accept `xhigh` AND `max` as separate levels in 2026;
/// the Kimi wire spells its top tier `max` with no `xhigh`).
Max,
/// Codex-only top tier above `max` (the ChatGPT Codex backend exposes an
/// `ultra` reasoning effort on its flagship models). Reachable ONLY via a
/// model's server-declared effort menu (openai-codex); no built-in fallback
/// menu offers it, so other providers never emit it.
Ultra,
}
impl ReasoningEffort {
@@ -944,15 +939,12 @@ impl ReasoningEffort {
Self::High => "high",
Self::Xhigh => "xhigh",
Self::Max => "max",
Self::Ultra => "ultra",
}
}
/// Anthropic Messages API effort string; `None` for unsupported variants.
/// `xhigh` and `max` are distinct levels on the 2026 Messages API (both
/// appear in `GET /v1/models` `capabilities.effort`). `ultra` is codex-only
/// and never selected on an Anthropic model, but maps to its own string for
/// completeness (the Responses path writes effort via `as_str`, not this).
/// appear in `GET /v1/models` `capabilities.effort`).
pub fn to_messages_api(self) -> Option<&'static str> {
match self {
Self::None | Self::Minimal => None,
@@ -961,7 +953,6 @@ impl ReasoningEffort {
Self::High => Some("high"),
Self::Xhigh => Some("xhigh"),
Self::Max => Some("max"),
Self::Ultra => Some("ultra"),
}
}
}
@@ -984,9 +975,8 @@ impl std::str::FromStr for ReasoningEffort {
"high" => Ok(Self::High),
"xhigh" => Ok(Self::Xhigh),
"max" => Ok(Self::Max),
"ultra" => Ok(Self::Ultra),
_ => Err(format!(
"invalid reasoning effort: {s:?} (expected one of: none, minimal, low, medium, high, xhigh, max, ultra)"
"invalid reasoning effort: {s:?} (expected one of: none, minimal, low, medium, high, xhigh, max)"
)),
}
}
@@ -1878,30 +1868,16 @@ mod tests {
assert_eq!(ReasoningEffort::Max.to_messages_api(), Some("max"));
}
/// The codex-only `ultra` tier parses, serializes, and patches onto a
/// Responses body as `reasoning.effort = "ultra"` (the crux of surfacing a
/// codex model's full thinking menu). It is a DISTINCT level above `max`.
/// `ultra` is not a tier any backend accepts.
///
/// The Codex Responses endpoint rejects it with a 400 listing its menu,
/// which tops out at `max`. Parsing it would only send it again.
#[test]
fn reasoning_effort_ultra_is_a_distinct_codex_tier() {
assert_eq!(
"ultra".parse::<ReasoningEffort>().unwrap(),
ReasoningEffort::Ultra
);
assert_eq!(
"ULTRA".parse::<ReasoningEffort>().unwrap(),
ReasoningEffort::Ultra
);
assert_ne!(ReasoningEffort::Ultra, ReasoningEffort::Max);
assert_eq!(ReasoningEffort::Ultra.as_str(), "ultra");
let json = serde_json::to_string(&ReasoningEffort::Ultra).unwrap();
assert_eq!(json, "\"ultra\"");
assert_eq!(
serde_json::from_str::<ReasoningEffort>("\"ultra\"").unwrap(),
ReasoningEffort::Ultra
);
let mut body = serde_json::json!({ "model": "gpt-5.6-sol" });
patch_reasoning_effort(&mut body, Some(ReasoningEffort::Ultra));
assert_eq!(body["reasoning"]["effort"], "ultra");
fn reasoning_effort_ultra_is_not_a_tier() {
assert!("ultra".parse::<ReasoningEffort>().is_err());
assert!("ULTRA".parse::<ReasoningEffort>().is_err());
assert!(serde_json::from_str::<ReasoningEffort>("\"ultra\"").is_err());
assert_eq!(ReasoningEffort::Max.as_str(), "max");
}
/// The account id is decoded STATELESSLY from the bearer JWT payload's
@@ -2122,7 +2098,7 @@ mod tests {
);
let bad_type = as_map(serde_json::json!({"reasoningEffort": 3}));
assert_eq!(parse_reasoning_effort_meta(Some(&bad_type)), None);
// `ultra` is a real codex tier; a genuinely-unknown token still None.
// A genuinely-unknown token is dropped, not guessed at.
let unknown = as_map(serde_json::json!({"reasoningEffort": "MEGA"}));
assert_eq!(parse_reasoning_effort_meta(Some(&unknown)), None);
}
@@ -1475,7 +1475,7 @@ mod tests {
/// short-circuits BEFORE any HTTP — there is NO mock `/models` server, yet
/// the fetch returns exactly the 4 compiled-in models keyed
/// `openai-codex/<slug>` on the Responses backend, ctx 272000, each exposing
/// its exact reasoning efforts (incl. the codex-only `xhigh`/`max`/`ultra`).
/// its exact reasoning efforts (incl. the codex-only `xhigh`/`max`).
/// A BOGUS base URL confirms no live `/models` request is attempted (it would
/// otherwise fail against an unroutable host).
#[tokio::test(flavor = "multi_thread")]
@@ -1556,10 +1556,10 @@ mod tests {
.iter()
.map(|o| o.id.as_str())
.collect::<Vec<_>>(),
vec!["low", "medium", "high", "xhigh", "max", "ultra"],
"sol exposes the full codex effort menu incl. ultra"
vec!["low", "medium", "high", "xhigh", "max"],
"sol exposes the full codex effort menu"
);
// gpt-5.5 tops out at xhigh (no max/ultra).
// gpt-5.5 tops out at xhigh (no max).
let five_five = result
.models
.iter()
@@ -60,7 +60,6 @@ fn effort_label(effort: ReasoningEffort) -> String {
ReasoningEffort::High => "High",
ReasoningEffort::Xhigh => "X-High",
ReasoningEffort::Max => "Max",
ReasoningEffort::Ultra => "Ultra",
}
.to_string()
}
@@ -25,7 +25,6 @@ pub(crate) fn effort_description(level: ReasoningEffort) -> &'static str {
ReasoningEffort::High => "Heavy reasoning",
ReasoningEffort::Xhigh => "Extra-heavy reasoning",
ReasoningEffort::Max => "Maximum reasoning",
ReasoningEffort::Ultra => "Ultra reasoning",
}
}