1use anyhow::{Result, anyhow};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
5#[serde(rename_all = "kebab-case")]
6pub enum CodexPatchMode {
7 #[default]
9 Default,
10 ChatGptBridge,
13 ImagegenBridge,
16 #[serde(
20 rename = "official-relay",
21 alias = "official_relay",
22 alias = "official-relay-bridge",
23 alias = "official_relay_bridge"
24 )]
25 OfficialRelayBridge,
26 #[serde(
30 rename = "official-imagegen",
31 alias = "official_imagegen",
32 alias = "official-imagegen-bridge",
33 alias = "official_imagegen_bridge"
34 )]
35 OfficialImagegenBridge,
36}
37
38impl CodexPatchMode {
39 pub fn as_str(self) -> &'static str {
40 match self {
41 Self::Default => "default",
42 Self::ChatGptBridge => "chatgpt-bridge",
43 Self::ImagegenBridge => "imagegen-bridge",
44 Self::OfficialRelayBridge => "official-relay",
45 Self::OfficialImagegenBridge => "official-imagegen",
46 }
47 }
48
49 pub fn as_preset_str(self) -> &'static str {
50 match self {
51 Self::Default => "default",
52 Self::ChatGptBridge => "chatgpt-bridge",
53 Self::ImagegenBridge => "imagegen-bridge",
54 Self::OfficialRelayBridge => "official-relay",
55 Self::OfficialImagegenBridge => "official-imagegen",
56 }
57 }
58
59 pub fn is_default(self) -> bool {
60 matches!(self, Self::Default)
61 }
62
63 pub fn strips_codex_client_auth(self) -> bool {
64 matches!(
65 self,
66 Self::ChatGptBridge
67 | Self::ImagegenBridge
68 | Self::OfficialRelayBridge
69 | Self::OfficialImagegenBridge
70 )
71 }
72
73 pub fn enables_official_relay_features(self) -> bool {
74 matches!(
75 self,
76 Self::OfficialRelayBridge | Self::OfficialImagegenBridge
77 )
78 }
79
80 pub fn enables_imagegen_facade(self) -> bool {
81 matches!(self, Self::ImagegenBridge | Self::OfficialImagegenBridge)
82 }
83}
84
85impl std::fmt::Display for CodexPatchMode {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.write_str(self.as_str())
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
92#[serde(rename_all = "kebab-case")]
93pub enum CodexCompactionStrategy {
94 #[default]
97 Auto,
98 Local,
100 #[serde(alias = "remote_v1")]
102 RemoteV1,
103 #[serde(alias = "remote_v2")]
105 RemoteV2,
106}
107
108impl CodexCompactionStrategy {
109 pub fn as_str(self) -> &'static str {
110 match self {
111 Self::Auto => "auto",
112 Self::Local => "local",
113 Self::RemoteV1 => "remote-v1",
114 Self::RemoteV2 => "remote-v2",
115 }
116 }
117
118 pub fn is_auto(&self) -> bool {
119 matches!(self, Self::Auto)
120 }
121
122 pub fn provider_identity_for_mode(self, mode: CodexPatchMode) -> CodexPatchProviderIdentity {
123 match self {
124 Self::Auto => {
125 if mode.enables_official_relay_features() {
126 CodexPatchProviderIdentity::OfficialOpenAi
127 } else {
128 CodexPatchProviderIdentity::HelperRelay
129 }
130 }
131 Self::Local => CodexPatchProviderIdentity::HelperRelay,
132 Self::RemoteV1 | Self::RemoteV2 => CodexPatchProviderIdentity::OfficialOpenAi,
133 }
134 }
135
136 pub fn remote_compaction_v2_feature_patch(self) -> CodexFeatureBoolPatch {
137 match self {
138 Self::Auto => CodexFeatureBoolPatch::Preserve,
139 Self::Local | Self::RemoteV1 => CodexFeatureBoolPatch::Set(false),
140 Self::RemoteV2 => CodexFeatureBoolPatch::Set(true),
141 }
142 }
143}
144
145impl std::fmt::Display for CodexCompactionStrategy {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.write_str(self.as_str())
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
152#[serde(rename_all = "kebab-case")]
153pub enum CodexHostedImageGenerationMode {
154 #[default]
156 Auto,
157 Enabled,
159 Disabled,
161}
162
163impl CodexHostedImageGenerationMode {
164 pub fn as_str(self) -> &'static str {
165 match self {
166 Self::Auto => "auto",
167 Self::Enabled => "enabled",
168 Self::Disabled => "disabled",
169 }
170 }
171
172 pub fn is_auto(&self) -> bool {
173 matches!(self, Self::Auto)
174 }
175
176 pub fn feature_patch(self) -> CodexFeatureBoolPatch {
177 match self {
178 Self::Auto => CodexFeatureBoolPatch::Preserve,
179 Self::Enabled => CodexFeatureBoolPatch::Set(true),
180 Self::Disabled => CodexFeatureBoolPatch::Set(false),
181 }
182 }
183
184 pub fn filters_request_tools(self) -> bool {
185 matches!(self, Self::Disabled)
186 }
187}
188
189impl std::fmt::Display for CodexHostedImageGenerationMode {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 f.write_str(self.as_str())
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
196pub struct CodexSwitchOptions {
197 #[serde(default, skip_serializing_if = "bool_is_false")]
201 pub responses_websocket: bool,
202 #[serde(default, skip_serializing_if = "CodexCompactionStrategy::is_auto")]
204 pub compaction: CodexCompactionStrategy,
205}
206
207impl CodexSwitchOptions {
208 pub fn validate_for_mode(self, mode: CodexPatchMode) -> Result<()> {
209 if matches!(
210 self.compaction,
211 CodexCompactionStrategy::RemoteV1 | CodexCompactionStrategy::RemoteV2
212 ) && !mode.enables_official_relay_features()
213 {
214 return Err(anyhow!(
215 "remote compaction strategies require --preset official-relay or --preset official-imagegen"
216 ));
217 }
218
219 let provider_identity = self.compaction.provider_identity_for_mode(mode);
220 if self.responses_websocket
221 && provider_identity != CodexPatchProviderIdentity::OfficialOpenAi
222 {
223 return Err(anyhow!(
224 "Responses WebSocket transport requires remote compaction identity; use --preset official-relay or --preset official-imagegen without --compaction local"
225 ));
226 }
227 Ok(())
228 }
229}
230
231fn bool_is_false(value: &bool) -> bool {
232 !*value
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum CodexPatchProviderIdentity {
237 HelperRelay,
238 OfficialOpenAi,
239}
240
241impl CodexPatchProviderIdentity {
242 pub fn provider_name(self) -> &'static str {
243 match self {
244 Self::HelperRelay => "codex-helper",
245 Self::OfficialOpenAi => "OpenAI",
246 }
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum CodexTomlBoolPatch {
252 Remove,
253 Set(bool),
254}
255
256impl CodexTomlBoolPatch {
257 pub fn value(self) -> Option<bool> {
258 match self {
259 Self::Remove => None,
260 Self::Set(value) => Some(value),
261 }
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum CodexFeatureBoolPatch {
267 Preserve,
268 Set(bool),
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub struct CodexProviderPatchPlan {
273 identity: CodexPatchProviderIdentity,
274 requires_openai_auth: CodexTomlBoolPatch,
275 supports_websockets: CodexTomlBoolPatch,
276}
277
278impl CodexProviderPatchPlan {
279 pub fn identity(self) -> CodexPatchProviderIdentity {
280 self.identity
281 }
282
283 pub fn provider_name(self) -> &'static str {
284 self.identity.provider_name()
285 }
286
287 pub fn requires_openai_auth(self) -> CodexTomlBoolPatch {
288 self.requires_openai_auth
289 }
290
291 pub fn supports_websockets(self) -> CodexTomlBoolPatch {
292 self.supports_websockets
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum CodexAuthPatchPlan {
298 RestoreOriginalIfHelperPatched,
299 PatchChatGptBridge,
300 PatchImagegenFacade,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum CodexSwitchOnEffectOrder {
305 ConfigAuthState,
306 StateConfigAuth,
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub struct CodexPatchPlan {
311 mode: CodexPatchMode,
312 options: CodexSwitchOptions,
313 provider: CodexProviderPatchPlan,
314 remote_compaction_v2: CodexFeatureBoolPatch,
315 auth: CodexAuthPatchPlan,
316 effect_order: CodexSwitchOnEffectOrder,
317}
318
319impl CodexPatchPlan {
320 pub fn for_switch_on(mode: CodexPatchMode, options: CodexSwitchOptions) -> Result<Self> {
321 options.validate_for_mode(mode)?;
322
323 let provider = CodexProviderPatchPlan {
324 identity: options.compaction.provider_identity_for_mode(mode),
325 requires_openai_auth: match mode {
326 CodexPatchMode::ChatGptBridge => CodexTomlBoolPatch::Set(true),
327 CodexPatchMode::Default
328 | CodexPatchMode::ImagegenBridge
329 | CodexPatchMode::OfficialRelayBridge
330 | CodexPatchMode::OfficialImagegenBridge => CodexTomlBoolPatch::Remove,
331 },
332 supports_websockets: match mode {
333 CodexPatchMode::Default | CodexPatchMode::ImagegenBridge => {
334 CodexTomlBoolPatch::Remove
335 }
336 CodexPatchMode::ChatGptBridge => CodexTomlBoolPatch::Set(false),
337 CodexPatchMode::OfficialRelayBridge | CodexPatchMode::OfficialImagegenBridge => {
338 CodexTomlBoolPatch::Set(options.responses_websocket)
339 }
340 },
341 };
342
343 let auth = match mode {
344 CodexPatchMode::Default | CodexPatchMode::OfficialRelayBridge => {
345 CodexAuthPatchPlan::RestoreOriginalIfHelperPatched
346 }
347 CodexPatchMode::ChatGptBridge => CodexAuthPatchPlan::PatchChatGptBridge,
348 CodexPatchMode::ImagegenBridge | CodexPatchMode::OfficialImagegenBridge => {
349 CodexAuthPatchPlan::PatchImagegenFacade
350 }
351 };
352
353 let effect_order = match auth {
354 CodexAuthPatchPlan::RestoreOriginalIfHelperPatched => {
355 CodexSwitchOnEffectOrder::ConfigAuthState
356 }
357 CodexAuthPatchPlan::PatchChatGptBridge | CodexAuthPatchPlan::PatchImagegenFacade => {
358 CodexSwitchOnEffectOrder::StateConfigAuth
359 }
360 };
361
362 Ok(Self {
363 mode,
364 options,
365 provider,
366 remote_compaction_v2: options.compaction.remote_compaction_v2_feature_patch(),
367 auth,
368 effect_order,
369 })
370 }
371
372 pub fn mode(self) -> CodexPatchMode {
373 self.mode
374 }
375
376 pub fn options(self) -> CodexSwitchOptions {
377 self.options
378 }
379
380 pub fn provider(self) -> CodexProviderPatchPlan {
381 self.provider
382 }
383
384 pub fn remote_compaction_v2_feature(self) -> CodexFeatureBoolPatch {
385 self.remote_compaction_v2
386 }
387
388 pub fn auth(self) -> CodexAuthPatchPlan {
389 self.auth
390 }
391
392 pub fn effect_order(self) -> CodexSwitchOnEffectOrder {
393 self.effect_order
394 }
395
396 pub fn requires_bridge_runtime_ready(self) -> bool {
397 self.mode.strips_codex_client_auth() && self.mode != CodexPatchMode::ChatGptBridge
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn codex_patch_mode_serializes_current_official_preset_names() {
407 assert_eq!(
408 serde_json::to_string(&CodexPatchMode::OfficialRelayBridge).unwrap(),
409 "\"official-relay\""
410 );
411 assert_eq!(
412 serde_json::to_string(&CodexPatchMode::OfficialImagegenBridge).unwrap(),
413 "\"official-imagegen\""
414 );
415 }
416
417 #[test]
418 fn codex_patch_mode_reads_legacy_official_bridge_state() {
419 assert_eq!(
420 serde_json::from_str::<CodexPatchMode>("\"official-relay-bridge\"").unwrap(),
421 CodexPatchMode::OfficialRelayBridge
422 );
423 assert_eq!(
424 serde_json::from_str::<CodexPatchMode>("\"official-imagegen-bridge\"").unwrap(),
425 CodexPatchMode::OfficialImagegenBridge
426 );
427 }
428}