feat(models): add GPT-5.5 support

This commit is contained in:
James Peter
2026-04-25 22:00:56 +10:00
parent dd4f263596
commit bd64cb7d1c
10 changed files with 536 additions and 78 deletions
+3
View File
@@ -1651,6 +1651,9 @@ dependencies = [
[[package]]
name = "code-version"
version = "0.0.0"
dependencies = [
"serde_json",
]
[[package]]
name = "codex-experimental-api-macros"
+3
View File
@@ -6,3 +6,6 @@ build = "build.rs"
[lib]
path = "src/lib.rs"
[dependencies]
serde_json = { workspace = true }
+94 -11
View File
@@ -1,5 +1,8 @@
use std::collections::HashMap;
use std::sync::LazyLock;
use serde_json::Value;
// Compile-time embedded version string.
// Prefer the CODE_VERSION provided by CI; fall back to the package
// version for local builds.
@@ -11,23 +14,36 @@ pub const CODE_VERSION: &str = {
};
const ANNOUNCEMENT_TIP: &str = include_str!("../../../announcement_tip.toml");
const MODELS_MANIFEST: &str = include_str!("../../../codex-rs/models-manager/models.json");
pub const MIN_WIRE_COMPAT_VERSION_FALLBACK: &str = "0.101.0";
static MIN_WIRE_COMPAT_VERSION: LazyLock<&'static str> = LazyLock::new(|| {
let extracted = extract_max_semver(ANNOUNCEMENT_TIP).unwrap_or(MIN_WIRE_COMPAT_VERSION_FALLBACK);
let Some(extracted_triplet) = parse_semver_triplet(extracted) else {
return MIN_WIRE_COMPAT_VERSION_FALLBACK;
static MIN_WIRE_COMPAT_VERSION: LazyLock<String> = LazyLock::new(|| {
let mut minimum = MIN_WIRE_COMPAT_VERSION_FALLBACK.to_string();
if let Some(extracted) = extract_max_semver(ANNOUNCEMENT_TIP) {
minimum = max_semver(&minimum, extracted).to_string();
}
minimum
});
static MODEL_MINIMUM_CLIENT_VERSIONS: LazyLock<HashMap<String, String>> =
LazyLock::new(|| parse_model_minimum_client_versions(MODELS_MANIFEST));
fn max_semver<'a>(current: &'a str, candidate: &'a str) -> &'a str {
let Some(current_triplet) = parse_semver_triplet(current) else {
return candidate;
};
let Some(fallback_triplet) = parse_semver_triplet(MIN_WIRE_COMPAT_VERSION_FALLBACK) else {
return extracted;
let Some(candidate_triplet) = parse_semver_triplet(candidate) else {
return current;
};
if extracted_triplet < fallback_triplet {
MIN_WIRE_COMPAT_VERSION_FALLBACK
if candidate_triplet > current_triplet {
candidate
} else {
extracted
current
}
});
}
fn parse_semver_triplet(version: &str) -> Option<(u64, u64, u64)> {
let trimmed = version.trim().trim_start_matches('v');
@@ -71,6 +87,33 @@ fn extract_max_semver(input: &'static str) -> Option<&'static str> {
max.map(|(_, version)| version)
}
fn parse_model_minimum_client_versions(input: &str) -> HashMap<String, String> {
let Ok(root) = serde_json::from_str::<Value>(input) else {
return HashMap::new();
};
let Some(models) = root.get("models").and_then(Value::as_array) else {
return HashMap::new();
};
let mut versions = HashMap::new();
for model in models {
let Some(slug) = model.get("slug").and_then(Value::as_str) else {
continue;
};
let Some(candidate) = model.get("minimal_client_version").and_then(Value::as_str) else {
continue;
};
if parse_semver_triplet(candidate).is_none() {
continue;
};
versions.insert(slug.to_ascii_lowercase(), candidate.to_string());
}
versions
}
fn wire_compatible_version_for<'a>(version: &'a str, minimum: &'a str) -> &'a str {
let Some(version_triplet) = parse_semver_triplet(version) else {
return version;
@@ -93,7 +136,7 @@ pub fn version() -> &'static str {
#[inline]
pub fn min_wire_compat_version() -> &'static str {
*MIN_WIRE_COMPAT_VERSION
MIN_WIRE_COMPAT_VERSION.as_str()
}
#[inline]
@@ -101,6 +144,17 @@ pub fn wire_compatible_version() -> &'static str {
wire_compatible_version_for(CODE_VERSION, min_wire_compat_version())
}
pub fn wire_compatible_version_for_model(model: &str) -> String {
let canonical_model = model.rsplit('/').next().unwrap_or(model).trim();
let Some(required_version) = MODEL_MINIMUM_CLIENT_VERSIONS
.get(&canonical_model.to_ascii_lowercase())
else {
return wire_compatible_version().to_string();
};
max_semver(wire_compatible_version(), required_version).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -165,6 +219,35 @@ mod tests {
assert!(configured >= fallback);
}
#[test]
fn parse_model_minimum_client_versions_extracts_versions() {
let input = r#"{
"models": [
{"slug": "gpt-5.4", "minimal_client_version": "0.98.0"},
{"slug": "gpt-5.5", "minimal_client_version": "0.124.0"},
{"slug": "legacy", "minimal_client_version": "0.0.1"}
]
}"#;
assert_eq!(
parse_model_minimum_client_versions(input).get("gpt-5.5"),
Some(&"0.124.0".to_string())
);
}
#[test]
fn wire_compatible_version_for_model_raises_for_gpt_5_5() {
assert_eq!(wire_compatible_version_for_model("gpt-5.5"), "0.124.0");
}
#[test]
fn wire_compatible_version_for_model_uses_base_version_for_unknown_models() {
assert_eq!(
wire_compatible_version_for_model("unknown-model"),
wire_compatible_version()
);
}
#[test]
fn parse_semver_triplet_rejects_four_component_versions() {
assert_eq!(parse_semver_triplet("1.2.3.4"), None);
+37
View File
@@ -49,6 +49,37 @@ const ALL_TEXT_VERBOSITY: &[TextVerbosityConfig] = &[
static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
vec![
ModelPreset {
id: "gpt-5.5".to_string(),
model: "gpt-5.5".to_string(),
display_name: "GPT-5.5".to_string(),
description: "Frontier model for complex coding, research, and real-world work."
.to_string(),
default_reasoning_effort: ReasoningEffort::Medium,
supported_reasoning_efforts: vec![
ReasoningEffortPreset {
effort: ReasoningEffort::Low,
description: "Fast responses with lighter reasoning".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::Medium,
description: "Balances speed and reasoning depth for everyday tasks".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::High,
description: "Greater reasoning depth for complex problems".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::XHigh,
description: "Extra high reasoning depth for complex problems".to_string(),
},
],
supported_text_verbosity: ALL_TEXT_VERBOSITY,
is_default: false,
upgrade: None,
pro_only: false,
show_in_picker: true,
},
ModelPreset {
id: "gpt-5.4".to_string(),
model: "gpt-5.4".to_string(),
@@ -673,6 +704,12 @@ mod tests {
assert!(presets.iter().any(|preset| preset.id == "gpt-5.4-mini"));
}
#[test]
fn gpt_5_5_available_for_chatgpt_auth() {
let presets = builtin_model_presets(Some(AuthMode::Chatgpt), true);
assert!(presets.iter().any(|preset| preset.id == "gpt-5.5"));
}
#[test]
fn clamp_reasoning_effort_downgrades_to_supported_level() {
let clamped = clamp_reasoning_effort_for_model(
+5
View File
@@ -46,6 +46,7 @@ pub(crate) async fn stream_chat_completions(
model_slug: &str,
client: &reqwest::Client,
provider: &ModelProviderInfo,
responses_originator_header: &str,
debug_logger: &Arc<Mutex<DebugLogger>>,
auth_manager: Option<Arc<AuthManager>>,
otel_event_manager: Option<OtelEventManager>,
@@ -396,6 +397,10 @@ pub(crate) async fn stream_chat_completions(
let base_auth = auth_manager.as_ref().and_then(|m| m.auth());
let auth = provider.effective_auth(&base_auth).await?;
let mut req_builder = provider.create_request_builder_with_auth(client, &auth).await?;
req_builder = req_builder.headers(crate::default_client::requested_model_headers(
Some(responses_originator_header),
model_slug,
));
if let Some(auth) = auth.as_ref() {
if auth.mode.is_chatgpt() {
+15
View File
@@ -365,6 +365,17 @@ impl ModelClient {
}
}
fn apply_requested_model_headers(
&self,
req_builder: reqwest::RequestBuilder,
model: &str,
) -> reqwest::RequestBuilder {
req_builder.headers(crate::default_client::requested_model_headers(
Some(self.config.responses_originator_header.as_str()),
model,
))
}
fn current_reasoning_param(
&self,
family: &ModelFamily,
@@ -631,6 +642,7 @@ impl ModelClient {
model_slug,
&self.client,
&self.provider,
self.config.responses_originator_header.as_str(),
&self.debug_logger,
self.auth_manager.clone(),
self.otel_event_manager.clone(),
@@ -821,6 +833,7 @@ impl ModelClient {
url,
)
.await?;
req_builder = self.apply_requested_model_headers(req_builder, request_model);
let has_beta_header = req_builder
.try_clone()
@@ -1293,6 +1306,7 @@ impl ModelClient {
.provider
.create_request_builder_with_auth(&self.client, &auth)
.await?;
req_builder = self.apply_requested_model_headers(req_builder, request_model);
let has_beta_header = req_builder
.try_clone()
@@ -1927,6 +1941,7 @@ impl ModelClient {
.provider
.create_compact_request_builder_with_auth(&self.client, &auth)
.await?;
request = self.apply_requested_model_headers(request, model_slug);
// Ensure Responses API beta header is present for compact calls. Mirror the
// streaming path: use the public "responses=v1" header for the public OpenAI
+24
View File
@@ -3517,6 +3517,30 @@ context_mode = "1m"
Ok(())
}
#[test]
fn context_mode_one_m_is_inert_for_gpt_5_5() -> anyhow::Result<()> {
let code_home = TempDir::new()?;
let cfg = toml::from_str::<ConfigToml>(
r#"
model = "gpt-5.5"
context_mode = "1m"
"#,
)?;
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides {
cwd: Some(code_home.path().to_path_buf()),
..Default::default()
},
code_home.path().to_path_buf(),
)?;
assert_eq!(config.context_mode, Some(ContextMode::OneM));
assert_eq!(config.model_context_window, Some(272_000));
assert_eq!(config.model_auto_compact_token_limit, Some(244_800));
Ok(())
}
#[test]
fn context_mode_auto_expands_gpt_5_4_context() -> anyhow::Result<()> {
let code_home = TempDir::new()?;
+77 -1
View File
@@ -1,4 +1,5 @@
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderValue;
use std::sync::LazyLock;
use std::sync::Mutex;
@@ -94,11 +95,49 @@ pub fn get_code_user_agent(originator_override: Option<&str>) -> String {
get_code_user_agent_with_suffix(originator_override, suffix.as_deref())
}
pub fn get_code_user_agent_for_model(originator_override: Option<&str>, model: &str) -> String {
let suffix = USER_AGENT_SUFFIX
.lock()
.ok()
.and_then(|guard| guard.clone());
let build_version = code_version::wire_compatible_version_for_model(model);
get_code_user_agent_with_version(originator_override, suffix.as_deref(), &build_version)
}
pub fn requested_model_headers(originator_override: Option<&str>, model: &str) -> HeaderMap {
let requested_version = code_version::wire_compatible_version_for_model(model);
let user_agent = get_code_user_agent_for_model(originator_override, model);
let mut headers = HeaderMap::new();
headers.insert(
"version",
HeaderValue::from_str(&requested_version)
.expect("requested model version should be a valid header value"),
);
headers.insert(
reqwest::header::USER_AGENT,
HeaderValue::from_str(&user_agent)
.expect("requested model user-agent should be a valid header value"),
);
headers
}
pub fn get_code_user_agent_with_suffix(
originator_override: Option<&str>,
user_agent_suffix: Option<&str>,
) -> String {
let build_version = code_version::wire_compatible_version();
get_code_user_agent_with_version(
originator_override,
user_agent_suffix,
code_version::wire_compatible_version(),
)
}
fn get_code_user_agent_with_version(
originator_override: Option<&str>,
user_agent_suffix: Option<&str>,
build_version: &str,
) -> String {
let os_info = os_info::get();
let originator_value = originator_override
.map(str::to_string)
@@ -249,6 +288,43 @@ mod tests {
});
}
#[test]
fn requested_model_headers_replace_version_and_user_agent() {
with_originator_env_cleared(|| {
let client = reqwest::Client::new();
let mut original_headers = HeaderMap::new();
original_headers.insert("version", HeaderValue::from_static("0.101.0"));
original_headers.insert(
reqwest::header::USER_AGENT,
HeaderValue::from_static("old-originator/0.101.0"),
);
let request = client
.get("https://example.com")
.headers(original_headers)
.headers(requested_model_headers(Some("test_originator"), "gpt-5.5"))
.build()
.expect("request should build");
let version_values: Vec<_> = request
.headers()
.get_all("version")
.iter()
.map(|value| value.to_str().expect("version header"))
.collect();
assert_eq!(version_values, vec!["0.124.0"]);
let ua_values: Vec<_> = request
.headers()
.get_all(reqwest::header::USER_AGENT)
.iter()
.map(|value| value.to_str().expect("user-agent header"))
.collect();
assert_eq!(ua_values.len(), 1);
assert!(ua_values[0].starts_with("test_originator/0.124.0"));
});
}
#[tokio::test]
async fn test_create_client_sets_default_headers() {
use wiremock::Mock;
@@ -651,19 +651,23 @@ impl ModelSelectionView {
});
let context_selected = matches!(self.entries().get(self.selected_index), Some(EntryKind::ContextMode));
let context_status = match self.current_context_mode {
Some(ContextMode::OneM) => "enabled",
Some(ContextMode::Auto) => "auto",
Some(ContextMode::Disabled) | None => "disabled",
};
let context_available = self.supports_extended_context();
let context_status = if context_available {
match self.current_context_mode {
Some(ContextMode::OneM) => "enabled",
Some(ContextMode::Auto) => "auto",
Some(ContextMode::Disabled) | None => "disabled",
}
} else {
"unavailable"
};
let mut context_label_style = Style::default().fg(crate::colors::text());
if context_selected {
context_label_style = context_label_style
.bg(crate::colors::selection())
.add_modifier(Modifier::BOLD);
}
if self.current_context_mode.is_some() {
if context_available && self.current_context_mode.is_some() {
context_label_style = context_label_style.fg(crate::colors::success());
}
if !context_available {
@@ -898,15 +902,7 @@ impl ModelSelectionView {
}
fn compare_presets(a: &FlatPreset, b: &FlatPreset) -> Ordering {
let model_rank = Self::model_rank(&a.model).cmp(&Self::model_rank(&b.model));
if model_rank != Ordering::Equal {
return model_rank;
}
let model_name_rank = a
.model
.to_ascii_lowercase()
.cmp(&b.model.to_ascii_lowercase());
let model_name_rank = Self::compare_model_names(&a.model, &b.model);
if model_name_rank != Ordering::Equal {
return model_name_rank;
}
@@ -919,34 +915,53 @@ impl ModelSelectionView {
a.label.cmp(&b.label)
}
fn model_rank(model: &str) -> u8 {
if model.eq_ignore_ascii_case("gpt-5.4") {
0
} else if model.eq_ignore_ascii_case("gpt-5.4-mini") {
1
} else if model.eq_ignore_ascii_case("gpt-5.3-codex") {
2
} else if model.eq_ignore_ascii_case("gpt-5.3-codex-spark") {
3
} else if model.eq_ignore_ascii_case("gpt-5.2-codex") {
4
} else if model.eq_ignore_ascii_case("gpt-5.2") {
5
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-max") {
6
} else if model.eq_ignore_ascii_case("gpt-5.1-codex") {
7
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-mini") {
8
} else if model.eq_ignore_ascii_case("gpt-5.1") {
9
} else {
10
fn compare_model_names(a: &str, b: &str) -> Ordering {
let a_lower = a.to_ascii_lowercase();
let b_lower = b.to_ascii_lowercase();
match (
Self::parse_model_version_components(&a_lower),
Self::parse_model_version_components(&b_lower),
) {
(Some(a_components), Some(b_components)) => {
let component_rank = b_components.cmp(&a_components);
if component_rank != Ordering::Equal {
return component_rank;
}
a_lower.cmp(&b_lower)
}
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => a_lower.cmp(&b_lower),
}
}
fn parse_model_version_components(model: &str) -> Option<Vec<u32>> {
let canonical = model.rsplit('/').next().unwrap_or(model);
let mut components = Vec::new();
for segment in canonical.split('-') {
let first = segment.chars().next()?;
if !first.is_ascii_digit() {
continue;
}
for part in segment.split('.') {
if part.is_empty() || !part.chars().all(|ch| ch.is_ascii_digit()) {
return None;
}
components.push(part.parse().ok()?);
}
return (!components.is_empty()).then_some(components);
}
None
}
fn model_description(model: &str) -> Option<&'static str> {
if model.eq_ignore_ascii_case("gpt-5.4") {
if model.eq_ignore_ascii_case("gpt-5.5") {
Some("Frontier model for complex coding, research, and real-world work.")
} else if model.eq_ignore_ascii_case("gpt-5.4") {
Some("Brings together flagship reasoning, coding, and tool use in a single frontier model.")
} else if model.eq_ignore_ascii_case("gpt-5.4-mini") {
Some("Smaller GPT-5.4 variant tuned for faster coding loops.")
@@ -1160,6 +1175,17 @@ mod tests {
rows
}
fn sorted_model_order(view: &ModelSelectionView) -> Vec<String> {
let mut order = Vec::new();
for idx in view.sorted_indices() {
let model = view.flat_presets[idx].model.clone();
if order.last() != Some(&model) {
order.push(model);
}
}
order
}
#[test]
fn model_selection_scrolls_selected_model_into_view() {
let presets = (0..12).map(|i| make_preset(&format!("model-{i:02}"))).collect();
@@ -1167,7 +1193,7 @@ mod tests {
let mut view = ModelSelectionView::new(
presets,
"model-00".to_string(),
"model-11".to_string(),
ReasoningEffort::Low,
None,
None,
@@ -1176,7 +1202,22 @@ mod tests {
AppEventSender::new(tx),
);
for _ in 0..13 {
let last_model = sorted_model_order(&view)
.last()
.cloned()
.expect("at least one model");
for _ in 0..view.entries().len() {
let selected_entry = view
.entries()
.get(view.selected_index)
.copied()
.expect("selected entry");
if let EntryKind::Preset(idx) = selected_entry
&& view.flat_presets[idx].model == last_model
{
break;
}
let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
}
@@ -1195,9 +1236,19 @@ mod tests {
height,
}, &mut buf);
let lines = buffer_body_lines(&buf, width, height);
let has_last_model = lines.iter().any(|line| line.contains("MODEL-11"));
assert!(has_last_model);
let selected_entry = view
.entries()
.get(view.selected_index)
.copied()
.expect("selected entry");
assert!(matches!(selected_entry, EntryKind::Preset(_)));
assert_eq!(view.flat_presets[match selected_entry {
EntryKind::Preset(idx) => idx,
_ => unreachable!("selected entry should be a preset"),
}]
.model, last_model);
assert!(view.scroll_top.get() > 0);
}
#[test]
@@ -1248,7 +1299,19 @@ mod tests {
}
#[test]
fn model_selection_prioritizes_gpt_5_4_and_shows_fast_mode_toggle() {
fn model_selection_prioritizes_higher_versions_and_shows_fast_mode_toggle() {
let mut gpt_5_5 = make_preset("gpt-5.5");
gpt_5_5.supported_reasoning_efforts = vec![
ReasoningEffortPreset {
effort: ReasoningEffort::Low.into(),
description: "Fast responses with lighter reasoning".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::High.into(),
description: "Maximizes reasoning depth".to_string(),
},
];
let mut gpt_5_4 = make_preset("gpt-5.4");
gpt_5_4.supported_reasoning_efforts = vec![
ReasoningEffortPreset {
@@ -1278,14 +1341,15 @@ mod tests {
make_preset("gpt-5.3-codex-spark"),
gpt_5_4_mini,
gpt_5_4,
gpt_5_5,
];
let (tx, _rx) = mpsc::channel::<AppEvent>();
let view = ModelSelectionView::new(
presets,
"gpt-5.4".to_string(),
"gpt-5.5".to_string(),
ReasoningEffort::Low,
Some(ServiceTier::Fast),
Some(ContextMode::OneM),
None,
false,
ModelSelectionTarget::Session,
AppEventSender::new(tx),
@@ -1306,25 +1370,27 @@ mod tests {
height,
}, &mut buf);
let order = sorted_model_order(&view);
assert_eq!(
order,
vec![
"gpt-5.5".to_string(),
"gpt-5.4".to_string(),
"gpt-5.4-mini".to_string(),
"gpt-5.3-codex".to_string(),
"gpt-5.3-codex-spark".to_string(),
]
);
let lines = buffer_body_lines(&buf, width, height);
let visible = lines.join("\n");
let gpt_5_4_idx = visible
.find("GPT-5.4")
.expect("gpt-5.4 header");
let gpt_5_4_mini_idx = visible
.find("GPT-5.4-Mini")
.expect("gpt-5.4-mini header");
let gpt_5_3_idx = visible
.find("GPT-5.3-Codex")
.expect("gpt-5.3-codex header");
assert!(gpt_5_4_idx < gpt_5_4_mini_idx);
assert!(gpt_5_4_mini_idx < gpt_5_3_idx);
assert!(visible.contains("Mode Settings"));
assert!(visible.contains("Fast Mode: enabled"));
assert!(visible.contains("1M Context: enabled"));
assert!(visible.contains("1M Context: unavailable"));
assert!(visible.contains("Fast mode speeds up replies."));
assert!(visible.contains("1M Context is available on supported models."));
assert!(visible.contains(
"Unavailable for this model. Saved settings apply automatically on supported models."
));
}
#[test]
@@ -1443,7 +1509,55 @@ mod tests {
}, &mut buf);
let lines = buffer_body_lines(&buf, width, height);
assert!(lines.iter().any(|line| line.contains("1M Context: auto")));
assert!(
lines.iter().any(|line| line.contains("1M Context: unavailable")),
"expected unsupported model to show unavailable context, got:\n{}",
lines.join("\n")
);
assert!(lines.iter().any(|line| {
line.contains("Unavailable for this model. Saved settings apply automatically on supported models.")
}));
}
#[test]
fn model_selection_marks_gpt_5_5_context_unavailable() {
let presets = vec![make_preset("gpt-5.5")];
let (tx, _rx) = mpsc::channel::<AppEvent>();
let view = ModelSelectionView::new(
presets,
"gpt-5.5".to_string(),
ReasoningEffort::Low,
None,
Some(ContextMode::OneM),
false,
ModelSelectionTarget::Session,
AppEventSender::new(tx),
);
let width = 100;
let height = 18;
let mut buf = ratatui::buffer::Buffer::empty(Rect {
x: 0,
y: 0,
width,
height,
});
view.render(
Rect {
x: 0,
y: 0,
width,
height,
},
&mut buf,
);
let lines = buffer_body_lines(&buf, width, height);
assert!(
lines.iter().any(|line| line.contains("1M Context: unavailable")),
"expected gpt-5.5 to show unavailable context, got:\n{}",
lines.join("\n")
);
assert!(lines.iter().any(|line| {
line.contains("Unavailable for this model. Saved settings apply automatically on supported models.")
}));
+102 -4
View File
@@ -48,6 +48,7 @@ use code_core::plan_tool::{PlanItemArg, StepStatus, UpdatePlanArgs};
use code_core::model_family::derive_default_model_family;
use code_core::model_family::find_family_for_model;
use code_core::model_family::resolve_context_mode_limits;
use code_core::model_family::supports_extended_context;
use code_core::account_usage::{
self,
RateLimitWarningScope,
@@ -23020,14 +23021,15 @@ Have we met every part of this goal and is there no further work to do?"#
}
fn curated_model_presets(presets: Vec<ModelPreset>) -> Vec<ModelPreset> {
const MODEL_PICKER_ORDER: [&str; 4] = [
const MODEL_PICKER_ORDER: [&str; 5] = [
"gpt-5.5",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
];
let curated: Vec<ModelPreset> = MODEL_PICKER_ORDER
let mut curated: Vec<ModelPreset> = MODEL_PICKER_ORDER
.into_iter()
.filter_map(|model| {
presets.iter().find(|preset| {
@@ -23038,6 +23040,21 @@ Have we met every part of this goal and is there no further work to do?"#
.cloned()
.collect();
let curated_slugs: std::collections::HashSet<String> = curated
.iter()
.flat_map(|preset| {
[
preset.id.to_ascii_lowercase(),
preset.model.to_ascii_lowercase(),
]
})
.collect();
curated.extend(presets.iter().filter(|preset| {
!curated_slugs.contains(&preset.id.to_ascii_lowercase())
&& !curated_slugs.contains(&preset.model.to_ascii_lowercase())
}).cloned());
if curated.is_empty() { presets } else { curated }
}
@@ -23536,8 +23553,19 @@ Have we met every part of this goal and is there no further work to do?"#
self.refresh_settings_overview_rows();
let status = match self.config.context_mode {
Some(ContextMode::OneM) => "1M context enabled.".to_string(),
Some(ContextMode::Auto) => "Auto Context enabled.".to_string(),
Some(ContextMode::OneM) if supports_extended_context(&self.config.model) => {
"1M context enabled.".to_string()
}
Some(ContextMode::OneM) => {
"1M context is unavailable for this model and will only apply on supported models."
.to_string()
}
Some(ContextMode::Auto) if supports_extended_context(&self.config.model) => {
"Auto Context enabled.".to_string()
}
Some(ContextMode::Auto) => {
"Auto Context will only expand on supported models.".to_string()
}
Some(ContextMode::Disabled) | None => "Context mode disabled.".to_string(),
};
self.bottom_pane.flash_footer_notice(status);
@@ -31285,6 +31313,76 @@ use code_core::protocol::OrderMeta;
assert_eq!(ids, vec!["gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex"]);
}
#[test]
fn builtin_model_presets_include_gpt_5_5() {
let presets = builtin_model_presets(Some(AuthMode::ChatGPT), true);
assert!(presets.iter().any(|preset| preset.id == "gpt-5.5"));
}
#[test]
fn curated_model_presets_appends_new_remote_models() {
let mut gpt_5_5 = builtin_model_presets(Some(AuthMode::ChatGPT), true)
.into_iter()
.find(|preset| preset.id == "gpt-5.4")
.expect("expected gpt-5.4 builtin preset");
gpt_5_5.id = "gpt-5.5".to_string();
gpt_5_5.model = "gpt-5.5".to_string();
gpt_5_5.display_name = "GPT-5.5".to_string();
let presets = vec![
gpt_5_5,
ModelPreset {
id: "gpt-5.4".to_string(),
model: "gpt-5.4".to_string(),
display_name: "gpt-5.4".to_string(),
description: "Frontier flagship model.".to_string(),
default_reasoning_effort: ReasoningEffort::Medium.into(),
supported_reasoning_efforts: vec![ReasoningEffortPreset {
effort: ReasoningEffort::Medium.into(),
description: "balanced".to_string(),
}],
supported_text_verbosity: &[TextVerbosity::Medium],
is_default: false,
upgrade: None,
pro_only: false,
show_in_picker: true,
},
];
let curated = ChatWidget::curated_model_presets(presets);
let ids: Vec<&str> = curated.iter().map(|preset| preset.id.as_str()).collect();
assert_eq!(ids, vec!["gpt-5.5", "gpt-5.4"]);
}
#[test]
fn curated_model_presets_deduplicates_alias_slugs() {
let canonical = ModelPreset {
id: "alias/gpt-5.5".to_string(),
model: "gpt-5.5".to_string(),
display_name: "GPT-5.5".to_string(),
description: "Frontier flagship model.".to_string(),
default_reasoning_effort: ReasoningEffort::Medium.into(),
supported_reasoning_efforts: vec![ReasoningEffortPreset {
effort: ReasoningEffort::Medium.into(),
description: "balanced".to_string(),
}],
supported_text_verbosity: &[TextVerbosity::Medium],
is_default: false,
upgrade: None,
pro_only: false,
show_in_picker: true,
};
let duplicate = ModelPreset {
id: "gpt-5.5".to_string(),
model: "gpt-5.5".to_string(),
..canonical.clone()
};
let curated = ChatWidget::curated_model_presets(vec![canonical, duplicate]);
let ids: Vec<&str> = curated.iter().map(|preset| preset.id.as_str()).collect();
assert_eq!(ids, vec!["alias/gpt-5.5"]);
}
#[test]
fn model_command_accepts_supported_non_curated_builtin_model() {
let _runtime_guard = enter_test_runtime_guard();