feat(models): add gpt-5.4-mini and simplify auth refresh

This commit is contained in:
James Peter
2026-03-24 13:23:14 +10:00
parent 0c1e81d101
commit 124314ebcf
3 changed files with 99 additions and 41 deletions
+31
View File
@@ -79,6 +79,36 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
pro_only: false,
show_in_picker: true,
},
ModelPreset {
id: "gpt-5.4-mini".to_string(),
model: "gpt-5.4-mini".to_string(),
display_name: "gpt-5.4-mini".to_string(),
description: "Smaller GPT-5.4 variant tuned for faster coding loops.".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: "Maximizes 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.3-codex".to_string(),
model: "gpt-5.3-codex".to_string(),
@@ -640,6 +670,7 @@ mod tests {
fn gpt_5_4_available_for_api_key_auth() {
let presets = builtin_model_presets(Some(AuthMode::ApiKey), false);
assert!(presets.iter().any(|preset| preset.id == "gpt-5.4"));
assert!(presets.iter().any(|preset| preset.id == "gpt-5.4-mini"));
}
#[test]
+32 -30
View File
@@ -188,43 +188,29 @@ impl CodexAuth {
pub async fn get_token_data(&self) -> Result<TokenData, std::io::Error> {
let auth_dot_json: Option<AuthDotJson> = self.get_current_auth_json();
match auth_dot_json {
Some(AuthDotJson {
tokens: Some(mut tokens),
last_refresh: Some(last_refresh),
..
}) => {
Some(auth_dot_json) => {
let mut tokens = auth_dot_json
.tokens
.clone()
.ok_or(std::io::Error::other("Token data is not available."))?;
if self.mode == AuthMode::ChatgptAuthTokens {
return Ok(tokens);
}
if last_refresh < Utc::now() - chrono::Duration::days(28) {
let refresh_response = tokio::time::timeout(
Duration::from_secs(60),
try_refresh_token(tokens.refresh_token.clone(), &self.client),
)
.await
.map_err(|_| {
std::io::Error::other("timed out while refreshing OpenAI API key")
})?
.map_err(|err| std::io::Error::other(err))?;
if should_proactively_refresh_auth(auth_dot_json.last_refresh) {
tokio::time::timeout(Duration::from_secs(60), self.refresh_token())
.await
.map_err(|_| {
std::io::Error::other(
"timed out while refreshing OpenAI API key",
)
})?
.map_err(std::io::Error::other)?;
let updated_auth_dot_json = update_tokens(
&self.auth_file,
refresh_response.id_token,
refresh_response.access_token,
refresh_response.refresh_token,
)
.await?;
tokens = updated_auth_dot_json
.tokens
.clone()
tokens = self
.get_current_token_data()
.ok_or(std::io::Error::other(
"Token data is not available after refresh.",
))?;
#[expect(clippy::unwrap_used)]
let mut auth_lock = self.auth_dot_json.lock().unwrap();
*auth_lock = Some(updated_auth_dot_json);
}
Ok(tokens)
@@ -346,6 +332,12 @@ impl CodexAuth {
}
}
fn should_proactively_refresh_auth(last_refresh: Option<DateTime<Utc>>) -> bool {
last_refresh.is_some_and(|last_refresh| {
last_refresh < Utc::now() - chrono::Duration::days(28)
})
}
pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
pub const CODEX_API_KEY_ENV_VAR: &str = "CODEX_API_KEY";
@@ -1307,6 +1299,16 @@ mod tests {
assert_eq!(updated.access_token, rotated_access);
}
#[test]
fn proactive_refresh_only_triggers_for_stale_chatgpt_auth() {
let fresh = Utc::now() - chrono::Duration::days(1);
let stale = Utc::now() - chrono::Duration::days(29);
assert!(!should_proactively_refresh_auth(Some(fresh)));
assert!(should_proactively_refresh_auth(Some(stale)));
assert!(!should_proactively_refresh_auth(None));
}
struct AuthFileParams {
openai_api_key: Option<String>,
chatgpt_plan_type: String,
@@ -922,30 +922,34 @@ impl ModelSelectionView {
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.3-codex") {
} else if model.eq_ignore_ascii_case("gpt-5.4-mini") {
1
} else if model.eq_ignore_ascii_case("gpt-5.3-codex-spark") {
} else if model.eq_ignore_ascii_case("gpt-5.3-codex") {
2
} else if model.eq_ignore_ascii_case("gpt-5.2-codex") {
} else if model.eq_ignore_ascii_case("gpt-5.3-codex-spark") {
3
} else if model.eq_ignore_ascii_case("gpt-5.2") {
} else if model.eq_ignore_ascii_case("gpt-5.2-codex") {
4
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-max") {
} else if model.eq_ignore_ascii_case("gpt-5.2") {
5
} else if model.eq_ignore_ascii_case("gpt-5.1-codex") {
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-max") {
6
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-mini") {
} else if model.eq_ignore_ascii_case("gpt-5.1-codex") {
7
} else if model.eq_ignore_ascii_case("gpt-5.1") {
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-mini") {
8
} else {
} else if model.eq_ignore_ascii_case("gpt-5.1") {
9
} else {
10
}
}
fn model_description(model: &str) -> Option<&'static str> {
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.")
} else if model.eq_ignore_ascii_case("gpt-5.3-codex") {
Some("Frontier agentic coding, 25% faster than previous models.")
} else if model.eq_ignore_ascii_case("gpt-5.3-codex-spark") {
@@ -1257,7 +1261,24 @@ mod tests {
},
];
let presets = vec![make_preset("gpt-5.3-codex"), make_preset("gpt-5.3-codex-spark"), gpt_5_4];
let mut gpt_5_4_mini = make_preset("gpt-5.4-mini");
gpt_5_4_mini.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 presets = vec![
make_preset("gpt-5.3-codex"),
make_preset("gpt-5.3-codex-spark"),
gpt_5_4_mini,
gpt_5_4,
];
let (tx, _rx) = mpsc::channel::<AppEvent>();
let view = ModelSelectionView::new(
presets,
@@ -1290,11 +1311,15 @@ mod tests {
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_3_idx);
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"));