Skip to main content

codex_helper_core/
codex_patch_plan.rs

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    /// Keep the historical codex-helper patch behavior.
8    #[default]
9    Default,
10    /// Keep Codex/ChatGPT account auth for app/mobile features while model traffic goes through
11    /// codex-helper.
12    ChatGptBridge,
13    /// Use a minimal ChatGPT-looking auth facade to expose Codex hosted image generation while
14    /// request credentials still come from codex-helper routing/upstream configuration.
15    ImagegenBridge,
16    /// Advertise the local relay as the official OpenAI Responses provider so Codex can use
17    /// first-party HTTP features that helper can safely forward, starting with remote compaction
18    /// v1. Request credentials still come from codex-helper routing/upstream configuration.
19    #[serde(alias = "official-relay", alias = "official_relay")]
20    OfficialRelayBridge,
21    /// Combine official relay provider identity for remote compaction with the image generation
22    /// ChatGPT auth facade. Request credentials still come from codex-helper routing/upstream
23    /// configuration.
24    #[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    /// Keep the preset-derived behavior and do not rewrite Codex's existing
85    /// `remote_compaction_v2` feature flag.
86    #[default]
87    Auto,
88    /// Force Codex to see a helper-shaped provider so the client chooses local compaction.
89    Local,
90    /// Force remote compaction v1 through `/responses/compact`.
91    #[serde(alias = "remote_v1")]
92    RemoteV1,
93    /// Force remote compaction v2 through `/responses` with `compaction_trigger`.
94    #[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    /// Keep Codex's own feature flag and request tools unchanged.
145    #[default]
146    Auto,
147    /// Explicitly enable Codex hosted image generation in the client config.
148    Enabled,
149    /// Hide Codex hosted image generation from the client config and outgoing Responses requests.
150    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    /// Advertise `model_providers.codex_proxy.supports_websockets = true` so Codex may choose
188    /// Responses WebSocket transport. This is intentionally separate from `CodexPatchMode` to
189    /// avoid mode-combination explosion.
190    #[serde(default, skip_serializing_if = "bool_is_false")]
191    pub responses_websocket: bool,
192    /// Choose Codex's compaction path without adding more client presets.
193    #[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}