diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 7a29ef92b..28df2574b 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -46,6 +46,16 @@ "type": "null" } ] + }, + "planType": { + "anyOf": [ + { + "$ref": "#/definitions/PlanType" + }, + { + "type": "null" + } + ] } }, "type": "object" 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 374d43de7..edee815e0 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 @@ -7319,6 +7319,16 @@ "type": "null" } ] + }, + "planType": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PlanType" + }, + { + "type": "null" + } + ] } }, "title": "AccountUpdatedNotification", diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json index d95d73706..8348b774c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AccountUpdatedNotification.json @@ -26,6 +26,20 @@ "type": "string" } ] + }, + "PlanType": { + "enum": [ + "free", + "go", + "plus", + "pro", + "team", + "business", + "enterprise", + "edu", + "unknown" + ], + "type": "string" } }, "properties": { @@ -38,6 +52,16 @@ "type": "null" } ] + }, + "planType": { + "anyOf": [ + { + "$ref": "#/definitions/PlanType" + }, + { + "type": "null" + } + ] } }, "title": "AccountUpdatedNotification", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AccountUpdatedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AccountUpdatedNotification.ts index eacb81541..84bf626e0 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AccountUpdatedNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AccountUpdatedNotification.ts @@ -2,5 +2,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AuthMode } from "../AuthMode"; +import type { PlanType } from "../PlanType"; -export type AccountUpdatedNotification = { authMode: AuthMode | null, }; +export type AccountUpdatedNotification = { authMode: AuthMode | null, planType: PlanType | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a26ca25bd..bde0e17fa 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2557,6 +2557,7 @@ pub struct Thread { #[ts(export_to = "v2/")] pub struct AccountUpdatedNotification { pub auth_mode: Option, + pub plan_type: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index b43cd99c6..64d0625c8 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -952,7 +952,7 @@ The JSON-RPC auth/account surface exposes request/response methods plus server-i ### Authentication modes -Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`) and can be inferred from `account/read`. +Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`), which also includes the current ChatGPT `planType` when available, and can be inferred from `account/read`. - **API key (`apiKey`)**: Caller supplies an OpenAI API key via `account/login/start` with `type: "apiKey"`. The API key is saved and used for API requests. - **ChatGPT managed (`chatgpt`)** (recommended): Codex owns the ChatGPT OAuth flow and refresh tokens. Start via `account/login/start` with `type: "chatgpt"`; Codex persists tokens to disk and refreshes them automatically. @@ -964,7 +964,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco - `account/login/completed` (notify) — emitted when a login attempt finishes (success or error). - `account/login/cancel` — cancel a pending ChatGPT login by `loginId`. - `account/logout` — sign out; triggers `account/updated`. -- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`). +- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType` when available. - `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify). - `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change. - `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`. @@ -1008,7 +1008,7 @@ Field notes: 3. Notifications: ```json { "method": "account/login/completed", "params": { "loginId": null, "success": true, "error": null } } - { "method": "account/updated", "params": { "authMode": "apikey" } } + { "method": "account/updated", "params": { "authMode": "apikey", "planType": null } } ``` ### 3) Log in with ChatGPT (browser flow) @@ -1022,7 +1022,7 @@ Field notes: 3. Wait for notifications: ```json { "method": "account/login/completed", "params": { "loginId": "", "success": true, "error": null } } - { "method": "account/updated", "params": { "authMode": "chatgpt" } } + { "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus" } } ``` ### 4) Cancel a ChatGPT login @@ -1037,7 +1037,7 @@ Field notes: ```json { "method": "account/logout", "id": 5 } { "id": 5, "result": {} } -{ "method": "account/updated", "params": { "authMode": null } } +{ "method": "account/updated", "params": { "authMode": null, "planType": null } } ``` ### 6) Rate limits (ChatGPT) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index eef211ff8..e65f7e888 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -423,6 +423,14 @@ pub(crate) struct CodexMessageProcessorArgs { } impl CodexMessageProcessor { + fn current_account_updated_notification(&self) -> AccountUpdatedNotification { + let auth = self.auth_manager.auth_cached(); + AccountUpdatedNotification { + auth_mode: auth.as_ref().map(CodexAuth::api_auth_mode), + plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type), + } + } + async fn load_thread( &self, thread_id: &str, @@ -1057,15 +1065,10 @@ impl CodexMessageProcessor { )) .await; - let payload_v2 = AccountUpdatedNotification { - auth_mode: self - .auth_manager - .auth_cached() - .as_ref() - .map(CodexAuth::api_auth_mode), - }; self.outgoing - .send_server_notification(ServerNotification::AccountUpdated(payload_v2)) + .send_server_notification(ServerNotification::AccountUpdated( + self.current_account_updated_notification(), + )) .await; } Err(error) => { @@ -1281,12 +1284,10 @@ impl CodexMessageProcessor { .await; // Notify clients with the actual current auth mode. - let current_auth_method = auth_manager - .auth_cached() - .as_ref() - .map(CodexAuth::api_auth_mode); + let auth = auth_manager.auth_cached(); let payload_v2 = AccountUpdatedNotification { - auth_mode: current_auth_method, + auth_mode: auth.as_ref().map(CodexAuth::api_auth_mode), + plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type), }; outgoing_clone .send_server_notification(ServerNotification::AccountUpdated( @@ -1467,11 +1468,10 @@ impl CodexMessageProcessor { )) .await; - let payload_v2 = AccountUpdatedNotification { - auth_mode: self.auth_manager.get_api_auth_mode(), - }; self.outgoing - .send_server_notification(ServerNotification::AccountUpdated(payload_v2)) + .send_server_notification(ServerNotification::AccountUpdated( + self.current_account_updated_notification(), + )) .await; } @@ -1529,6 +1529,7 @@ impl CodexMessageProcessor { let payload_v2 = AccountUpdatedNotification { auth_mode: current_auth_method, + plan_type: None, }; self.outgoing .send_server_notification(ServerNotification::AccountUpdated(payload_v2)) diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index d6605ebe5..5f56c81c5 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -612,6 +612,7 @@ mod tests { fn verify_account_updated_notification_serialization() { let notification = ServerNotification::AccountUpdated(AccountUpdatedNotification { auth_mode: Some(AuthMode::ApiKey), + plan_type: None, }); let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification); @@ -619,7 +620,8 @@ mod tests { json!({ "method": "account/updated", "params": { - "authMode": "apikey" + "authMode": "apikey", + "planType": null }, }), serde_json::to_value(jsonrpc_notification) diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index d4d702a0d..48ede78f4 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -131,6 +131,7 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> { payload.auth_mode.is_none(), "auth_method should be None after logout" ); + assert_eq!(payload.plan_type, None); assert!( !codex_home.path().join("auth.json").exists(), @@ -201,6 +202,7 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> { bail!("unexpected notification: {parsed:?}"); }; assert_eq!(payload.auth_mode, Some(AuthMode::ChatgptAuthTokens)); + assert_eq!(payload.plan_type, Some(AccountPlanType::Pro)); let get_id = mcp .send_get_account_request(GetAccountParams { @@ -843,6 +845,7 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> { bail!("unexpected notification: {parsed:?}"); }; pretty_assertions::assert_eq!(payload.auth_mode, Some(AuthMode::ApiKey)); + pretty_assertions::assert_eq!(payload.plan_type, None); assert!(codex_home.path().join("auth.json").exists()); Ok(())