Skip to main content

codex_app_server_protocol/protocol/
common.rs

1use std::path::Path;
2
3use crate::JSONRPCNotification;
4use crate::JSONRPCRequest;
5use crate::RequestId;
6use crate::export::GeneratedSchema;
7use crate::export::write_json_schema;
8use crate::protocol::v1;
9use crate::protocol::v2;
10use schemars::JsonSchema;
11use serde::Deserialize;
12use serde::Serialize;
13use strum_macros::Display;
14use ts_rs::TS;
15
16#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
17#[ts(type = "string")]
18pub struct GitSha(pub String);
19
20impl GitSha {
21    pub fn new(sha: &str) -> Self {
22        Self(sha.to_string())
23    }
24}
25
26#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
27#[serde(rename_all = "lowercase")]
28pub enum AuthMode {
29    ApiKey,
30    ChatGPT,
31}
32
33/// Generates an `enum ClientRequest` where each variant is a request that the
34/// client can send to the server. Each variant has associated `params` and
35/// `response` types. Also generates a `export_client_responses()` function to
36/// export all response types to TypeScript.
37macro_rules! client_request_definitions {
38    (
39        $(
40            $(#[$variant_meta:meta])*
41            $variant:ident $(=> $wire:literal)? {
42                params: $(#[$params_meta:meta])* $params:ty,
43                response: $response:ty,
44            }
45        ),* $(,)?
46    ) => {
47        /// Request from the client to the server.
48        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
49        #[serde(tag = "method", rename_all = "camelCase")]
50        pub enum ClientRequest {
51            $(
52                $(#[$variant_meta])*
53                $(#[serde(rename = $wire)] #[ts(rename = $wire)])?
54                $variant {
55                    #[serde(rename = "id")]
56                    request_id: RequestId,
57                    $(#[$params_meta])*
58                    params: $params,
59                },
60            )*
61        }
62
63        pub fn export_client_responses(
64            out_dir: &::std::path::Path,
65        ) -> ::std::result::Result<(), ::ts_rs::ExportError> {
66            $(
67                <$response as ::ts_rs::TS>::export_all_to(out_dir)?;
68            )*
69            Ok(())
70        }
71
72        #[allow(clippy::vec_init_then_push)]
73        pub fn export_client_response_schemas(
74            out_dir: &::std::path::Path,
75        ) -> ::anyhow::Result<Vec<GeneratedSchema>> {
76            let mut schemas = Vec::new();
77            $(
78                schemas.push(write_json_schema::<$response>(out_dir, stringify!($response))?);
79            )*
80            Ok(schemas)
81        }
82
83        #[allow(clippy::vec_init_then_push)]
84        pub fn export_client_param_schemas(
85            out_dir: &::std::path::Path,
86        ) -> ::anyhow::Result<Vec<GeneratedSchema>> {
87            let mut schemas = Vec::new();
88            $(
89                schemas.push(write_json_schema::<$params>(out_dir, stringify!($params))?);
90            )*
91            Ok(schemas)
92        }
93    };
94}
95
96client_request_definitions! {
97    Initialize {
98        params: v1::InitializeParams,
99        response: v1::InitializeResponse,
100    },
101
102    /// NEW APIs
103    // Thread lifecycle
104    ThreadStart => "thread/start" {
105        params: v2::ThreadStartParams,
106        response: v2::ThreadStartResponse,
107    },
108    ThreadResume => "thread/resume" {
109        params: v2::ThreadResumeParams,
110        response: v2::ThreadResumeResponse,
111    },
112    ThreadArchive => "thread/archive" {
113        params: v2::ThreadArchiveParams,
114        response: v2::ThreadArchiveResponse,
115    },
116    ThreadList => "thread/list" {
117        params: v2::ThreadListParams,
118        response: v2::ThreadListResponse,
119    },
120    ThreadCompact => "thread/compact" {
121        params: v2::ThreadCompactParams,
122        response: v2::ThreadCompactResponse,
123    },
124    TurnStart => "turn/start" {
125        params: v2::TurnStartParams,
126        response: v2::TurnStartResponse,
127    },
128    TurnInterrupt => "turn/interrupt" {
129        params: v2::TurnInterruptParams,
130        response: v2::TurnInterruptResponse,
131    },
132    ReviewStart => "review/start" {
133        params: v2::ReviewStartParams,
134        response: v2::TurnStartResponse,
135    },
136
137    ModelList => "model/list" {
138        params: v2::ModelListParams,
139        response: v2::ModelListResponse,
140    },
141
142    LoginAccount => "account/login/start" {
143        params: v2::LoginAccountParams,
144        response: v2::LoginAccountResponse,
145    },
146
147    CancelLoginAccount => "account/login/cancel" {
148        params: v2::CancelLoginAccountParams,
149        response: v2::CancelLoginAccountResponse,
150    },
151
152    LogoutAccount => "account/logout" {
153        params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
154        response: v2::LogoutAccountResponse,
155    },
156
157    GetAccountRateLimits => "account/rateLimits/read" {
158        params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
159        response: v2::GetAccountRateLimitsResponse,
160    },
161
162    FeedbackUpload => "feedback/upload" {
163        params: v2::FeedbackUploadParams,
164        response: v2::FeedbackUploadResponse,
165    },
166
167    GetAccount => "account/read" {
168        params: v2::GetAccountParams,
169        response: v2::GetAccountResponse,
170    },
171
172    /// DEPRECATED APIs below
173    NewConversation {
174        params: v1::NewConversationParams,
175        response: v1::NewConversationResponse,
176    },
177    GetConversationSummary {
178        params: v1::GetConversationSummaryParams,
179        response: v1::GetConversationSummaryResponse,
180    },
181    /// List recorded Codex conversations (rollouts) with optional pagination and search.
182    ListConversations {
183        params: v1::ListConversationsParams,
184        response: v1::ListConversationsResponse,
185    },
186    /// Resume a recorded Codex conversation from a rollout file.
187    ResumeConversation {
188        params: v1::ResumeConversationParams,
189        response: v1::ResumeConversationResponse,
190    },
191    ArchiveConversation {
192        params: v1::ArchiveConversationParams,
193        response: v1::ArchiveConversationResponse,
194    },
195    SendUserMessage {
196        params: v1::SendUserMessageParams,
197        response: v1::SendUserMessageResponse,
198    },
199    SendUserTurn {
200        params: v1::SendUserTurnParams,
201        response: v1::SendUserTurnResponse,
202    },
203    InterruptConversation {
204        params: v1::InterruptConversationParams,
205        response: v1::InterruptConversationResponse,
206    },
207    AddConversationListener {
208        params: v1::AddConversationListenerParams,
209        response: v1::AddConversationSubscriptionResponse,
210    },
211    RemoveConversationListener {
212        params: v1::RemoveConversationListenerParams,
213        response: v1::RemoveConversationSubscriptionResponse,
214    },
215    GitDiffToRemote {
216        params: v1::GitDiffToRemoteParams,
217        response: v1::GitDiffToRemoteResponse,
218    },
219    LoginApiKey {
220        params: v1::LoginApiKeyParams,
221        response: v1::LoginApiKeyResponse,
222    },
223    LoginChatGpt {
224        params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
225        response: v1::LoginChatGptResponse,
226    },
227    // DEPRECATED in favor of CancelLoginAccount
228    CancelLoginChatGpt {
229        params: v1::CancelLoginChatGptParams,
230        response: v1::CancelLoginChatGptResponse,
231    },
232    LogoutChatGpt {
233        params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
234        response: v1::LogoutChatGptResponse,
235    },
236    /// DEPRECATED in favor of GetAccount
237    GetAuthStatus {
238        params: v1::GetAuthStatusParams,
239        response: v1::GetAuthStatusResponse,
240    },
241    GetUserSavedConfig {
242        params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
243        response: v1::GetUserSavedConfigResponse,
244    },
245    SetDefaultModel {
246        params: v1::SetDefaultModelParams,
247        response: v1::SetDefaultModelResponse,
248    },
249    GetUserAgent {
250        params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
251        response: v1::GetUserAgentResponse,
252    },
253    UserInfo {
254        params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
255        response: v1::UserInfoResponse,
256    },
257    FuzzyFileSearch {
258        params: FuzzyFileSearchParams,
259        response: FuzzyFileSearchResponse,
260    },
261    /// Execute a command (argv vector) under the server's sandbox.
262    ExecOneOffCommand {
263        params: v1::ExecOneOffCommandParams,
264        response: v1::ExecOneOffCommandResponse,
265    },
266}
267
268/// Generates an `enum ServerRequest` where each variant is a request that the
269/// server can send to the client along with the corresponding params and
270/// response types. It also generates helper types used by the app/server
271/// infrastructure (payload enum, request constructor, and export helpers).
272macro_rules! server_request_definitions {
273    (
274        $(
275            $(#[$variant_meta:meta])*
276            $variant:ident $(=> $wire:literal)? {
277                params: $params:ty,
278                response: $response:ty,
279            }
280        ),* $(,)?
281    ) => {
282        /// Request initiated from the server and sent to the client.
283        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
284        #[serde(tag = "method", rename_all = "camelCase")]
285        pub enum ServerRequest {
286            $(
287                $(#[$variant_meta])*
288                $(#[serde(rename = $wire)] #[ts(rename = $wire)])?
289                $variant {
290                    #[serde(rename = "id")]
291                    request_id: RequestId,
292                    params: $params,
293                },
294            )*
295        }
296
297        #[derive(Debug, Clone, PartialEq, JsonSchema)]
298        pub enum ServerRequestPayload {
299            $( $variant($params), )*
300        }
301
302        impl ServerRequestPayload {
303            pub fn request_with_id(self, request_id: RequestId) -> ServerRequest {
304                match self {
305                    $(Self::$variant(params) => ServerRequest::$variant { request_id, params },)*
306                }
307            }
308        }
309
310        pub fn export_server_responses(
311            out_dir: &::std::path::Path,
312        ) -> ::std::result::Result<(), ::ts_rs::ExportError> {
313            $(
314                <$response as ::ts_rs::TS>::export_all_to(out_dir)?;
315            )*
316            Ok(())
317        }
318
319        #[allow(clippy::vec_init_then_push)]
320        pub fn export_server_response_schemas(
321            out_dir: &Path,
322        ) -> ::anyhow::Result<Vec<GeneratedSchema>> {
323            let mut schemas = Vec::new();
324            $(
325                schemas.push(crate::export::write_json_schema::<$response>(
326                    out_dir,
327                    concat!(stringify!($variant), "Response"),
328                )?);
329            )*
330            Ok(schemas)
331        }
332
333        #[allow(clippy::vec_init_then_push)]
334        pub fn export_server_param_schemas(
335            out_dir: &Path,
336        ) -> ::anyhow::Result<Vec<GeneratedSchema>> {
337            let mut schemas = Vec::new();
338            $(
339                schemas.push(crate::export::write_json_schema::<$params>(
340                    out_dir,
341                    concat!(stringify!($variant), "Params"),
342                )?);
343            )*
344            Ok(schemas)
345        }
346    };
347}
348
349/// Generates `ServerNotification` enum and helpers, including a JSON Schema
350/// exporter for each notification.
351macro_rules! server_notification_definitions {
352    (
353        $(
354            $(#[$variant_meta:meta])*
355            $variant:ident $(=> $wire:literal)? ( $payload:ty )
356        ),* $(,)?
357    ) => {
358        /// Notification sent from the server to the client.
359        #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
360        #[serde(tag = "method", content = "params", rename_all = "camelCase")]
361        #[strum(serialize_all = "camelCase")]
362        pub enum ServerNotification {
363            $(
364                $(#[$variant_meta])*
365                $(#[serde(rename = $wire)] #[ts(rename = $wire)] #[strum(serialize = $wire)])?
366                $variant($payload),
367            )*
368        }
369
370        impl ServerNotification {
371            pub fn to_params(self) -> Result<serde_json::Value, serde_json::Error> {
372                match self {
373                    $(Self::$variant(params) => serde_json::to_value(params),)*
374                }
375            }
376        }
377
378        impl TryFrom<JSONRPCNotification> for ServerNotification {
379            type Error = serde_json::Error;
380
381            fn try_from(value: JSONRPCNotification) -> Result<Self, serde_json::Error> {
382                serde_json::from_value(serde_json::to_value(value)?)
383            }
384        }
385
386        #[allow(clippy::vec_init_then_push)]
387        pub fn export_server_notification_schemas(
388            out_dir: &::std::path::Path,
389        ) -> ::anyhow::Result<Vec<GeneratedSchema>> {
390            let mut schemas = Vec::new();
391            $(schemas.push(crate::export::write_json_schema::<$payload>(out_dir, stringify!($payload))?);)*
392            Ok(schemas)
393        }
394    };
395}
396/// Notifications sent from the client to the server.
397macro_rules! client_notification_definitions {
398    (
399        $(
400            $(#[$variant_meta:meta])*
401            $variant:ident $( ( $payload:ty ) )?
402        ),* $(,)?
403    ) => {
404        #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)]
405        #[serde(tag = "method", content = "params", rename_all = "camelCase")]
406        #[strum(serialize_all = "camelCase")]
407        pub enum ClientNotification {
408            $(
409                $(#[$variant_meta])*
410                $variant $( ( $payload ) )?,
411            )*
412        }
413
414        pub fn export_client_notification_schemas(
415            _out_dir: &::std::path::Path,
416        ) -> ::anyhow::Result<Vec<GeneratedSchema>> {
417            let schemas = Vec::new();
418            $( $(schemas.push(crate::export::write_json_schema::<$payload>(_out_dir, stringify!($payload))?);)? )*
419            Ok(schemas)
420        }
421    };
422}
423
424impl TryFrom<JSONRPCRequest> for ServerRequest {
425    type Error = serde_json::Error;
426
427    fn try_from(value: JSONRPCRequest) -> Result<Self, Self::Error> {
428        serde_json::from_value(serde_json::to_value(value)?)
429    }
430}
431
432server_request_definitions! {
433    /// NEW APIs
434    /// Sent when approval is requested for a specific command execution.
435    /// This request is used for Turns started via turn/start.
436    CommandExecutionRequestApproval => "item/commandExecution/requestApproval" {
437        params: v2::CommandExecutionRequestApprovalParams,
438        response: v2::CommandExecutionRequestApprovalResponse,
439    },
440
441    /// Sent when approval is requested for a specific file change.
442    /// This request is used for Turns started via turn/start.
443    FileChangeRequestApproval => "item/fileChange/requestApproval" {
444        params: v2::FileChangeRequestApprovalParams,
445        response: v2::FileChangeRequestApprovalResponse,
446    },
447
448    /// DEPRECATED APIs below
449    /// Request to approve a patch.
450    /// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
451    ApplyPatchApproval {
452        params: v1::ApplyPatchApprovalParams,
453        response: v1::ApplyPatchApprovalResponse,
454    },
455    /// Request to exec a command.
456    /// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
457    ExecCommandApproval {
458        params: v1::ExecCommandApprovalParams,
459        response: v1::ExecCommandApprovalResponse,
460    },
461}
462
463#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
464#[serde(rename_all = "camelCase")]
465#[ts(rename_all = "camelCase")]
466pub struct FuzzyFileSearchParams {
467    pub query: String,
468    pub roots: Vec<String>,
469    // if provided, will cancel any previous request that used the same value
470    pub cancellation_token: Option<String>,
471}
472
473/// Superset of [`codex_file_search::FileMatch`]
474#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
475pub struct FuzzyFileSearchResult {
476    pub root: String,
477    pub path: String,
478    pub file_name: String,
479    pub score: u32,
480    pub indices: Option<Vec<u32>>,
481}
482
483#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
484pub struct FuzzyFileSearchResponse {
485    pub files: Vec<FuzzyFileSearchResult>,
486}
487
488server_notification_definitions! {
489    /// NEW NOTIFICATIONS
490    Error => "error" (v2::ErrorNotification),
491    ThreadStarted => "thread/started" (v2::ThreadStartedNotification),
492    TurnStarted => "turn/started" (v2::TurnStartedNotification),
493    TurnCompleted => "turn/completed" (v2::TurnCompletedNotification),
494    ItemStarted => "item/started" (v2::ItemStartedNotification),
495    ItemCompleted => "item/completed" (v2::ItemCompletedNotification),
496    AgentMessageDelta => "item/agentMessage/delta" (v2::AgentMessageDeltaNotification),
497    CommandExecutionOutputDelta => "item/commandExecution/outputDelta" (v2::CommandExecutionOutputDeltaNotification),
498    McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification),
499    AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
500    AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),
501    ReasoningSummaryTextDelta => "item/reasoning/summaryTextDelta" (v2::ReasoningSummaryTextDeltaNotification),
502    ReasoningSummaryPartAdded => "item/reasoning/summaryPartAdded" (v2::ReasoningSummaryPartAddedNotification),
503    ReasoningTextDelta => "item/reasoning/textDelta" (v2::ReasoningTextDeltaNotification),
504
505    /// Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.
506    WindowsWorldWritableWarning => "windows/worldWritableWarning" (v2::WindowsWorldWritableWarningNotification),
507
508    #[serde(rename = "account/login/completed")]
509    #[ts(rename = "account/login/completed")]
510    #[strum(serialize = "account/login/completed")]
511    AccountLoginCompleted(v2::AccountLoginCompletedNotification),
512
513    /// DEPRECATED NOTIFICATIONS below
514    AuthStatusChange(v1::AuthStatusChangeNotification),
515
516    /// Deprecated: use `account/login/completed` instead.
517    LoginChatGptComplete(v1::LoginChatGptCompleteNotification),
518    SessionConfigured(v1::SessionConfiguredNotification),
519}
520
521client_notification_definitions! {
522    Initialized,
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528    use anyhow::Result;
529    use codex_protocol::ConversationId;
530    use codex_protocol::account::PlanType;
531    use codex_protocol::parse_command::ParsedCommand;
532    use codex_protocol::protocol::AskForApproval;
533    use pretty_assertions::assert_eq;
534    use serde_json::json;
535    use std::path::PathBuf;
536
537    #[test]
538    fn serialize_new_conversation() -> Result<()> {
539        let request = ClientRequest::NewConversation {
540            request_id: RequestId::Integer(42),
541            params: v1::NewConversationParams {
542                model: Some("gpt-5.1-codex-max".to_string()),
543                model_provider: None,
544                profile: None,
545                cwd: None,
546                approval_policy: Some(AskForApproval::OnRequest),
547                sandbox: None,
548                config: None,
549                base_instructions: None,
550                developer_instructions: None,
551                compact_prompt: None,
552                include_apply_patch_tool: None,
553            },
554        };
555        assert_eq!(
556            json!({
557                "method": "newConversation",
558                "id": 42,
559                "params": {
560                    "model": "gpt-5.1-codex-max",
561                    "modelProvider": null,
562                    "profile": null,
563                    "cwd": null,
564                    "approvalPolicy": "on-request",
565                    "sandbox": null,
566                    "config": null,
567                    "baseInstructions": null,
568                    "includeApplyPatchTool": null
569                }
570            }),
571            serde_json::to_value(&request)?,
572        );
573        Ok(())
574    }
575
576    #[test]
577    fn conversation_id_serializes_as_plain_string() -> Result<()> {
578        let id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
579
580        assert_eq!(
581            json!("67e55044-10b1-426f-9247-bb680e5fe0c8"),
582            serde_json::to_value(id)?
583        );
584        Ok(())
585    }
586
587    #[test]
588    fn conversation_id_deserializes_from_plain_string() -> Result<()> {
589        let id: ConversationId =
590            serde_json::from_value(json!("67e55044-10b1-426f-9247-bb680e5fe0c8"))?;
591
592        assert_eq!(
593            ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?,
594            id,
595        );
596        Ok(())
597    }
598
599    #[test]
600    fn serialize_client_notification() -> Result<()> {
601        let notification = ClientNotification::Initialized;
602        // Note there is no "params" field for this notification.
603        assert_eq!(
604            json!({
605                "method": "initialized",
606            }),
607            serde_json::to_value(&notification)?,
608        );
609        Ok(())
610    }
611
612    #[test]
613    fn serialize_server_request() -> Result<()> {
614        let conversation_id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
615        let params = v1::ExecCommandApprovalParams {
616            conversation_id,
617            call_id: "call-42".to_string(),
618            command: vec!["echo".to_string(), "hello".to_string()],
619            cwd: PathBuf::from("/tmp"),
620            reason: Some("because tests".to_string()),
621            risk: None,
622            parsed_cmd: vec![ParsedCommand::Unknown {
623                cmd: "echo hello".to_string(),
624            }],
625        };
626        let request = ServerRequest::ExecCommandApproval {
627            request_id: RequestId::Integer(7),
628            params: params.clone(),
629        };
630
631        assert_eq!(
632            json!({
633                "method": "execCommandApproval",
634                "id": 7,
635                "params": {
636                    "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8",
637                    "callId": "call-42",
638                    "command": ["echo", "hello"],
639                    "cwd": "/tmp",
640                    "reason": "because tests",
641                    "risk": null,
642                    "parsedCmd": [
643                        {
644                            "type": "unknown",
645                            "cmd": "echo hello"
646                        }
647                    ]
648                }
649            }),
650            serde_json::to_value(&request)?,
651        );
652
653        let payload = ServerRequestPayload::ExecCommandApproval(params);
654        assert_eq!(payload.request_with_id(RequestId::Integer(7)), request);
655        Ok(())
656    }
657
658    #[test]
659    fn serialize_get_account_rate_limits() -> Result<()> {
660        let request = ClientRequest::GetAccountRateLimits {
661            request_id: RequestId::Integer(1),
662            params: None,
663        };
664        assert_eq!(
665            json!({
666                "method": "account/rateLimits/read",
667                "id": 1,
668            }),
669            serde_json::to_value(&request)?,
670        );
671        Ok(())
672    }
673
674    #[test]
675    fn serialize_account_login_api_key() -> Result<()> {
676        let request = ClientRequest::LoginAccount {
677            request_id: RequestId::Integer(2),
678            params: v2::LoginAccountParams::ApiKey {
679                api_key: "secret".to_string(),
680            },
681        };
682        assert_eq!(
683            json!({
684                "method": "account/login/start",
685                "id": 2,
686                "params": {
687                    "type": "apiKey",
688                    "apiKey": "secret"
689                }
690            }),
691            serde_json::to_value(&request)?,
692        );
693        Ok(())
694    }
695
696    #[test]
697    fn serialize_account_login_chatgpt() -> Result<()> {
698        let request = ClientRequest::LoginAccount {
699            request_id: RequestId::Integer(3),
700            params: v2::LoginAccountParams::Chatgpt,
701        };
702        assert_eq!(
703            json!({
704                "method": "account/login/start",
705                "id": 3,
706                "params": {
707                    "type": "chatgpt"
708                }
709            }),
710            serde_json::to_value(&request)?,
711        );
712        Ok(())
713    }
714
715    #[test]
716    fn serialize_account_logout() -> Result<()> {
717        let request = ClientRequest::LogoutAccount {
718            request_id: RequestId::Integer(4),
719            params: None,
720        };
721        assert_eq!(
722            json!({
723                "method": "account/logout",
724                "id": 4,
725            }),
726            serde_json::to_value(&request)?,
727        );
728        Ok(())
729    }
730
731    #[test]
732    fn serialize_get_account() -> Result<()> {
733        let request = ClientRequest::GetAccount {
734            request_id: RequestId::Integer(5),
735            params: v2::GetAccountParams {
736                refresh_token: false,
737            },
738        };
739        assert_eq!(
740            json!({
741                "method": "account/read",
742                "id": 5,
743                "params": {
744                    "refreshToken": false
745                }
746            }),
747            serde_json::to_value(&request)?,
748        );
749        Ok(())
750    }
751
752    #[test]
753    fn account_serializes_fields_in_camel_case() -> Result<()> {
754        let api_key = v2::Account::ApiKey {};
755        assert_eq!(
756            json!({
757                "type": "apiKey",
758            }),
759            serde_json::to_value(&api_key)?,
760        );
761
762        let chatgpt = v2::Account::Chatgpt {
763            email: "user@example.com".to_string(),
764            plan_type: PlanType::Plus,
765        };
766        assert_eq!(
767            json!({
768                "type": "chatgpt",
769                "email": "user@example.com",
770                "planType": "plus",
771            }),
772            serde_json::to_value(&chatgpt)?,
773        );
774
775        Ok(())
776    }
777
778    #[test]
779    fn serialize_list_models() -> Result<()> {
780        let request = ClientRequest::ModelList {
781            request_id: RequestId::Integer(6),
782            params: v2::ModelListParams::default(),
783        };
784        assert_eq!(
785            json!({
786                "method": "model/list",
787                "id": 6,
788                "params": {
789                    "limit": null,
790                    "cursor": null
791                }
792            }),
793            serde_json::to_value(&request)?,
794        );
795        Ok(())
796    }
797}