From aee321f62b2ad8ce6126f1a9d6a07cffe62ff7a4 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Wed, 22 Oct 2025 15:36:11 -0700 Subject: [PATCH] [app-server] add new account method API stubs (#5527) These are the schema definitions for the new JSON-RPC APIs associated with accounts. These are not wired up to business logic yet and will currently throw an internal error indicating these are unimplemented. --- codex-rs/app-server-protocol/src/protocol.rs | 162 ++++++++++++++++-- .../app-server/src/codex_message_processor.rs | 30 ++++ codex-rs/protocol/src/account.rs | 35 ++++ codex-rs/protocol/src/lib.rs | 1 + 4 files changed, 213 insertions(+), 15 deletions(-) create mode 100644 codex-rs/protocol/src/account.rs diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs index bca57c72..4b4563aa 100644 --- a/codex-rs/app-server-protocol/src/protocol.rs +++ b/codex-rs/app-server-protocol/src/protocol.rs @@ -5,6 +5,7 @@ use crate::JSONRPCNotification; use crate::JSONRPCRequest; use crate::RequestId; use codex_protocol::ConversationId; +use codex_protocol::account::Account; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ReasoningEffort; use codex_protocol::config_types::ReasoningSummary; @@ -93,6 +94,43 @@ macro_rules! client_request_definitions { } client_request_definitions! { + /// NEW APIs + #[serde(rename = "model/list")] + #[ts(rename = "model/list")] + ListModels { + params: ListModelsParams, + response: ListModelsResponse, + }, + + #[serde(rename = "account/login")] + #[ts(rename = "account/login")] + LoginAccount { + params: LoginAccountParams, + response: LoginAccountResponse, + }, + + #[serde(rename = "account/logout")] + #[ts(rename = "account/logout")] + LogoutAccount { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + response: LogoutAccountResponse, + }, + + #[serde(rename = "account/rateLimits/read")] + #[ts(rename = "account/rateLimits/read")] + GetAccountRateLimits { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + response: GetAccountRateLimitsResponse, + }, + + #[serde(rename = "account/read")] + #[ts(rename = "account/read")] + GetAccount { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + response: Option, + }, + + /// DEPRECATED APIs below Initialize { params: InitializeParams, response: InitializeResponse, @@ -106,13 +144,6 @@ client_request_definitions! { params: ListConversationsParams, response: ListConversationsResponse, }, - #[serde(rename = "model/list")] - #[ts(rename = "model/list")] - /// List available Codex models along with display metadata. - ListModels { - params: ListModelsParams, - response: ListModelsResponse, - }, /// Resume a recorded Codex conversation from a rollout file. ResumeConversation { params: ResumeConversationParams, @@ -191,12 +222,6 @@ client_request_definitions! { params: ExecOneOffCommandParams, response: ExecOneOffCommandResponse, }, - #[serde(rename = "account/rateLimits/read")] - #[ts(rename = "account/rateLimits/read")] - GetAccountRateLimits { - params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, - response: GetAccountRateLimitsResponse, - }, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] @@ -352,6 +377,38 @@ pub struct ListModelsResponse { pub next_cursor: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type")] +#[ts(tag = "type")] +pub enum LoginAccountParams { + #[serde(rename = "apiKey")] + #[ts(rename = "apiKey")] + ApiKey { + #[serde(rename = "apiKey")] + #[ts(rename = "apiKey")] + api_key: String, + }, + #[serde(rename = "chatgpt")] + #[ts(rename = "chatgpt")] + ChatGpt, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LoginAccountResponse { + /// Only set if the login method is ChatGPT. + #[schemars(with = "String")] + pub login_id: Option, + + /// URL the client should open in a browser to initiate the OAuth flow. + /// Only set if the login method is ChatGPT. + pub auth_url: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LogoutAccountResponse {} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ResumeConversationParams { @@ -875,11 +932,13 @@ pub struct AuthStatusChangeNotification { #[serde(tag = "method", content = "params", rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] pub enum ServerNotification { + /// NEW NOTIFICATIONS #[serde(rename = "account/rateLimits/updated")] #[ts(rename = "account/rateLimits/updated")] #[strum(serialize = "account/rateLimits/updated")] AccountRateLimitsUpdated(RateLimitSnapshot), + /// DEPRECATED NOTIFICATIONS below /// Authentication status changed AuthStatusChange(AuthStatusChangeNotification), @@ -1049,16 +1108,89 @@ mod tests { Ok(()) } + #[test] + fn serialize_account_login_api_key() -> Result<()> { + let request = ClientRequest::LoginAccount { + request_id: RequestId::Integer(2), + params: LoginAccountParams::ApiKey { + api_key: "secret".to_string(), + }, + }; + assert_eq!( + json!({ + "method": "account/login", + "id": 2, + "params": { + "type": "apiKey", + "apiKey": "secret" + } + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } + + #[test] + fn serialize_account_login_chatgpt() -> Result<()> { + let request = ClientRequest::LoginAccount { + request_id: RequestId::Integer(3), + params: LoginAccountParams::ChatGpt, + }; + assert_eq!( + json!({ + "method": "account/login", + "id": 3, + "params": { + "type": "chatgpt" + } + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } + + #[test] + fn serialize_account_logout() -> Result<()> { + let request = ClientRequest::LogoutAccount { + request_id: RequestId::Integer(4), + params: None, + }; + assert_eq!( + json!({ + "method": "account/logout", + "id": 4, + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } + + #[test] + fn serialize_get_account() -> Result<()> { + let request = ClientRequest::GetAccount { + request_id: RequestId::Integer(5), + params: None, + }; + assert_eq!( + json!({ + "method": "account/read", + "id": 5, + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } + #[test] fn serialize_list_models() -> Result<()> { let request = ClientRequest::ListModels { - request_id: RequestId::Integer(2), + request_id: RequestId::Integer(6), params: ListModelsParams::default(), }; assert_eq!( json!({ "method": "model/list", - "id": 2, + "id": 6, "params": {} }), serde_json::to_value(&request)?, diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index f256b78d..5c10a89c 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -176,6 +176,27 @@ impl CodexMessageProcessor { ClientRequest::ListModels { request_id, params } => { self.list_models(request_id, params).await; } + ClientRequest::LoginAccount { + request_id, + params: _, + } => { + self.send_unimplemented_error(request_id, "account/login") + .await; + } + ClientRequest::LogoutAccount { + request_id, + params: _, + } => { + self.send_unimplemented_error(request_id, "account/logout") + .await; + } + ClientRequest::GetAccount { + request_id, + params: _, + } => { + self.send_unimplemented_error(request_id, "account/read") + .await; + } ClientRequest::ResumeConversation { request_id, params } => { self.handle_resume_conversation(request_id, params).await; } @@ -257,6 +278,15 @@ impl CodexMessageProcessor { } } + async fn send_unimplemented_error(&self, request_id: RequestId, method: &str) { + let error = JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("{method} is not implemented yet"), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + } + async fn login_api_key(&mut self, request_id: RequestId, params: LoginApiKeyParams) { if matches!( self.config.forced_login_method, diff --git a/codex-rs/protocol/src/account.rs b/codex-rs/protocol/src/account.rs new file mode 100644 index 00000000..1d63910c --- /dev/null +++ b/codex-rs/protocol/src/account.rs @@ -0,0 +1,35 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(rename_all = "lowercase")] +#[ts(rename_all = "lowercase")] +pub enum PlanType { + #[default] + Free, + Plus, + Pro, + Team, + Business, + Enterprise, + Edu, + #[serde(other)] + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] +#[serde(tag = "type")] +#[ts(tag = "type")] +pub enum Account { + ApiKey { + api_key: String, + }, + #[serde(rename = "chatgpt")] + #[ts(rename = "chatgpt")] + ChatGpt { + email: Option, + plan_type: PlanType, + }, +} diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index e79eff3f..3b8747e6 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -1,3 +1,4 @@ +pub mod account; mod conversation_id; pub use conversation_id::ConversationId; pub mod config_types;