Skip to main content

cc_switch/codex/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// CodexConfiguration represents a Codex (OpenAI) API configuration
4///
5/// Supports two authentication modes:
6/// - "chatgpt": Uses OAuth tokens (id_token, access_token, refresh_token, account_id)
7/// - "apikey": Uses a direct OpenAI API key
8#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
9pub struct CodexConfiguration {
10    /// Unique alias name for this configuration
11    pub alias_name: String,
12    /// Authentication mode: "chatgpt" or "apikey"
13    pub auth_mode: String,
14    /// OpenAI API key (apikey mode)
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub openai_api_key: Option<String>,
17    /// OAuth ID token (chatgpt mode)
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub id_token: Option<String>,
20    /// OAuth access token (chatgpt mode)
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub access_token: Option<String>,
23    /// OAuth refresh token (chatgpt mode)
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub refresh_token: Option<String>,
26    /// Account ID (chatgpt mode)
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub account_id: Option<String>,
29    /// Last token refresh timestamp (chatgpt mode)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub last_refresh: Option<String>,
32}
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37
38    #[test]
39    fn test_serialize_apikey_mode() {
40        let config = CodexConfiguration {
41            alias_name: "my-apikey".to_string(),
42            auth_mode: "apikey".to_string(),
43            openai_api_key: Some("sk-abc123".to_string()),
44            id_token: None,
45            access_token: None,
46            refresh_token: None,
47            account_id: None,
48            last_refresh: None,
49        };
50
51        let json = serde_json::to_string(&config).unwrap();
52        assert!(json.contains("\"auth_mode\":\"apikey\""));
53        assert!(json.contains("\"openai_api_key\":\"sk-abc123\""));
54        // None fields should not appear
55        assert!(!json.contains("id_token"));
56        assert!(!json.contains("access_token"));
57    }
58
59    #[test]
60    fn test_deserialize_apikey_mode() {
61        let json = r#"{
62            "alias_name": "my-apikey",
63            "auth_mode": "apikey",
64            "openai_api_key": "sk-abc123"
65        }"#;
66
67        let config: CodexConfiguration = serde_json::from_str(json).unwrap();
68        assert_eq!(config.alias_name, "my-apikey");
69        assert_eq!(config.auth_mode, "apikey");
70        assert_eq!(config.openai_api_key, Some("sk-abc123".to_string()));
71        assert_eq!(config.id_token, None);
72        assert_eq!(config.access_token, None);
73    }
74
75    #[test]
76    fn test_serialize_chatgpt_mode() {
77        let config = CodexConfiguration {
78            alias_name: "my-chatgpt".to_string(),
79            auth_mode: "chatgpt".to_string(),
80            openai_api_key: None,
81            id_token: Some("id-xyz".to_string()),
82            access_token: Some("at-xyz".to_string()),
83            refresh_token: Some("rt-xyz".to_string()),
84            account_id: Some("acc-123".to_string()),
85            last_refresh: Some("2026-05-16T00:00:00Z".to_string()),
86        };
87
88        let json = serde_json::to_string(&config).unwrap();
89        assert!(json.contains("\"auth_mode\":\"chatgpt\""));
90        assert!(json.contains("\"id_token\":\"id-xyz\""));
91        assert!(json.contains("\"access_token\":\"at-xyz\""));
92        assert!(json.contains("\"refresh_token\":\"rt-xyz\""));
93        assert!(json.contains("\"account_id\":\"acc-123\""));
94        assert!(json.contains("\"last_refresh\""));
95        assert!(!json.contains("openai_api_key"));
96    }
97
98    #[test]
99    fn test_deserialize_chatgpt_mode() {
100        let json = r#"{
101            "alias_name": "my-chatgpt",
102            "auth_mode": "chatgpt",
103            "id_token": "id-xyz",
104            "access_token": "at-xyz",
105            "refresh_token": "rt-xyz",
106            "account_id": "acc-123",
107            "last_refresh": "2026-05-16T00:00:00Z"
108        }"#;
109
110        let config: CodexConfiguration = serde_json::from_str(json).unwrap();
111        assert_eq!(config.alias_name, "my-chatgpt");
112        assert_eq!(config.auth_mode, "chatgpt");
113        assert_eq!(config.id_token, Some("id-xyz".to_string()));
114        assert_eq!(config.access_token, Some("at-xyz".to_string()));
115        assert_eq!(config.refresh_token, Some("rt-xyz".to_string()));
116        assert_eq!(config.account_id, Some("acc-123".to_string()));
117        assert_eq!(
118            config.last_refresh,
119            Some("2026-05-16T00:00:00Z".to_string())
120        );
121        assert_eq!(config.openai_api_key, None);
122    }
123
124    #[test]
125    fn test_roundtrip_serialization() {
126        let config = CodexConfiguration {
127            alias_name: "roundtrip".to_string(),
128            auth_mode: "apikey".to_string(),
129            openai_api_key: Some("sk-roundtrip".to_string()),
130            id_token: None,
131            access_token: None,
132            refresh_token: None,
133            account_id: None,
134            last_refresh: None,
135        };
136
137        let json = serde_json::to_string(&config).unwrap();
138        let deserialized: CodexConfiguration = serde_json::from_str(&json).unwrap();
139        assert_eq!(config, deserialized);
140    }
141}