@@ -633,6 +633,12 @@ pub struct AzureSttSettings {
633633pub struct UiSettings {
634634 /// BCP-47 language tag. Unsupported tags fall back in the UI layer.
635635 pub language : Option < String > ,
636+ /// Model chosen in the panel quick-pick, as `provider/model`.
637+ ///
638+ /// Kept separate from `[codex].model`: the picker covers every configured
639+ /// provider, including user-named Azure deployments.
640+ #[ serde( default ) ]
641+ pub selected_model : Option < String > ,
636642 /// Let aibo switch on an application's accessibility tree so its content
637643 /// can be read (§8, macOS only).
638644 ///
@@ -696,6 +702,7 @@ impl Default for UiSettings {
696702 fn default ( ) -> Self {
697703 Self {
698704 language : None ,
705+ selected_model : None ,
699706 allow_ax_tree_activation : false ,
700707 panel_hotkey : None ,
701708 screen_capture_hotkey : None ,
@@ -1043,7 +1050,7 @@ impl Config {
10431050 registry : & ProviderRegistry ,
10441051 codex_authenticated : bool ,
10451052 ) -> Result < RoleBindings , ConfigError > {
1046- if self . roles . is_empty ( ) {
1053+ let mut bindings = if self . roles . is_empty ( ) {
10471054 // §4's shipped table, filtered to what is actually usable. A chain
10481055 // whose primary is a provider the user never configured spends its
10491056 // first request discovering that.
@@ -1055,36 +1062,87 @@ impl Config {
10551062 if codex_authenticated {
10561063 self . retarget_codex_model ( & mut seeded) ?;
10571064 }
1058- return Ok ( seeded) ;
1059- }
1060-
1061- let mut chains = Vec :: new ( ) ;
1062- for ( name , role_config ) in & self . roles {
1063- let role = parse_role ( name ) ? ;
1064- let entries : Vec < ModelBinding > = role_config
1065- . entries
1066- . iter ( )
1067- . map ( |e| ModelBinding {
1068- provider : ProviderId :: new ( e . provider . clone ( ) ) ,
1069- model : e . model . clone ( ) ,
1070- } )
1071- . collect ( ) ;
1072- // Same pre-dispatch refusal as the seeded path, applied to a chain
1073- // the user typed. §4's no-fallback-on-400 rule makes a rejected
1074- // Codex id a dead end wherever it came from.
1075- for binding in & entries {
1076- if binding . provider == ProviderId :: CODEX {
1077- check_codex_model ( & binding . model ) ? ;
1065+ seeded
1066+ } else {
1067+ let mut chains = Vec :: new ( ) ;
1068+ for ( name , role_config ) in & self . roles {
1069+ let role = parse_role ( name ) ? ;
1070+ let entries : Vec < ModelBinding > = role_config
1071+ . entries
1072+ . iter ( )
1073+ . map ( |e| ModelBinding {
1074+ provider : ProviderId :: new ( e . provider . clone ( ) ) ,
1075+ model : e . model . clone ( ) ,
1076+ } )
1077+ . collect ( ) ;
1078+ // Same pre-dispatch refusal as the seeded path, applied to a chain
1079+ // the user typed. §4's no-fallback-on-400 rule makes a rejected
1080+ // Codex id a dead end wherever it came from.
1081+ for binding in & entries {
1082+ if binding. provider == ProviderId :: CODEX {
1083+ check_codex_model ( & binding . model ) ? ;
1084+ }
10781085 }
1086+ chains. push ( RoleChain {
1087+ role,
1088+ entries,
1089+ fallback_enabled : role_config. fallback ,
1090+ allow_crossing_trust_boundary : role_config. allow_crossing_trust_boundary ,
1091+ } ) ;
10791092 }
1080- chains. push ( RoleChain {
1093+ RoleBindings :: from_chains ( chains) ?
1094+ } ;
1095+ self . apply_selected_model ( & mut bindings, registry) ?;
1096+ Ok ( bindings)
1097+ }
1098+
1099+ /// Put the quick-pick's explicit choice at the front of the roles that
1100+ /// represent a user-selected general model. The provider must still exist
1101+ /// in the built registry; a stale preference after provider removal is
1102+ /// ignored rather than breaking startup.
1103+ fn apply_selected_model (
1104+ & self ,
1105+ bindings : & mut RoleBindings ,
1106+ registry : & ProviderRegistry ,
1107+ ) -> Result < ( ) , ConfigError > {
1108+ let Some ( raw) = self . ui . selected_model . as_deref ( ) else {
1109+ return Ok ( ( ) ) ;
1110+ } ;
1111+ let Some ( ( provider, model) ) = raw. split_once ( '/' ) else {
1112+ tracing:: warn!( selected_model = raw, "ignoring malformed selected model" ) ;
1113+ return Ok ( ( ) ) ;
1114+ } ;
1115+ if provider. is_empty ( ) || model. is_empty ( ) {
1116+ tracing:: warn!( selected_model = raw, "ignoring malformed selected model" ) ;
1117+ return Ok ( ( ) ) ;
1118+ }
1119+ let selected = ModelBinding {
1120+ provider : ProviderId :: new ( provider. to_owned ( ) ) ,
1121+ model : model. to_owned ( ) ,
1122+ } ;
1123+ if registry. get ( & selected. provider ) . is_none ( ) {
1124+ tracing:: warn!(
1125+ selected_model = raw,
1126+ "selected model provider is not configured"
1127+ ) ;
1128+ return Ok ( ( ) ) ;
1129+ }
1130+ if selected. provider == ProviderId :: CODEX {
1131+ check_codex_model ( & selected. model ) ?;
1132+ }
1133+
1134+ for role in [ Role :: Smart , Role :: Vision , Role :: Agent ] {
1135+ let mut chain = bindings. chain ( role) . cloned ( ) . unwrap_or ( RoleChain {
10811136 role,
1082- entries,
1083- fallback_enabled : role_config . fallback ,
1084- allow_crossing_trust_boundary : role_config . allow_crossing_trust_boundary ,
1137+ entries : Vec :: new ( ) ,
1138+ fallback_enabled : false ,
1139+ allow_crossing_trust_boundary : false ,
10851140 } ) ;
1141+ chain. entries . retain ( |binding| binding != & selected) ;
1142+ chain. entries . insert ( 0 , selected. clone ( ) ) ;
1143+ bindings. set_chain ( chain) ?;
10861144 }
1087- Ok ( RoleBindings :: from_chains ( chains ) ? )
1145+ Ok ( ( ) )
10881146 }
10891147
10901148 /// Point every seeded Codex entry at the model the user actually chose.
@@ -1318,6 +1376,34 @@ mod tests {
13181376 ) ;
13191377 }
13201378
1379+ #[ test]
1380+ fn a_selected_azure_deployment_becomes_the_general_model ( ) {
1381+ let config = Config :: from_toml_str (
1382+ r#"
1383+ [ui]
1384+ selected_model = "azure-openai/team-gpt"
1385+
1386+ [[providers]]
1387+ backend = "azure"
1388+ base_url = "https://team.services.ai.azure.com"
1389+ models = ["team-gpt"]
1390+ "# ,
1391+ )
1392+ . unwrap ( ) ;
1393+
1394+ let ( _, engine) = config. build ( & FixedKey , PriceTable :: empty ( ) ) . unwrap ( ) ;
1395+ for role in [ Role :: Smart , Role :: Vision , Role :: Agent ] {
1396+ assert_eq ! (
1397+ engine. bindings. primary( role) ,
1398+ Some ( & ModelBinding {
1399+ provider: ProviderId :: AZURE_OPENAI ,
1400+ model: "team-gpt" . to_owned( ) ,
1401+ } ) ,
1402+ "{role:?} must use the explicit quick-pick choice"
1403+ ) ;
1404+ }
1405+ }
1406+
13211407 #[ test]
13221408 fn the_fast_never_codex_rule_is_enforced_on_a_user_config ( ) {
13231409 let config = Config :: from_toml_str (
0 commit comments