diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index 55386ca5..d122d338 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -382,14 +382,16 @@ pub fn write_auth_json(auth_file: &Path, auth_dot_json: &AuthDotJson) -> std::io async fn update_tokens( auth_file: &Path, - id_token: String, + id_token: Option, access_token: Option, refresh_token: Option, ) -> std::io::Result { let mut auth_dot_json = try_read_auth_json(auth_file)?; let tokens = auth_dot_json.tokens.get_or_insert_with(TokenData::default); - tokens.id_token = parse_id_token(&id_token).map_err(std::io::Error::other)?; + if let Some(id_token) = id_token { + tokens.id_token = parse_id_token(&id_token).map_err(std::io::Error::other)?; + } if let Some(access_token) = access_token { tokens.access_token = access_token; } @@ -445,7 +447,7 @@ struct RefreshRequest { #[derive(Deserialize, Clone)] struct RefreshResponse { - id_token: String, + id_token: Option, access_token: Option, refresh_token: Option, } @@ -511,6 +513,35 @@ mod tests { assert_eq!(auth_dot_json, same_auth_dot_json); } + #[tokio::test] + async fn refresh_without_id_token() { + let codex_home = tempdir().unwrap(); + let fake_jwt = write_auth_file( + AuthFileParams { + openai_api_key: None, + chatgpt_plan_type: "pro".to_string(), + chatgpt_account_id: None, + }, + codex_home.path(), + ) + .expect("failed to write auth file"); + + let auth_file = super::get_auth_file(codex_home.path()); + let updated = super::update_tokens( + auth_file.as_path(), + None, + Some("new-access-token".to_string()), + Some("new-refresh-token".to_string()), + ) + .await + .expect("update_tokens should succeed"); + + let tokens = updated.tokens.expect("tokens should exist"); + assert_eq!(tokens.id_token.raw_jwt, fake_jwt); + assert_eq!(tokens.access_token, "new-access-token"); + assert_eq!(tokens.refresh_token, "new-refresh-token"); + } + #[test] fn login_with_api_key_overwrites_existing_auth_json() { let dir = tempdir().unwrap(); diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index de10d640..cc40ef42 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -389,9 +389,14 @@ impl ModelClient { if status == StatusCode::UNAUTHORIZED && let Some(manager) = auth_manager.as_ref() - && manager.auth().is_some() + && let Some(auth) = auth.as_ref() + && auth.mode == AuthMode::ChatGPT { - let _ = manager.refresh_token().await; + manager.refresh_token().await.map_err(|err| { + StreamAttemptError::Fatal(CodexErr::Fatal(format!( + "Failed to refresh ChatGPT credentials: {err}" + ))) + })?; } // The OpenAI Responses endpoint returns structured JSON bodies even for 4xx/5xx diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 65dad461..f811211c 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1911,7 +1911,7 @@ async fn run_turn( // at a seemingly frozen screen. sess.notify_stream_error( turn_context.as_ref(), - format!("Re-connecting... {retries}/{max_retries}"), + format!("Reconnecting... {retries}/{max_retries}"), ) .await; diff --git a/codex-rs/core/src/codex/compact.rs b/codex-rs/core/src/codex/compact.rs index 236b0241..dc7a6aa5 100644 --- a/codex-rs/core/src/codex/compact.rs +++ b/codex-rs/core/src/codex/compact.rs @@ -134,7 +134,7 @@ async fn run_compact_task_inner( let delay = backoff(retries); sess.notify_stream_error( turn_context.as_ref(), - format!("Re-connecting... {retries}/{max_retries}"), + format!("Reconnecting... {retries}/{max_retries}"), ) .await; tokio::time::sleep(delay).await; diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 75796d7d..a9c1d58c 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -2325,7 +2325,7 @@ fn plan_update_renders_history_cell() { fn stream_error_updates_status_indicator() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); chat.bottom_pane.set_task_running(true); - let msg = "Re-connecting... 2/5"; + let msg = "Reconnecting... 2/5"; chat.handle_codex_event(Event { id: "sub-1".into(), msg: EventMsg::StreamError(StreamErrorEvent {