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)]
82pub struct CodexSwitchOptions {
83    /// Advertise `model_providers.codex_proxy.supports_websockets = true` so Codex may choose
84    /// Responses WebSocket transport. This is intentionally separate from `CodexPatchMode` to
85    /// avoid mode-combination explosion.
86    #[serde(default, skip_serializing_if = "bool_is_false")]
87    pub responses_websocket: bool,
88}
89
90impl CodexSwitchOptions {
91    pub fn validate_for_mode(self, mode: CodexPatchMode) -> Result<()> {
92        if self.responses_websocket && !mode.enables_official_relay_features() {
93            return Err(anyhow!(
94                "Responses WebSocket transport currently requires --preset official-relay or --preset official-imagegen"
95            ));
96        }
97        Ok(())
98    }
99}
100
101fn bool_is_false(value: &bool) -> bool {
102    !*value
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum CodexPatchProviderIdentity {
107    HelperRelay,
108    OfficialOpenAi,
109}
110
111impl CodexPatchProviderIdentity {
112    pub fn provider_name(self) -> &'static str {
113        match self {
114            Self::HelperRelay => "codex-helper",
115            Self::OfficialOpenAi => "OpenAI",
116        }
117    }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum CodexTomlBoolPatch {
122    Remove,
123    Set(bool),
124}
125
126impl CodexTomlBoolPatch {
127    pub fn value(self) -> Option<bool> {
128        match self {
129            Self::Remove => None,
130            Self::Set(value) => Some(value),
131        }
132    }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct CodexProviderPatchPlan {
137    identity: CodexPatchProviderIdentity,
138    requires_openai_auth: CodexTomlBoolPatch,
139    supports_websockets: CodexTomlBoolPatch,
140}
141
142impl CodexProviderPatchPlan {
143    pub fn identity(self) -> CodexPatchProviderIdentity {
144        self.identity
145    }
146
147    pub fn provider_name(self) -> &'static str {
148        self.identity.provider_name()
149    }
150
151    pub fn requires_openai_auth(self) -> CodexTomlBoolPatch {
152        self.requires_openai_auth
153    }
154
155    pub fn supports_websockets(self) -> CodexTomlBoolPatch {
156        self.supports_websockets
157    }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum CodexAuthPatchPlan {
162    RestoreOriginalIfHelperPatched,
163    PatchChatGptBridge,
164    PatchImagegenFacade,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum CodexSwitchOnEffectOrder {
169    ConfigAuthState,
170    StateConfigAuth,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub struct CodexPatchPlan {
175    mode: CodexPatchMode,
176    options: CodexSwitchOptions,
177    provider: CodexProviderPatchPlan,
178    auth: CodexAuthPatchPlan,
179    effect_order: CodexSwitchOnEffectOrder,
180}
181
182impl CodexPatchPlan {
183    pub fn for_switch_on(mode: CodexPatchMode, options: CodexSwitchOptions) -> Result<Self> {
184        options.validate_for_mode(mode)?;
185
186        let provider = CodexProviderPatchPlan {
187            identity: if mode.enables_official_relay_features() {
188                CodexPatchProviderIdentity::OfficialOpenAi
189            } else {
190                CodexPatchProviderIdentity::HelperRelay
191            },
192            requires_openai_auth: match mode {
193                CodexPatchMode::ChatGptBridge => CodexTomlBoolPatch::Set(true),
194                CodexPatchMode::Default
195                | CodexPatchMode::ImagegenBridge
196                | CodexPatchMode::OfficialRelayBridge
197                | CodexPatchMode::OfficialImagegenBridge => CodexTomlBoolPatch::Remove,
198            },
199            supports_websockets: match mode {
200                CodexPatchMode::Default | CodexPatchMode::ImagegenBridge => {
201                    CodexTomlBoolPatch::Remove
202                }
203                CodexPatchMode::ChatGptBridge => CodexTomlBoolPatch::Set(false),
204                CodexPatchMode::OfficialRelayBridge | CodexPatchMode::OfficialImagegenBridge => {
205                    CodexTomlBoolPatch::Set(options.responses_websocket)
206                }
207            },
208        };
209
210        let auth = match mode {
211            CodexPatchMode::Default | CodexPatchMode::OfficialRelayBridge => {
212                CodexAuthPatchPlan::RestoreOriginalIfHelperPatched
213            }
214            CodexPatchMode::ChatGptBridge => CodexAuthPatchPlan::PatchChatGptBridge,
215            CodexPatchMode::ImagegenBridge | CodexPatchMode::OfficialImagegenBridge => {
216                CodexAuthPatchPlan::PatchImagegenFacade
217            }
218        };
219
220        let effect_order = match auth {
221            CodexAuthPatchPlan::RestoreOriginalIfHelperPatched => {
222                CodexSwitchOnEffectOrder::ConfigAuthState
223            }
224            CodexAuthPatchPlan::PatchChatGptBridge | CodexAuthPatchPlan::PatchImagegenFacade => {
225                CodexSwitchOnEffectOrder::StateConfigAuth
226            }
227        };
228
229        Ok(Self {
230            mode,
231            options,
232            provider,
233            auth,
234            effect_order,
235        })
236    }
237
238    pub fn mode(self) -> CodexPatchMode {
239        self.mode
240    }
241
242    pub fn options(self) -> CodexSwitchOptions {
243        self.options
244    }
245
246    pub fn provider(self) -> CodexProviderPatchPlan {
247        self.provider
248    }
249
250    pub fn auth(self) -> CodexAuthPatchPlan {
251        self.auth
252    }
253
254    pub fn effect_order(self) -> CodexSwitchOnEffectOrder {
255        self.effect_order
256    }
257
258    pub fn requires_bridge_runtime_ready(self) -> bool {
259        self.mode.strips_codex_client_auth() && self.mode != CodexPatchMode::ChatGptBridge
260    }
261}