Skip to content

Commit b43d30e

Browse files
committed
feat: user-configured LLM_MODEL takes priority over auto-detection
Fetch the full model list from /models endpoint. If LLM_MODEL is set, validate it against the supported list and warn with available models if not found. If LLM_MODEL is not set, auto-detect the highest-priority model. Also bumps client_version to 1.0.0 to unlock gpt-5.3/5.4.
1 parent 2682f75 commit b43d30e

1 file changed

Lines changed: 65 additions & 25 deletions

File tree

src/llm/codex_chatgpt.rs

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,56 @@ impl CodexChatGptProvider {
3535
}
3636
}
3737

38-
/// Create a provider, auto-detecting the default model from the `/models` endpoint.
38+
/// Create a provider, resolving the model to use.
39+
///
40+
/// **Model selection priority:**
41+
/// 1. If `configured_model` is non-empty, validate it against the
42+
/// `/models` endpoint. If it isn't in the supported list, log a
43+
/// warning with available models and fall back to the top model.
44+
/// 2. If `configured_model` is empty (or a generic placeholder like
45+
/// "default"), auto-detect the highest-priority model from the API.
3946
///
4047
/// A single `reqwest::Client` is created and reused for both the model
4148
/// discovery request and all subsequent LLM calls.
42-
///
43-
/// If model discovery fails or the configured model is already Codex-specific,
44-
/// falls back to `fallback_model`.
4549
pub async fn with_auto_model(
4650
base_url: &str,
4751
api_key: &str,
48-
fallback_model: &str,
52+
configured_model: &str,
4953
) -> Self {
5054
let base = base_url.trim_end_matches('/');
5155
let client = Client::new();
52-
let model = Self::fetch_default_model(&client, base, api_key)
53-
.await
54-
.unwrap_or_else(|| fallback_model.to_string());
55-
tracing::info!(model = %model, "Codex ChatGPT: resolved model");
56+
let available = Self::fetch_available_models(&client, base, api_key).await;
57+
58+
let model = if !configured_model.is_empty() && configured_model != "default" {
59+
// User explicitly configured a model — validate it
60+
if available.is_empty() {
61+
// Could not reach the /models endpoint; trust the user's choice
62+
tracing::warn!(
63+
"Could not fetch model list; using configured model '{configured_model}'"
64+
);
65+
configured_model.to_string()
66+
} else if available.iter().any(|m| m == configured_model) {
67+
tracing::info!(model = %configured_model, "Codex ChatGPT: using configured model");
68+
configured_model.to_string()
69+
} else {
70+
tracing::warn!(
71+
configured = %configured_model,
72+
available = ?available,
73+
"Configured model not found in supported list, falling back to top model"
74+
);
75+
available.into_iter().next().unwrap_or_else(|| configured_model.to_string())
76+
}
77+
} else {
78+
// No user preference — auto-detect
79+
if let Some(top) = available.into_iter().next() {
80+
tracing::info!(model = %top, "Codex ChatGPT: auto-detected model");
81+
top
82+
} else {
83+
tracing::warn!("Could not auto-detect model, using fallback '{configured_model}'");
84+
configured_model.to_string()
85+
}
86+
};
87+
5688
Self {
5789
client,
5890
base_url: base.to_string(),
@@ -61,27 +93,35 @@ impl CodexChatGptProvider {
6193
}
6294
}
6395

64-
/// Query `/models?client_version=1.0.0` and return the default model slug.
65-
async fn fetch_default_model(client: &Client, base_url: &str, api_key: &str) -> Option<String> {
96+
/// Query `/models?client_version=1.0.0` and return the list of available
97+
/// model slugs, ordered by priority (highest first).
98+
async fn fetch_available_models(client: &Client, base_url: &str, api_key: &str) -> Vec<String> {
6699
let url = format!("{base_url}/models?client_version=1.0.0");
67-
let resp = client
68-
.get(&url)
69-
.bearer_auth(api_key)
70-
.send()
71-
.await
72-
.ok()?;
100+
let resp = match client.get(&url).bearer_auth(api_key).send().await {
101+
Ok(r) => r,
102+
Err(e) => {
103+
tracing::warn!("Failed to fetch Codex models: {e}");
104+
return Vec::new();
105+
}
106+
};
73107
if !resp.status().is_success() {
74108
tracing::warn!(status = %resp.status(), "Failed to fetch Codex models");
75-
return None;
109+
return Vec::new();
76110
}
77-
let body: Value = resp.json().await.ok()?;
111+
let body: Value = match resp.json().await {
112+
Ok(v) => v,
113+
Err(_) => return Vec::new(),
114+
};
78115
// The response has { "models": [ { "slug": "...", ... }, ... ] }
79-
let models = body.get("models")?.as_array()?;
80-
// Find the model with highest priority or is_default
81-
models
82-
.iter()
83-
.filter_map(|m| m.get("slug").and_then(|s| s.as_str()).map(|s| s.to_string()))
84-
.next()
116+
body.get("models")
117+
.and_then(|m| m.as_array())
118+
.map(|models| {
119+
models
120+
.iter()
121+
.filter_map(|m| m.get("slug").and_then(|s| s.as_str()).map(|s| s.to_string()))
122+
.collect()
123+
})
124+
.unwrap_or_default()
85125
}
86126

87127
/// Convert IronClaw messages to Responses API request JSON.

0 commit comments

Comments
 (0)