@@ -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