@@ -382,6 +382,22 @@ fn sync_all_session_providers(home: &Path) -> codexmate_data::ProviderSyncResult
382382 codexmate_data:: run_provider_sync ( Some ( home) )
383383}
384384
385+ fn prepare_direct_mode_state ( home : & Path ) -> anyhow:: Result < codexmate_data:: ProviderSyncResult > {
386+ let config_path = home. join ( "config.toml" ) ;
387+ let existing = std:: fs:: read_to_string ( & config_path) . unwrap_or_default ( ) ;
388+ let mut doc = codexmate_core:: relay_config:: parse_toml_document ( & existing) ?;
389+ codexmate_core:: relay_config:: set_openai_model_provider_for_direct_mode ( & mut doc) ;
390+ let content = codexmate_core:: relay_config:: ensure_trailing_newline ( doc. to_string ( ) ) ;
391+ std:: fs:: create_dir_all ( home) ?;
392+ std:: fs:: write ( & config_path, content. as_bytes ( ) ) ?;
393+
394+ let sync = sync_all_session_providers ( home) ;
395+ if sync. status != codexmate_data:: ProviderSyncStatus :: Synced {
396+ anyhow:: bail!( "{}" , sync. message) ;
397+ }
398+ Ok ( sync)
399+ }
400+
385401#[ allow( dead_code) ]
386402fn strip_model_provider_from_toml ( contents : & str ) -> String {
387403 let Ok ( mut doc) = codexmate_core:: relay_config:: parse_toml_document ( contents) else {
@@ -823,6 +839,14 @@ pub fn get_codex_mode() -> CommandResult<Value> {
823839 let mode = std:: fs:: read_to_string ( & config_path)
824840 . map ( |contents| codex_mode_from_config ( & contents) )
825841 . unwrap_or ( "direct" ) ;
842+ if mode == "direct" {
843+ if let Err ( error) = prepare_direct_mode_state ( & home) {
844+ return failed (
845+ & format ! ( "直连模式配置修复失败:{error}" ) ,
846+ json ! ( { "mode" : "direct" } ) ,
847+ ) ;
848+ }
849+ }
826850 CommandResult {
827851 status : "ok" . to_string ( ) ,
828852 message : format ! (
@@ -914,10 +938,23 @@ pub fn restart_codex(mode: String, request: LaunchRequest) -> CommandResult<Valu
914938 codexmate_core:: watcher:: stop_codex_processes ( ) ;
915939 codexmate_core:: watcher:: stop_launcher_processes ( ) ;
916940 std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 800 ) ) ;
941+ let home = codexmate_core:: relay_config:: default_codex_home_dir ( ) ;
917942
918943 if mode == "direct" {
944+ if let Err ( error) = prepare_direct_mode_state ( & home) {
945+ return failed (
946+ & format ! ( "直连模式配置修复失败:{error}" ) ,
947+ json ! ( { "mode" : "direct" } ) ,
948+ ) ;
949+ }
919950 let app_path = if request. app_path . trim ( ) . is_empty ( ) {
920- "/Applications/Codex.app" . to_string ( )
951+ let settings = SettingsStore :: default ( ) . load ( ) . unwrap_or_default ( ) ;
952+ codexmate_core:: app_paths:: resolve_codex_app_dir_with_saved (
953+ None ,
954+ Some ( settings. codex_app_path . as_str ( ) ) ,
955+ )
956+ . map ( |path| path. to_string_lossy ( ) . to_string ( ) )
957+ . unwrap_or_else ( || "/Applications/ChatGPT.app" . to_string ( ) )
921958 } else {
922959 request. app_path . trim ( ) . to_string ( )
923960 } ;
@@ -1268,7 +1305,9 @@ base_url = "http://127.0.0.1:57321/v1"
12681305 assert ! ( direct. contains( "model_provider = \" openai\" " ) ) ;
12691306 assert ! ( direct. contains( "[model_providers.custom]" ) ) ;
12701307 assert ! ( direct. contains( "base_url = \" http://127.0.0.1:57321/v1\" " ) ) ;
1271- assert ! ( direct. contains( "model = \" deepseek-v4-flash:deepseek-v4-flash\" " ) ) ;
1308+ // 直连模式必须移除 "provider:model" 格式的 model 值,否则新版 Codex 报
1309+ // "Model provider 'xxx' not found"
1310+ assert ! ( !direct. contains( "model = \" deepseek-v4-flash:deepseek-v4-flash\" " ) ) ;
12721311 }
12731312
12741313 #[ test]
@@ -1303,6 +1342,39 @@ base_url = "http://127.0.0.1:57321/v1"
13031342 assert_eq ! ( codex_mode_from_config( config) , "proxy" ) ;
13041343 }
13051344
1345+ #[ test]
1346+ fn direct_mode_preparation_repairs_historical_provider ( ) {
1347+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
1348+ std:: fs:: write (
1349+ temp. path ( ) . join ( "config.toml" ) ,
1350+ "model_provider = \" openai\" \n " ,
1351+ )
1352+ . unwrap ( ) ;
1353+ let db = rusqlite:: Connection :: open ( temp. path ( ) . join ( "state_5.sqlite" ) ) . unwrap ( ) ;
1354+ db. execute (
1355+ "CREATE TABLE threads (id TEXT PRIMARY KEY, model_provider TEXT)" ,
1356+ [ ] ,
1357+ )
1358+ . unwrap ( ) ;
1359+ db. execute ( "INSERT INTO threads VALUES ('thread-1', 'custom')" , [ ] )
1360+ . unwrap ( ) ;
1361+ drop ( db) ;
1362+
1363+ let sync = prepare_direct_mode_state ( temp. path ( ) ) . unwrap ( ) ;
1364+
1365+ assert_eq ! ( sync. status, codexmate_data:: ProviderSyncStatus :: Synced ) ;
1366+ assert_eq ! ( sync. sqlite_rows_updated, 1 ) ;
1367+ let db = rusqlite:: Connection :: open ( temp. path ( ) . join ( "state_5.sqlite" ) ) . unwrap ( ) ;
1368+ let provider: String = db
1369+ . query_row (
1370+ "SELECT model_provider FROM threads WHERE id = 'thread-1'" ,
1371+ [ ] ,
1372+ |row| row. get ( 0 ) ,
1373+ )
1374+ . unwrap ( ) ;
1375+ assert_eq ! ( provider, "openai" ) ;
1376+ }
1377+
13061378 #[ test]
13071379 fn sync_all_session_providers_updates_rollouts_at_session_root ( ) {
13081380 let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments