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