[codex] include plan type in account updates (#13181)
This change fixes a Codex app account-state sync bug where clients could know the user was signed in but still miss the ChatGPT subscription tier, which could lead to incorrect upgrade messaging for paid users. The root cause was that `account/updated` only carried `authMode` while plan information was available separately via `account/read` and rate-limit snapshots, so this update adds `planType` to `account/updated`, populates it consistently across login and refresh paths.
This commit is contained in:
parent
4ae60cf03c
commit
c9cef6ba9e
9 changed files with 76 additions and 24 deletions
|
|
@ -46,6 +46,16 @@
|
|||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PlanType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
|
|
|||
|
|
@ -7319,6 +7319,16 @@
|
|||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/PlanType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "AccountUpdatedNotification",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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, };
|
||||
|
|
|
|||
|
|
@ -2557,6 +2557,7 @@ pub struct Thread {
|
|||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountUpdatedNotification {
|
||||
pub auth_mode: Option<AuthMode>,
|
||||
pub plan_type: Option<PlanType>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
|
|
|||
|
|
@ -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": "<uuid>", "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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue