use snafu::{ResultExt, Snafu};
use toml_edit::{Document, Item, Table, value};
use crate::launch::{CODEX_API_KEY_ENV, CodexAuth};
const WIRE_API: &str = "responses";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodexProviderPatch {
provider_id: String,
display_name: String,
base_url: String,
auth: CodexAuth,
attribution_header: Option<String>,
env_key_instructions: Option<String>,
scrub_env_overrides: Vec<String>,
}
impl CodexProviderPatch {
pub fn new(
provider_id: impl Into<String>,
display_name: impl Into<String>,
base_url: impl Into<String>,
auth: CodexAuth,
) -> Self {
Self {
provider_id: provider_id.into(),
display_name: display_name.into(),
base_url: base_url.into(),
auth,
attribution_header: None,
env_key_instructions: None,
scrub_env_overrides: Vec::new(),
}
}
pub fn with_attribution_header(mut self, header: impl Into<String>) -> Self {
self.attribution_header = Some(header.into());
self
}
pub fn with_env_key_instructions(mut self, instructions: impl Into<String>) -> Self {
self.env_key_instructions = Some(instructions.into());
self
}
pub fn with_scrubbed_env_override(mut self, name: impl Into<String>) -> Self {
self.scrub_env_overrides.push(name.into());
self
}
}
pub fn apply_provider(
config_text: &str,
patch: &CodexProviderPatch,
) -> Result<String, CodexConfigError> {
let provider_id = require_provider_id(&patch.provider_id)?;
let mut doc = parse(config_text)?;
doc["model_provider"] = value(provider_id);
{
let providers = ensure_table(doc.as_table_mut(), "model_providers", "model_providers")?;
let provider = ensure_table(
providers,
provider_id,
&format!("model_providers.{provider_id}"),
)?;
provider["name"] = value(&patch.display_name);
provider["base_url"] = value(&patch.base_url);
provider["wire_api"] = value(WIRE_API);
scrub_stale_attribution_headers(provider, provider_id, patch.attribution_header.as_deref());
if let Some(header) = &patch.attribution_header {
let headers = ensure_table(
provider,
"http_headers",
&format!("model_providers.{provider_id}.http_headers"),
)?;
headers[header.as_str()] = value(provider_id);
}
match patch.auth {
CodexAuth::ChatGpt => {
provider["requires_openai_auth"] = value(true);
provider.remove("env_key");
provider.remove("env_key_instructions");
}
CodexAuth::ApiKey => {
provider["env_key"] = value(CODEX_API_KEY_ENV);
match &patch.env_key_instructions {
Some(instructions) => {
provider["env_key_instructions"] = value(instructions);
}
None => {
provider.remove("env_key_instructions");
}
}
provider.remove("requires_openai_auth");
}
}
}
let features = ensure_table(doc.as_table_mut(), "features", "features")?;
features["enable_request_compression"] = value(false);
scrub_env_overrides(&mut doc, &patch.scrub_env_overrides);
Ok(doc.to_string())
}
pub fn remove_provider(config_text: &str, provider_id: &str) -> Result<String, CodexConfigError> {
let provider_id = require_provider_id(provider_id)?;
let mut doc = parse(config_text)?;
let selected = doc
.get("model_provider")
.and_then(Item::as_str)
.is_some_and(|selected| selected == provider_id);
let declared = doc
.get("model_providers")
.and_then(Item::as_table_like)
.is_some_and(|providers| providers.contains_key(provider_id));
if !selected && !declared {
return Ok(config_text.to_string());
}
if selected {
doc.as_table_mut().remove("model_provider");
}
if declared {
let emptied = doc
.as_table_mut()
.get_mut("model_providers")
.and_then(Item::as_table_like_mut)
.map(|providers| {
providers.remove(provider_id);
providers.is_empty()
});
if emptied == Some(true) {
doc.as_table_mut().remove("model_providers");
}
}
Ok(doc.to_string())
}
pub fn is_provider_applied(
config_text: &str,
patch: &CodexProviderPatch,
) -> Result<bool, CodexConfigError> {
Ok(apply_provider(config_text, patch)? == config_text)
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct InstalledCodexProvider {
pub name: Option<String>,
pub base_url: Option<String>,
pub wire_api: Option<String>,
pub env_key: Option<String>,
pub requires_openai_auth: Option<bool>,
}
pub fn installed_provider(
config_text: &str,
provider_id: &str,
) -> Result<Option<InstalledCodexProvider>, CodexConfigError> {
let provider_id = require_provider_id(provider_id)?;
let doc = parse(config_text)?;
let Some(provider) = doc
.get("model_providers")
.and_then(Item::as_table_like)
.and_then(|providers| providers.get(provider_id))
.and_then(Item::as_table_like)
else {
return Ok(None);
};
let read = |key: &str| provider.get(key).and_then(Item::as_str).map(str::to_string);
Ok(Some(InstalledCodexProvider {
name: read("name"),
base_url: read("base_url"),
wire_api: read("wire_api"),
env_key: read("env_key"),
requires_openai_auth: provider.get("requires_openai_auth").and_then(Item::as_bool),
}))
}
#[derive(Debug, Snafu)]
#[snafu(module, visibility(pub(crate)))]
#[non_exhaustive]
pub enum CodexConfigError {
#[snafu(display("codex provider id must not be empty"))]
EmptyProviderId,
#[snafu(display("could not parse codex config.toml"))]
Parse {
source: toml_edit::TomlError,
},
#[snafu(display("codex config key `{key}` is not a table"))]
NotATable {
key: String,
},
}
fn require_provider_id(provider_id: &str) -> Result<&str, CodexConfigError> {
if provider_id.trim().is_empty() {
return codex_config_error::EmptyProviderIdSnafu.fail();
}
Ok(provider_id)
}
fn parse(config_text: &str) -> Result<Document, CodexConfigError> {
if config_text.trim().is_empty() {
return Ok(Document::new());
}
config_text
.parse::<Document>()
.context(codex_config_error::ParseSnafu)
}
fn ensure_table<'a>(
parent: &'a mut Table,
key: &str,
display_key: &str,
) -> Result<&'a mut Table, CodexConfigError> {
if !parent.contains_key(key) {
parent[key] = Item::Table(Table::new());
}
parent[key]
.as_table_mut()
.ok_or_else(|| CodexConfigError::NotATable {
key: display_key.to_string(),
})
}
fn scrub_stale_attribution_headers(
provider: &mut Table,
provider_id: &str,
current_header: Option<&str>,
) {
let Some(headers) = provider
.get_mut("http_headers")
.and_then(Item::as_table_like_mut)
else {
return;
};
let stale: Vec<String> = headers
.iter()
.filter(|(name, entry)| {
entry.as_str() == Some(provider_id) && Some(*name) != current_header
})
.map(|(name, _)| name.to_string())
.collect();
if stale.is_empty() {
return;
}
for name in &stale {
headers.remove(name);
}
if headers.is_empty() && current_header.is_none() {
provider.remove("http_headers");
}
}
fn scrub_env_overrides(doc: &mut Document, names: &[String]) {
if names.is_empty() {
return;
}
let Some(policy) = doc
.as_table_mut()
.get_mut("shell_environment_policy")
.and_then(Item::as_table_like_mut)
else {
return;
};
let Some(overrides) = policy.get_mut("set").and_then(Item::as_table_like_mut) else {
return;
};
for name in names {
overrides.remove(name);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn chatgpt_patch() -> CodexProviderPatch {
CodexProviderPatch::new(
"acme-openai",
"Acme OpenAI",
"http://127.0.0.1:51539/v1/openai-chatgpt/chatgpt-codex",
CodexAuth::ChatGpt,
)
.with_attribution_header("X-Acme-Codex-Attribution")
.with_scrubbed_env_override("ACME_EXECUTABLE")
}
fn api_key_patch() -> CodexProviderPatch {
CodexProviderPatch::new(
"acme-openai",
"Acme OpenAI",
"http://127.0.0.1:51539/v1/openai-responses/openai-transparent/v1",
CodexAuth::ApiKey,
)
.with_attribution_header("X-Acme-Codex-Attribution")
.with_env_key_instructions("Set OPENAI_API_KEY to an OpenAI API key.")
.with_scrubbed_env_override("ACME_EXECUTABLE")
}
const APPLIED_TO_EMPTY: &str = r#"model_provider = "acme-openai"
[model_providers]
[model_providers.acme-openai]
name = "Acme OpenAI"
base_url = "http://127.0.0.1:51539/v1/openai-chatgpt/chatgpt-codex"
wire_api = "responses"
requires_openai_auth = true
[model_providers.acme-openai.http_headers]
X-Acme-Codex-Attribution = "acme-openai"
[features]
enable_request_compression = false
"#;
#[test]
fn applying_to_an_empty_config_produces_the_canonical_fixture() {
for empty in ["", " \n\t\n"] {
assert_eq!(
apply_provider(empty, &chatgpt_patch()).unwrap(),
APPLIED_TO_EMPTY,
);
}
}
#[test]
fn applying_preserves_user_content_and_comments() {
let existing = r#"# Managed by hand — do not lose this comment.
model = "gpt-5-codex"
approval_policy = "on-request" # trailing comment
[tools]
web_search = true
"#;
let applied = apply_provider(existing, &chatgpt_patch()).unwrap();
assert_eq!(
applied,
r#"# Managed by hand — do not lose this comment.
model = "gpt-5-codex"
approval_policy = "on-request" # trailing comment
model_provider = "acme-openai"
[tools]
web_search = true
[model_providers]
[model_providers.acme-openai]
name = "Acme OpenAI"
base_url = "http://127.0.0.1:51539/v1/openai-chatgpt/chatgpt-codex"
wire_api = "responses"
requires_openai_auth = true
[model_providers.acme-openai.http_headers]
X-Acme-Codex-Attribution = "acme-openai"
[features]
enable_request_compression = false
"#,
);
}
#[test]
fn applying_twice_is_a_byte_level_no_op_and_the_probe_agrees() {
let patch = chatgpt_patch();
assert!(!is_provider_applied("", &patch).unwrap());
let once = apply_provider("", &patch).unwrap();
assert!(is_provider_applied(&once, &patch).unwrap());
assert_eq!(apply_provider(&once, &patch).unwrap(), once);
}
#[test]
fn apply_then_remove_round_trips_user_content_with_documented_residue() {
let existing = r#"# Keep me.
model = "gpt-5-codex"
[tools]
web_search = true
"#;
let applied = apply_provider(existing, &chatgpt_patch()).unwrap();
let removed = remove_provider(&applied, "acme-openai").unwrap();
assert_eq!(
removed,
r#"# Keep me.
model = "gpt-5-codex"
[tools]
web_search = true
[features]
enable_request_compression = false
"#,
);
assert_eq!(remove_provider(&removed, "acme-openai").unwrap(), removed);
}
#[test]
fn applying_over_a_stale_provider_updates_it_in_place() {
let existing = r#"model_provider = "acme-openai"
[model_providers.acme-openai]
# The user annotated our table; the comment must survive.
name = "Old Name"
base_url = "http://old.invalid"
wire_api = "responses"
requires_openai_auth = true
[features]
enable_request_compression = false
"#;
let applied = apply_provider(existing, &api_key_patch()).unwrap();
assert_eq!(
applied,
r#"model_provider = "acme-openai"
[model_providers.acme-openai]
# The user annotated our table; the comment must survive.
name = "Acme OpenAI"
base_url = "http://127.0.0.1:51539/v1/openai-responses/openai-transparent/v1"
wire_api = "responses"
env_key = "OPENAI_API_KEY"
env_key_instructions = "Set OPENAI_API_KEY to an OpenAI API key."
[model_providers.acme-openai.http_headers]
X-Acme-Codex-Attribution = "acme-openai"
[features]
enable_request_compression = false
"#,
);
}
#[test]
fn auth_modes_swap_credential_keys_in_both_directions() {
let chatgpt = apply_provider("", &chatgpt_patch()).unwrap();
assert!(chatgpt.contains("requires_openai_auth = true"));
assert!(!chatgpt.contains("env_key"), "{chatgpt}");
let to_api_key = apply_provider(&chatgpt, &api_key_patch()).unwrap();
assert!(to_api_key.contains("env_key = \"OPENAI_API_KEY\""));
assert!(!to_api_key.contains("requires_openai_auth"), "{to_api_key}");
let back = apply_provider(&to_api_key, &chatgpt_patch()).unwrap();
assert!(back.contains("requires_openai_auth = true"));
assert!(!back.contains("env_key"), "{back}");
}
#[test]
fn unset_env_key_instructions_are_removed_from_a_prior_install() {
let with_instructions = apply_provider("", &api_key_patch()).unwrap();
let patch_without = CodexProviderPatch::new(
"acme-openai",
"Acme OpenAI",
"http://127.0.0.1:51539/v1/openai-responses/openai-transparent/v1",
CodexAuth::ApiKey,
);
let applied = apply_provider(&with_instructions, &patch_without).unwrap();
assert!(!applied.contains("env_key_instructions"), "{applied}");
assert!(is_provider_applied(&applied, &patch_without).unwrap());
}
#[test]
fn scrubbed_env_overrides_are_removed_and_the_users_are_preserved() {
let existing = r#"
[shell_environment_policy]
set = { EXISTING_FLAG = "keep-me", ACME_EXECUTABLE = "/obsolete/acme" }
"#;
let applied = apply_provider(existing, &chatgpt_patch()).unwrap();
assert!(applied.contains("EXISTING_FLAG = \"keep-me\""));
assert!(!applied.contains("ACME_EXECUTABLE"), "{applied}");
}
#[test]
fn renaming_the_attribution_header_retires_the_old_entry() {
let first = apply_provider("", &chatgpt_patch()).unwrap();
assert!(first.contains(r#"X-Acme-Codex-Attribution = "acme-openai""#));
let renamed = CodexProviderPatch::new(
"acme-openai",
"Acme OpenAI",
"http://127.0.0.1:51539/v1/openai-chatgpt/chatgpt-codex",
CodexAuth::ChatGpt,
)
.with_attribution_header("X-Acme-Attribution")
.with_scrubbed_env_override("ACME_EXECUTABLE");
let second = apply_provider(&first, &renamed).unwrap();
assert!(
!second.contains("X-Acme-Codex-Attribution"),
"the stale header accumulated:\n{second}"
);
assert_eq!(
second.matches(r#" = "acme-openai""#).count(),
2, "{second}"
);
assert!(second.contains(r#"X-Acme-Attribution = "acme-openai""#));
assert!(is_provider_applied(&second, &renamed).unwrap());
}
#[test]
fn user_headers_survive_unless_they_wear_the_grammars_own_shape() {
let existing = r#"[model_providers.acme-openai]
name = "Acme OpenAI"
base_url = "http://127.0.0.1:51539/v1/openai-chatgpt/chatgpt-codex"
wire_api = "responses"
requires_openai_auth = true
[model_providers.acme-openai.http_headers]
X-User-Extra = "user-value"
X-Left-Over = "acme-openai"
"#;
let applied = apply_provider(existing, &chatgpt_patch()).unwrap();
assert!(
applied.contains(r#"X-User-Extra = "user-value""#),
"{applied}"
);
assert!(!applied.contains("X-Left-Over"), "{applied}");
assert!(applied.contains(r#"X-Acme-Codex-Attribution = "acme-openai""#));
assert!(is_provider_applied(&applied, &chatgpt_patch()).unwrap());
}
#[test]
fn dropping_the_attribution_header_removes_the_managed_entry_and_its_container() {
let headerless = CodexProviderPatch::new(
"acme-openai",
"Acme OpenAI",
"http://127.0.0.1:51539/v1/openai-chatgpt/chatgpt-codex",
CodexAuth::ChatGpt,
);
let installed = apply_provider("", &chatgpt_patch()).unwrap();
let applied = apply_provider(&installed, &headerless).unwrap();
assert!(!applied.contains("http_headers"), "{applied}");
assert!(is_provider_applied(&applied, &headerless).unwrap());
let mixed = apply_provider(
"[model_providers.acme-openai.http_headers]\nX-User-Extra = \"user-value\"\n",
&chatgpt_patch(),
)
.unwrap();
let applied = apply_provider(&mixed, &headerless).unwrap();
assert!(applied.contains("[model_providers.acme-openai.http_headers]"));
assert!(applied.contains(r#"X-User-Extra = "user-value""#));
assert!(!applied.contains("X-Acme-Codex-Attribution"), "{applied}");
assert!(is_provider_applied(&applied, &headerless).unwrap());
}
#[test]
fn adversarial_valid_toml_survives_untouched() {
let existing = r#"model = "gpt-5-codex"
notify = ["afplay", 'C:\literal\no-escapes.wav']
"weird key.with dots" = { nested = "inline" }
[projects."/Users/x/dev with spaces"]
trust_level = "trusted"
[[mcp_servers]]
name = "docs"
instructions = """
multi
line
"""
[model_providers]
other = { name = "Someone Else's", base_url = "http://other.example" }
[model_providers.acme-openai]
name = "stale"
base_url = "http://stale.invalid"
wire_api = "responses"
requires_openai_auth = true
[shell_environment_policy.set]
KEEP_UNICODE = "café ☕"
"#;
let applied = apply_provider(existing, &chatgpt_patch()).unwrap();
for preserved in [
r#"notify = ["afplay", 'C:\literal\no-escapes.wav']"#,
r#""weird key.with dots" = { nested = "inline" }"#,
r#"[projects."/Users/x/dev with spaces"]"#,
"[[mcp_servers]]",
"multi\nline",
r#"other = { name = "Someone Else's", base_url = "http://other.example" }"#,
r#"KEEP_UNICODE = "café ☕""#,
] {
assert!(
applied.contains(preserved),
"lost {preserved:?} in:\n{applied}"
);
}
assert!(!applied.contains("http://stale.invalid"), "{applied}");
assert!(is_provider_applied(&applied, &chatgpt_patch()).unwrap());
let removed = remove_provider(&applied, "acme-openai").unwrap();
assert!(removed.contains("other = { name"), "{removed}");
assert!(!removed.contains("acme-openai"), "{removed}");
}
#[test]
fn removal_respects_the_users_own_selection_and_untouched_configs() {
let switched = r#"model_provider = "their-provider"
[model_providers.acme-openai]
name = "Acme OpenAI"
[model_providers.their-provider]
name = "Theirs"
"#;
let removed = remove_provider(switched, "acme-openai").unwrap();
assert!(removed.contains(r#"model_provider = "their-provider""#));
assert!(removed.contains("[model_providers.their-provider]"));
assert!(!removed.contains("acme-openai"), "{removed}");
let untouched = "# nothing of ours\nmodel = \"gpt-5-codex\"\n";
assert_eq!(
remove_provider(untouched, "acme-openai").unwrap(),
untouched
);
assert_eq!(remove_provider("", "acme-openai").unwrap(), "");
}
#[test]
fn installed_provider_reads_the_table_back() {
assert_eq!(installed_provider("", "acme-openai").unwrap(), None);
let applied = apply_provider("", &api_key_patch()).unwrap();
let installed = installed_provider(&applied, "acme-openai")
.unwrap()
.unwrap();
assert_eq!(installed.name.as_deref(), Some("Acme OpenAI"));
assert_eq!(
installed.base_url.as_deref(),
Some("http://127.0.0.1:51539/v1/openai-responses/openai-transparent/v1"),
);
assert_eq!(installed.wire_api.as_deref(), Some(WIRE_API));
assert_eq!(installed.env_key.as_deref(), Some(CODEX_API_KEY_ENV));
assert_eq!(installed.requires_openai_auth, None);
let chatgpt = apply_provider("", &chatgpt_patch()).unwrap();
let installed = installed_provider(&chatgpt, "acme-openai")
.unwrap()
.unwrap();
assert_eq!(installed.requires_openai_auth, Some(true));
assert_eq!(installed.env_key, None);
}
#[test]
fn non_table_shapes_are_refused_not_restructured() {
for existing in [
"model_providers = 3\n",
"model_providers = {}\n",
"model_providers = { acme-openai = {} }\n",
] {
let err = apply_provider(existing, &chatgpt_patch())
.expect_err("non-table model_providers must be refused");
assert!(
matches!(err, CodexConfigError::NotATable { ref key } if key == "model_providers"),
"{existing:?} -> {err:?}",
);
}
let err = apply_provider("[model_providers]\nacme-openai = 3\n", &chatgpt_patch())
.expect_err("a non-table provider entry must be refused");
assert!(
matches!(err, CodexConfigError::NotATable { ref key } if key == "model_providers.acme-openai"),
"{err:?}",
);
}
#[test]
fn malformed_toml_is_an_error_everywhere() {
let malformed = "[invalid\npreserve = true\n";
assert!(matches!(
apply_provider(malformed, &chatgpt_patch()),
Err(CodexConfigError::Parse { .. })
));
assert!(matches!(
remove_provider(malformed, "acme-openai"),
Err(CodexConfigError::Parse { .. })
));
assert!(matches!(
is_provider_applied(malformed, &chatgpt_patch()),
Err(CodexConfigError::Parse { .. })
));
assert!(matches!(
installed_provider(malformed, "acme-openai"),
Err(CodexConfigError::Parse { .. })
));
}
#[test]
fn a_blank_provider_id_is_refused_everywhere() {
let blank = CodexProviderPatch::new(" ", "Name", "http://localhost:9", CodexAuth::ChatGpt);
assert!(matches!(
apply_provider("", &blank),
Err(CodexConfigError::EmptyProviderId)
));
assert!(matches!(
remove_provider("", " "),
Err(CodexConfigError::EmptyProviderId)
));
assert!(matches!(
installed_provider("", ""),
Err(CodexConfigError::EmptyProviderId)
));
}
#[test]
fn wire_api_matches_the_launch_recipes() {
use crate::launch::{CodexRecipe, LaunchRecipe, ProxyEndpoint};
let plan = CodexRecipe::new(
ProxyEndpoint::new("http://localhost:9"),
CodexAuth::ApiKey,
"p",
)
.plan()
.unwrap();
assert!(
plan.args
.iter()
.any(|arg| arg == &format!("model_providers.p.wire_api=\"{WIRE_API}\"")),
"{:?}",
plan.args,
);
}
}