refactor: use codex model list as source of truth

This commit is contained in:
Nathan Flurry 2026-02-08 11:48:47 -08:00
parent a97b15e19a
commit 98964f80ff

View file

@ -978,7 +978,6 @@ pub(crate) struct SessionManager {
struct ModelCatalogState { struct ModelCatalogState {
models: HashMap<AgentId, AgentModelsResponse>, models: HashMap<AgentId, AgentModelsResponse>,
in_flight: HashMap<AgentId, Arc<Notify>>, in_flight: HashMap<AgentId, Arc<Notify>>,
codex_unavailable_models: HashSet<String>,
} }
/// Shared Codex app-server process that handles multiple sessions via JSON-RPC. /// Shared Codex app-server process that handles multiple sessions via JSON-RPC.
@ -1928,18 +1927,6 @@ impl SessionManager {
Ok(()) Ok(())
} }
async fn mark_codex_model_unavailable(&self, model_id: &str) -> bool {
let mut catalog = self.model_catalog.lock().await;
let inserted = catalog
.codex_unavailable_models
.insert(model_id.to_string());
if inserted {
// Force a fresh fetch so provider/model lists drop unavailable models.
catalog.models.remove(&AgentId::Codex);
}
inserted
}
async fn clear_codex_session_model_if_unavailable( async fn clear_codex_session_model_if_unavailable(
&self, &self,
session_id: &str, session_id: &str,
@ -1960,6 +1947,11 @@ impl SessionManager {
false false
} }
async fn invalidate_codex_model_cache(&self) {
let mut catalog = self.model_catalog.lock().await;
catalog.models.remove(&AgentId::Codex);
}
async fn codex_native_session_id(&self, session_id: &str) -> Option<String> { async fn codex_native_session_id(&self, session_id: &str) -> Option<String> {
let sessions = self.sessions.lock().await; let sessions = self.sessions.lock().await;
let session = SessionManager::session_ref(&sessions, session_id)?; let session = SessionManager::session_ref(&sessions, session_id)?;
@ -1975,31 +1967,30 @@ impl SessionManager {
model_id: &str, model_id: &str,
native_session_id: Option<String>, native_session_id: Option<String>,
) { ) {
let newly_marked = self.mark_codex_model_unavailable(model_id).await; tracing::warn!(
if newly_marked { model_id = %model_id,
tracing::warn!( "codex model rejected at runtime; clearing session model and refreshing model cache"
model_id = %model_id, );
"codex model marked unavailable after runtime error" self.invalidate_codex_model_cache().await;
); if !self
}
if self
.clear_codex_session_model_if_unavailable(session_id, model_id) .clear_codex_session_model_if_unavailable(session_id, model_id)
.await .await
{ {
let native_session_id = match native_session_id { return;
Some(native_session_id) => Some(native_session_id),
None => self.codex_native_session_id(session_id).await,
};
let _ = self
.record_conversions(
session_id,
vec![codex_model_unavailable_status_event(
native_session_id,
model_id,
)],
)
.await;
} }
let native_session_id = match native_session_id {
Some(native_session_id) => Some(native_session_id),
None => self.codex_native_session_id(session_id).await,
};
let _ = self
.record_conversions(
session_id,
vec![codex_model_unavailable_status_event(
native_session_id,
model_id,
)],
)
.await;
} }
pub(crate) async fn delete_session(&self, session_id: &str) -> Result<(), SandboxError> { pub(crate) async fn delete_session(&self, session_id: &str) -> Result<(), SandboxError> {
@ -2096,10 +2087,7 @@ impl SessionManager {
Ok(response) if !response.models.is_empty() => Ok(response), Ok(response) if !response.models.is_empty() => Ok(response),
_ => Ok(claude_fallback_models()), _ => Ok(claude_fallback_models()),
}, },
AgentId::Codex => match self.fetch_codex_models().await { AgentId::Codex => self.fetch_codex_models().await,
Ok(response) if !response.models.is_empty() => Ok(response),
_ => Ok(codex_fallback_models()),
},
AgentId::Opencode => match self.fetch_opencode_models().await { AgentId::Opencode => match self.fetch_opencode_models().await {
Ok(models) => Ok(models), Ok(models) => Ok(models),
Err(_) => Ok(AgentModelsResponse { Err(_) => Ok(AgentModelsResponse {
@ -3887,22 +3875,6 @@ impl SessionManager {
let id = server.next_request_id(); let id = server.next_request_id();
let prompt_text = codex_prompt_for_mode(prompt, Some(&session.agent_mode)); let prompt_text = codex_prompt_for_mode(prompt, Some(&session.agent_mode));
let mut model = session.model.clone();
if let Some(model_id) = model.clone() {
let is_unavailable = {
let catalog = self.model_catalog.lock().await;
catalog.codex_unavailable_models.contains(&model_id)
};
if is_unavailable {
self.handle_codex_model_unavailable(
&session.session_id,
&model_id,
session.native_session_id.clone(),
)
.await;
model = None;
}
}
let params = codex_schema::TurnStartParams { let params = codex_schema::TurnStartParams {
approval_policy: codex_approval_policy(Some(&session.permission_mode)), approval_policy: codex_approval_policy(Some(&session.permission_mode)),
collaboration_mode: None, collaboration_mode: None,
@ -3912,7 +3884,7 @@ impl SessionManager {
text: prompt_text, text: prompt_text,
text_elements: Vec::new(), text_elements: Vec::new(),
}], }],
model, model: session.model.clone(),
output_schema: None, output_schema: None,
sandbox_policy: codex_sandbox_policy(Some(&session.permission_mode)), sandbox_policy: codex_sandbox_policy(Some(&session.permission_mode)),
summary: None, summary: None,
@ -4076,10 +4048,6 @@ impl SessionManager {
async fn fetch_codex_models(self: &Arc<Self>) -> Result<AgentModelsResponse, SandboxError> { async fn fetch_codex_models(self: &Arc<Self>) -> Result<AgentModelsResponse, SandboxError> {
let started = Instant::now(); let started = Instant::now();
let unavailable_models = {
let catalog = self.model_catalog.lock().await;
catalog.codex_unavailable_models.clone()
};
let server = self.ensure_codex_server().await?; let server = self.ensure_codex_server().await?;
tracing::info!( tracing::info!(
elapsed_ms = started.elapsed().as_millis() as u64, elapsed_ms = started.elapsed().as_millis() as u64,
@ -4170,9 +4138,6 @@ impl SessionManager {
let Some(model_id) = model_id else { let Some(model_id) = model_id else {
continue; continue;
}; };
if unavailable_models.contains(model_id) {
continue;
}
if !seen.insert(model_id.to_string()) { if !seen.insert(model_id.to_string()) {
continue; continue;
} }
@ -4235,12 +4200,6 @@ impl SessionManager {
} }
models.sort_by(|a, b| a.id.cmp(&b.id)); models.sort_by(|a, b| a.id.cmp(&b.id));
if default_model
.as_ref()
.is_some_and(|model_id| unavailable_models.contains(model_id))
{
default_model = None;
}
if default_model.is_none() { if default_model.is_none() {
default_model = models.first().map(|model| model.id.clone()); default_model = models.first().map(|model| model.id.clone());
} }
@ -5518,22 +5477,6 @@ fn mock_models_response() -> AgentModelsResponse {
} }
} }
fn codex_fallback_models() -> AgentModelsResponse {
let models = ["gpt-4o", "o3", "o4-mini"]
.into_iter()
.map(|id| AgentModelInfo {
id: id.to_string(),
name: None,
variants: Some(codex_variants()),
default_variant: Some("medium".to_string()),
})
.collect();
AgentModelsResponse {
models,
default_model: Some("gpt-4o".to_string()),
}
}
fn should_cache_agent_models(agent: AgentId, response: &AgentModelsResponse) -> bool { fn should_cache_agent_models(agent: AgentId, response: &AgentModelsResponse) -> bool {
if agent == AgentId::Opencode && response.models.is_empty() { if agent == AgentId::Opencode && response.models.is_empty() {
return false; return false;
@ -5954,7 +5897,6 @@ mod tests {
Some("gpt-5.3-codex-NOTREAL".to_string()) Some("gpt-5.3-codex-NOTREAL".to_string())
); );
} }
} }
fn claude_input_session_id(session: &SessionSnapshot) -> String { fn claude_input_session_id(session: &SessionSnapshot) -> String {