use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use super::AnyError;
use crate::clients::RouterModel;
pub(super) fn write_codex_model_catalog(
root: &Path,
models: &[RouterModel],
configured_effort: Option<&str>,
selected_model: Option<&str>,
) -> Result<PathBuf, AnyError> {
if models.is_empty() {
return Err("the Router advertised no models for Codex".into());
}
let mut described = Vec::with_capacity(models.len());
let mut omitted = Vec::new();
for model in models {
match validate_codex_reasoning_metadata(model) {
Ok(()) => described.push(model),
Err(error) if selected_model == Some(model.id.as_str()) => return Err(error),
Err(error) => omitted.push((model.id.as_str(), error.to_string())),
}
}
if !omitted.is_empty() {
let ids = omitted
.iter()
.map(|(id, _)| *id)
.collect::<Vec<_>>()
.join(", ");
eprintln!(
"warning: omitted Codex model(s) with unavailable reasoning metadata: {ids}; \
fully described models remain available"
);
}
if described.is_empty() {
let ids = omitted
.iter()
.map(|(id, _)| *id)
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"the live Codex catalog has no model with usable reasoning metadata; omitted: {ids}"
)
.into());
}
if let (Some(effort), Some(selected)) = (configured_effort, selected_model) {
let model = described
.iter()
.copied()
.find(|model| model.id == selected)
.ok_or_else(|| format!("the Router advertised no Codex model named `{selected}`"))?;
if !model_supports_reasoning_effort(model, effort) {
return Err(format!(
"Codex model `{selected}` does not support configured reasoning effort \
`{effort}`; choose a supported model or change `model_reasoning_effort`"
)
.into());
}
}
let compatible = described
.into_iter()
.filter(|model| {
configured_effort.is_none_or(|effort| model_supports_reasoning_effort(model, effort))
})
.collect::<Vec<_>>();
if compatible.is_empty() {
let effort = configured_effort.expect("an unfiltered non-empty catalog stays non-empty");
return Err(format!(
"the live Codex catalog has no model supporting configured reasoning effort \
`{effort}`; change `model_reasoning_effort` and retry"
)
.into());
}
let entries = compatible
.iter()
.enumerate()
.map(|(index, model)| {
let supported = model
.supported_reasoning_levels
.as_ref()
.expect("metadata was validated before projection");
crate::codex_catalog::model_info(
&crate::codex_catalog::ModelDescription {
id: &model.id,
display_name: &model.id,
owner: &model.owned_by,
default_reasoning_level: model
.default_reasoning_level
.clone()
.map(Value::String),
supported_reasoning_levels: serde_json::to_value(supported)
.expect("reasoning metadata serializes"),
},
index,
)
})
.collect::<Vec<_>>();
let path = root.join("router-codex-models.json");
let rendered = format!(
"{}\n",
serde_json::to_string_pretty(&json!({"models": entries}))?
);
crate::durable_file::atomic_write_owner_only(&path, rendered.as_bytes())?;
Ok(path)
}
fn validate_codex_reasoning_metadata(model: &RouterModel) -> Result<(), AnyError> {
let supported = model.supported_reasoning_levels.as_ref().ok_or_else(|| {
format!(
"the live Codex catalog omitted reasoning metadata for model `{}`; refusing to \
launch because model selection could silently replace the user's configured \
reasoning effort",
model.id
)
})?;
if let Some(default) = model.default_reasoning_level.as_deref()
&& !supported.iter().any(|level| level.effort == default)
{
return Err(format!(
"the live Codex catalog reports unsupported default reasoning level `{default}` \
for model `{}`",
model.id
)
.into());
}
Ok(())
}
fn model_supports_reasoning_effort(model: &RouterModel, effort: &str) -> bool {
model
.supported_reasoning_levels
.as_ref()
.is_some_and(|levels| levels.iter().any(|level| level.effort == effort))
}