diff --git a/codex-rs/app-server-client/src/cloud_backend/auth.rs b/codex-rs/app-server-client/src/cloud_backend/auth.rs index d637bbb755..bd0f293d04 100644 --- a/codex-rs/app-server-client/src/cloud_backend/auth.rs +++ b/codex-rs/app-server-client/src/cloud_backend/auth.rs @@ -41,6 +41,7 @@ pub async fn connect(config: &Config) -> Result { Ok(start_worker( client, protocol::GetAccountResponse { + user_id: auth.get_chatgpt_user_id(), account: Some(protocol::Account::Chatgpt { email: auth.get_account_email(), plan_type: auth.account_plan_type().unwrap_or(PlanType::Unknown), diff --git a/codex-rs/app-server-client/src/cloud_backend/submission_tests.rs b/codex-rs/app-server-client/src/cloud_backend/submission_tests.rs index f23cad2274..2f6e225727 100644 --- a/codex-rs/app-server-client/src/cloud_backend/submission_tests.rs +++ b/codex-rs/app-server-client/src/cloud_backend/submission_tests.rs @@ -38,6 +38,7 @@ async fn native_submission_uses_one_admission_and_keeps_preflight_guards() -> Re cloud::Credentials::new("synthetic-bearer", "synthetic-account")?, )?; let account = protocol::GetAccountResponse { + user_id: None, account: None, requires_openai_auth: true, workspace_routing: None, diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 9927db6e72..d47c1ff152 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -1477,6 +1477,7 @@ mod tests { JSONRPCMessage::Response(JSONRPCResponse { id: request.id, result: serde_json::to_value(GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: false, @@ -1533,6 +1534,7 @@ mod tests { JSONRPCMessage::Response(JSONRPCResponse { id: request.id, result: serde_json::to_value(GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: false, @@ -1611,6 +1613,7 @@ mod tests { assert_eq!( response, GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: false, @@ -1717,6 +1720,7 @@ mod tests { JSONRPCMessage::Response(JSONRPCResponse { id: request.id, result: serde_json::to_value(GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: false, @@ -1771,6 +1775,7 @@ mod tests { assert_eq!( first_response, GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: false, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index be423033a4..aeaff44114 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -13383,6 +13383,14 @@ }, "requiresOpenaiAuth": { "type": "boolean" + }, + "userId": { + "default": null, + "description": "ChatGPT user ID belonging to this account response, when available.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index a9774bde21..fbc7f8b5d4 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -9275,6 +9275,14 @@ }, "requiresOpenaiAuth": { "type": "boolean" + }, + "userId": { + "default": null, + "description": "ChatGPT user ID belonging to this account response, when available.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/GetAccountResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GetAccountResponse.json index 2c5d632ee3..fe91f91b70 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/GetAccountResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/GetAccountResponse.json @@ -159,6 +159,14 @@ }, "requiresOpenaiAuth": { "type": "boolean" + }, + "userId": { + "default": null, + "description": "ChatGPT user ID belonging to this account response, when available.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 755c576618..2317f2c209 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 3ac34b7fae..c35c1e5ff0 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GetAccountResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GetAccountResponse.ts index e88cc6abe3..dd131a0da2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/GetAccountResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GetAccountResponse.ts @@ -3,4 +3,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Account } from "./Account"; -export type GetAccountResponse = {account: Account | null, requiresOpenaiAuth: boolean}; +export type GetAccountResponse = {account: Account | null, /** + * ChatGPT user ID belonging to this account response, when available. + */ +userId: string | null, requiresOpenaiAuth: boolean}; diff --git a/codex-rs/app-server-protocol/src/protocol/common_tests.rs b/codex-rs/app-server-protocol/src/protocol/common_tests.rs index 64afbeaa15..300a594857 100644 --- a/codex-rs/app-server-protocol/src/protocol/common_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/common_tests.rs @@ -55,12 +55,14 @@ fn client_response_jsonrpc_parts_preserve_payloads_and_request_ids() -> Result<( ), ( ClientResponsePayload::GetAccount(v2::GetAccountResponse { + user_id: None, account: Some(v2::Account::ApiKey {}), requires_openai_auth: false, workspace_routing: None, }), json!({ "account": { "type": "apiKey" }, + "userId": null, "requiresOpenaiAuth": false, "workspaceRouting": null, }), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index de60aa91b6..b01d01641e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -586,6 +586,9 @@ pub struct GetAccountParams { #[ts(export_to = "v2/")] pub struct GetAccountResponse { pub account: Option, + /// ChatGPT user ID belonging to this account response, when available. + #[serde(default)] + pub user_id: Option, pub requires_openai_auth: bool, #[experimental("account/read.workspaceRouting")] pub workspace_routing: Option, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index ef6ad65b2a..b36a9d2a9d 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -576,6 +576,15 @@ cancels the previous attempt and waits for its callback listener to close. Use `account/login/cancel` to cancel explicitly. Start a fresh session after success to use the saved grant. +# Account identity + +`account/read` returns a nullable `userId` alongside `account`. For ChatGPT and +personal access token sessions, this is the canonical ChatGPT user ID from the +authenticated account metadata, when available. API-only and signed-out sessions +return `null`. The ID belongs to the same account snapshot as the rest of the +response. Clients should also accept an absent field from older app servers. +PAT sessions continue to return no bearer token from `getAuthStatus`. + # Selected workspace routing The experimental `account/read.workspaceRouting` response field returns the selected ChatGPT workspace's `chatgptAccountId`, resolved HTTPS `backendOrigin`, and backend-provided `accountRoutingOverride`. The routing value is `us`, `us_cr`, or the explicit `NO_CONSTRAINT` value. API-only and signed-out accounts return `null` and do not need `accounts/check`. diff --git a/codex-rs/app-server/src/message_processor_workspace_routing_tests.rs b/codex-rs/app-server/src/message_processor_workspace_routing_tests.rs index 6452bb14e4..179b79899b 100644 --- a/codex-rs/app-server/src/message_processor_workspace_routing_tests.rs +++ b/codex-rs/app-server/src/message_processor_workspace_routing_tests.rs @@ -141,6 +141,7 @@ async fn background_routing_call_sites_skip_config(api_key: Option<&str>) -> any assert_eq!( response, GetAccountResponse { + user_id: None, account: api_key.map(|_| codex_app_server_protocol::Account::ApiKey {}), requires_openai_auth: true, workspace_routing: None, diff --git a/codex-rs/app-server/src/request_processors/account_processor/workspace_routing.rs b/codex-rs/app-server/src/request_processors/account_processor/workspace_routing.rs index e768c8cec4..8f381bc5fa 100644 --- a/codex-rs/app-server/src/request_processors/account_processor/workspace_routing.rs +++ b/codex-rs/app-server/src/request_processors/account_processor/workspace_routing.rs @@ -49,6 +49,7 @@ pub(super) struct CachedWorkspaceRouting { pub(super) struct AccountRead { account_state: ProviderAccountState, + user_id: Option, pub(super) workspace_routing: Option, workspace_name: Option, } @@ -274,6 +275,7 @@ impl AccountRequestProcessor { Ok(Some( GetAccountResponse { account, + user_id: read.user_id, requires_openai_auth: read.account_state.requires_openai_auth, workspace_routing: read.workspace_routing.map(|routing| { codex_app_server_protocol::WorkspaceRouting { @@ -340,6 +342,15 @@ impl AccountRequestProcessor { let account_state = provider .account_state() .map_err(AccountReadError::InvalidAccount)?; + let user_id = auth + .as_ref() + .filter(|_| { + matches!( + &account_state.account, + Some(ProviderAccount::Chatgpt { .. }) + ) + }) + .and_then(CodexAuth::get_chatgpt_user_id); let mut workspace_routing = if let Some((auth, account_id)) = auth .as_ref() .filter(|auth| { @@ -545,6 +556,7 @@ impl AccountRequestProcessor { .unwrap_or_default(); Ok(AccountRead { account_state, + user_id, workspace_routing, workspace_name, }) diff --git a/codex-rs/app-server/tests/suite/auth.rs b/codex-rs/app-server/tests/suite/auth.rs index 4910058cfe..4f8118ec2b 100644 --- a/codex-rs/app-server/tests/suite/auth.rs +++ b/codex-rs/app-server/tests/suite/auth.rs @@ -220,6 +220,7 @@ async fn personal_access_token_without_email_supports_auth_status_and_account_re assert_eq!( to_response::(response)?, GetAccountResponse { + user_id: Some("user-123".to_string()), workspace_routing: Some(codex_app_server_protocol::WorkspaceRouting { chatgpt_account_id: "account-123".to_string(), backend_origin: "https://chatgpt.com".to_string(), diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index b5c39b109a..c8bb9eccbe 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -628,6 +628,7 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> { assert_eq!( account, GetAccountResponse { + user_id: None, workspace_routing: expected_workspace_routing(WORKSPACE_ID_EMBEDDED), account: Some(Account::Chatgpt { email: Some("embedded@example.com".to_string()), @@ -707,6 +708,7 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> { assert_eq!( account, GetAccountResponse { + user_id: None, workspace_routing: expected_workspace_routing(WORKSPACE_ID_EMBEDDED), account: Some(Account::Chatgpt { email: Some("embedded@example.com".to_string()), @@ -1431,6 +1433,7 @@ async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider( assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::AmazonBedrock { uses_codex_managed_credentials: true, @@ -1462,6 +1465,7 @@ async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider( assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: true, @@ -1678,6 +1682,7 @@ async fn logout_managed_bedrock_restores_default_account( assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::AmazonBedrock { uses_codex_managed_credentials: true, @@ -1729,6 +1734,7 @@ async fn logout_managed_bedrock_restores_default_account( assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: true, @@ -1794,6 +1800,7 @@ async fn logout_aws_managed_bedrock_clears_provider_and_restores_default_account assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::AmazonBedrock { uses_codex_managed_credentials: false, @@ -1833,6 +1840,7 @@ async fn logout_aws_managed_bedrock_clears_provider_and_restores_default_account assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: true, @@ -1902,6 +1910,7 @@ async fn logout_managed_bedrock_preserves_changed_provider_without_experimental_ assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: false, @@ -1982,6 +1991,7 @@ async fn login_managed_bedrock_updates_active_bedrock_account() -> Result<()> { assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::AmazonBedrock { uses_codex_managed_credentials: true, @@ -2508,6 +2518,7 @@ async fn login_survives_same_owner_token_refresh( }; timeout(DEFAULT_READ_TIMEOUT, started.notified()).await?; let expected_account = GetAccountResponse { + user_id: Some("device-user".to_string()), account: Some(Account::Chatgpt { email: Some("device@example.com".into()), plan_type: AccountPlanType::Enterprise, @@ -3248,8 +3259,15 @@ async fn get_account_no_auth() -> Result<()> { let account: GetAccountResponse = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; - assert_eq!(account.account, None, "expected no account"); - assert_eq!(account.requires_openai_auth, true); + assert_eq!( + account, + GetAccountResponse { + account: None, + user_id: None, + requires_openai_auth: true, + workspace_routing: None, + } + ); Ok(()) } @@ -3285,6 +3303,7 @@ async fn get_account_with_api_key() -> Result<()> { timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; let expected = GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::ApiKey {}), requires_openai_auth: true, @@ -3319,6 +3338,7 @@ async fn get_account_when_auth_not_required() -> Result<()> { timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; let expected = GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: false, @@ -3360,6 +3380,7 @@ region = "us-west-2" timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; let expected = GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::AmazonBedrock { uses_codex_managed_credentials: false, @@ -3399,6 +3420,7 @@ command = "print-token" assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::AmazonBedrock { uses_codex_managed_credentials: false, @@ -3438,6 +3460,7 @@ region = "us-west-2" assert_eq!( read_account(&mut mcp).await?, GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::AmazonBedrock { uses_codex_managed_credentials: false, @@ -3504,6 +3527,7 @@ async fn get_account_with_managed_bedrock_provider() -> Result<()> { assert_eq!( received, GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::AmazonBedrock { uses_codex_managed_credentials: true, @@ -3529,6 +3553,7 @@ async fn get_account_with_chatgpt() -> Result<()> { ChatGptAuthFixture::new("access-chatgpt") .account_id("account-123") .email("user@example.com") + .chatgpt_user_id("user-123") .plan_type("pro"), AuthCredentialsStoreMode::File, )?; @@ -3549,6 +3574,7 @@ async fn get_account_with_chatgpt() -> Result<()> { timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; let expected = GetAccountResponse { + user_id: Some("user-123".to_string()), workspace_routing: expected_workspace_routing("account-123"), account: Some(Account::Chatgpt { email: Some("user@example.com".to_string()), @@ -3607,6 +3633,7 @@ async fn get_account_with_chatgpt_plan_variants_returns_plan_type( assert_eq!( received, GetAccountResponse { + user_id: None, workspace_routing: expected_workspace_routing("account-123"), account: Some(Account::Chatgpt { email: Some("user@example.com".to_string()), @@ -3655,6 +3682,7 @@ async fn get_account_with_chatgpt_without_email() -> Result<()> { assert_eq!( received, GetAccountResponse { + user_id: None, workspace_routing: expected_workspace_routing("account-123"), account: Some(Account::Chatgpt { email: None, @@ -3738,6 +3766,7 @@ async fn get_account_omits_chatgpt_after_permanent_refresh_failure() -> Result<( assert_eq!( received, GetAccountResponse { + user_id: None, workspace_routing: None, account: None, requires_openai_auth: true, @@ -3781,6 +3810,7 @@ async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; let expected = GetAccountResponse { + user_id: None, workspace_routing: expected_workspace_routing("account-123"), account: Some(Account::Chatgpt { email: Some("user@example.com".to_string()), diff --git a/codex-rs/app-server/tests/suite/v2/account_gcp.rs b/codex-rs/app-server/tests/suite/v2/account_gcp.rs index 1f10f8dd7a..997bee143f 100644 --- a/codex-rs/app-server/tests/suite/v2/account_gcp.rs +++ b/codex-rs/app-server/tests/suite/v2/account_gcp.rs @@ -45,7 +45,7 @@ async fn gcp_account_is_reported_without_resolving_credentials(overrides: &str) let received: Value = timeout(READ_TIMEOUT, app_server.read_response(request_id)).await??; assert_eq!( received, - json!({"account": {"type": "gcp"}, "requiresOpenaiAuth": false, "workspaceRouting": null}) + json!({"account": {"type": "gcp"}, "userId": null, "requiresOpenaiAuth": false, "workspaceRouting": null}) ); } Ok(()) @@ -60,6 +60,7 @@ async fn gcp_account_tracks_provider_changes_with_saved_chatgpt_credentials() -> codex_home.path(), ChatGptAuthFixture::new("access-chatgpt") .email("user@example.com") + .chatgpt_user_id("user-saved") .plan_type("pro"), AuthCredentialsStoreMode::File, )?; @@ -72,19 +73,19 @@ async fn gcp_account_tracks_provider_changes_with_saved_chatgpt_credentials() -> for (provider, expected) in [ ( "gcp", - json!({"account": {"type": "gcp"}, "requiresOpenaiAuth": false, "workspaceRouting": null}), + json!({"account": {"type": "gcp"}, "userId": null, "requiresOpenaiAuth": false, "workspaceRouting": null}), ), ( "amazon-bedrock", - json!({"account": {"type": "amazonBedrock", "usesCodexManagedCredentials": false}, "requiresOpenaiAuth": false, "workspaceRouting": null}), + json!({"account": {"type": "amazonBedrock", "usesCodexManagedCredentials": false}, "userId": null, "requiresOpenaiAuth": false, "workspaceRouting": null}), ), ( "openai", - json!({"account": {"type": "chatgpt", "email": "user@example.com", "planType": "pro", "workspaceName": null}, "requiresOpenaiAuth": true, "workspaceRouting": null}), + json!({"account": {"type": "chatgpt", "email": "user@example.com", "planType": "pro", "workspaceName": null}, "userId": "user-saved", "requiresOpenaiAuth": true, "workspaceRouting": null}), ), ( "gcp", - json!({"account": {"type": "gcp"}, "requiresOpenaiAuth": false, "workspaceRouting": null}), + json!({"account": {"type": "gcp"}, "userId": null, "requiresOpenaiAuth": false, "workspaceRouting": null}), ), ] { std::fs::write(&config_path, format!("model_provider = \"{provider}\"\n"))?; diff --git a/codex-rs/app-server/tests/suite/v2/config_requirements_login.rs b/codex-rs/app-server/tests/suite/v2/config_requirements_login.rs index a3e2c487aa..0bdaed54cc 100644 --- a/codex-rs/app-server/tests/suite/v2/config_requirements_login.rs +++ b/codex-rs/app-server/tests/suite/v2/config_requirements_login.rs @@ -166,6 +166,7 @@ command = "print-token" assert_eq!( account, GetAccountResponse { + user_id: None, account: Some(Account::AmazonBedrock { uses_codex_managed_credentials: false }), diff --git a/codex-rs/app-server/tests/suite/v2/workspace_routing.rs b/codex-rs/app-server/tests/suite/v2/workspace_routing.rs index 117a0b847c..e921be65b4 100644 --- a/codex-rs/app-server/tests/suite/v2/workspace_routing.rs +++ b/codex-rs/app-server/tests/suite/v2/workspace_routing.rs @@ -159,7 +159,7 @@ async fn saved_workspace_is_discovered_once_and_not_the_default_account( ); let expected = json!({ "account": {"type": "chatgpt", "email": "user@example.com", "planType": plan_type, "workspaceName": "Selected workspace"}, - "requiresOpenaiAuth": true, "workspaceRouting": routing("selected", "https://gov.chatgpt.com", "NO_CONSTRAINT") + "userId": null, "requiresOpenaiAuth": true, "workspaceRouting": routing("selected", "https://gov.chatgpt.com", "NO_CONSTRAINT") }); assert_eq!(read(&mut server).await?, expected); assert_eq!(read(&mut server).await?, expected); @@ -186,7 +186,7 @@ async fn saved_chatgpt_login_without_selected_workspace_preserves_account() -> R assert_eq!( read(&mut server).await?, json!({"account": {"type": "chatgpt", "email": null, "planType": "pro", "workspaceName": null}, - "requiresOpenaiAuth": true, "workspaceRouting": null}) + "userId": null, "requiresOpenaiAuth": true, "workspaceRouting": null}) ); backend.verify().await; Ok(()) @@ -246,7 +246,7 @@ async fn stable_clients_do_not_treat_failed_workspace_discovery_as_unrestricted( assert_eq!( read(&mut server).await?, json!({"account": {"type": "chatgpt", "email": null, "planType": "pro", "workspaceName": null}, - "requiresOpenaiAuth": true, "workspaceRouting": routing("selected", "https://chatgpt.com", "NO_CONSTRAINT")}) + "userId": null, "requiresOpenaiAuth": true, "workspaceRouting": routing("selected", "https://chatgpt.com", "NO_CONSTRAINT")}) ); Ok(()) } @@ -364,7 +364,7 @@ async fn login_and_workspace_switch_notify_after_routing_is_ready_then_logout_cl let _: LogoutAccountResponse = timeout(READ_TIMEOUT, server.read_response(request)).await??; assert_eq!( read(&mut server).await?, - json!({"account": null, "requiresOpenaiAuth": true, "workspaceRouting": null}) + json!({"account": null, "userId": null, "requiresOpenaiAuth": true, "workspaceRouting": null}) ); backend.verify().await; Ok(()) @@ -466,7 +466,7 @@ async fn api_only_login_does_not_discover_chatgpt_routing() -> Result<()> { let _: LoginAccountResponse = timeout(READ_TIMEOUT, server.read_response(request)).await??; assert_eq!( read(&mut server).await?, - json!({"account": {"type": "apiKey"}, "requiresOpenaiAuth": true, "workspaceRouting": null}) + json!({"account": {"type": "apiKey"}, "userId": null, "requiresOpenaiAuth": true, "workspaceRouting": null}) ); backend.verify().await; Ok(()) diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index a0c6dbb917..d87374c570 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -3076,6 +3076,7 @@ mod tests { let mut app_server = crate::start_embedded_app_server_for_picker(&config).await?; let next_request_id = app_server.next_request_id; let account = GetAccountResponse { + user_id: None, workspace_routing: None, account: Some(Account::Chatgpt { email: Some("teammate@openai.com".to_string()), @@ -3138,6 +3139,7 @@ mod tests { .bootstrap_with_account( &client_config, GetAccountResponse { + user_id: None, account: Some(Account::ApiKey {}), requires_openai_auth: true, workspace_routing: None, @@ -3193,6 +3195,7 @@ mod tests { ThreadParamsMode::Cloud, ); let account = GetAccountResponse { + user_id: None, account: Some(Account::Chatgpt { email: Some("teammate@openai.com".to_string()), plan_type: codex_protocol::account::PlanType::Plus, diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index 020cb7b3cb..914cd5f063 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -8923,6 +8923,13 @@ class GetAccountResponse(BaseModel): ) account: Account | None = None requires_openai_auth: Annotated[bool, Field(alias="requiresOpenaiAuth")] + user_id: Annotated[ + str | None, + Field( + alias="userId", + description="ChatGPT user ID belonging to this account response, when available.", + ), + ] = None class GuardianApprovalReview(BaseModel):