use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodexProvider {
pub id: String,
pub base_url: String,
pub env_key: Option<String>,
pub inline_bearer_token: bool,
pub model_catalog_json: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodexProviderKind {
Zai,
DeepSeek,
Other,
}
impl CodexProviderKind {
pub fn from_host(host: &str) -> Self {
match host.to_ascii_lowercase().as_str() {
"api.z.ai" | "open.bigmodel.cn" => Self::Zai,
"api.deepseek.com" => Self::DeepSeek,
_ => Self::Other,
}
}
}
impl CodexProvider {
pub fn host(&self) -> Option<String> {
url::Url::parse(&self.base_url)
.ok()
.and_then(|url| url.host_str().map(|host| host.to_ascii_lowercase()))
}
pub fn kind(&self) -> CodexProviderKind {
self.host()
.as_deref()
.map_or(CodexProviderKind::Other, CodexProviderKind::from_host)
}
}
#[derive(Debug, Deserialize)]
struct CodexConfigFile {
model_provider: Option<String>,
model_catalog_json: Option<PathBuf>,
#[serde(default)]
model_providers: std::collections::BTreeMap<String, ProviderTable>,
}
#[derive(Debug, Deserialize)]
struct ProviderTable {
base_url: Option<String>,
wire_api: Option<String>,
env_key: Option<String>,
experimental_bearer_token: Option<String>,
}
pub fn codex_provider(home: &Path) -> Result<Option<CodexProvider>> {
let path = home.join("config.toml");
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(_) => return Ok(None),
};
parse_config(&text, &path)
}
pub fn parse_config(text: &str, path: &Path) -> Result<Option<CodexProvider>> {
let file: CodexConfigFile = toml::from_str(text)
.with_context(|| format!("parse Codex configuration {}", path.display()))?;
let Some(id) = file.model_provider else {
return Ok(None);
};
let Some(table) = file.model_providers.get(&id) else {
bail!(
"{} names model_provider {id:?} but has no [model_providers.{id}] table",
path.display()
);
};
let Some(base_url) = table.base_url.clone() else {
bail!(
"{} is missing base_url for model provider {id:?}",
path.display()
);
};
match table.wire_api.as_deref() {
Some("responses") => {}
Some(other) => bail!(
"{} sets wire_api = {other:?} for model provider {id:?}; Codex only supports \"responses\"",
path.display()
),
None => bail!(
"{} is missing wire_api = \"responses\" for model provider {id:?}",
path.display()
),
}
let inline_bearer_token = table
.experimental_bearer_token
.as_deref()
.is_some_and(|token| !token.trim().is_empty());
let env_key = table
.env_key
.as_deref()
.map(str::trim)
.filter(|key| !key.is_empty())
.map(str::to_owned);
if env_key.is_none() && !inline_bearer_token {
bail!(
"{} declares model provider {id:?} with neither env_key nor experimental_bearer_token",
path.display()
);
}
Ok(Some(CodexProvider {
id,
base_url,
env_key,
inline_bearer_token,
model_catalog_json: file.model_catalog_json.clone(),
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(text: &str) -> Result<Option<CodexProvider>> {
parse_config(text, Path::new("config.toml"))
}
#[test]
fn missing_config_file_reports_no_custom_provider() {
let home = tempfile::tempdir().expect("temporary home");
assert_eq!(codex_provider(home.path()).expect("read"), None);
}
#[test]
fn config_without_model_provider_reports_no_custom_provider() {
let home = tempfile::tempdir().expect("temporary home");
std::fs::write(home.path().join("config.toml"), "model = \"gpt-5.5\"\n").expect("write");
assert_eq!(codex_provider(home.path()).expect("read"), None);
}
#[test]
fn env_key_provider_reports_its_variable_and_base_url() {
let provider = parse(
"model = \"glm-5.3\"\n\
model_provider = \"zai\"\n\
[model_providers.zai]\n\
base_url = \"https://api.z.ai/api/v1\"\n\
env_key = \"ZAI_API_KEY\"\n\
wire_api = \"responses\"\n",
)
.expect("parse")
.expect("provider");
assert_eq!(provider.id, "zai");
assert_eq!(provider.base_url, "https://api.z.ai/api/v1");
assert_eq!(provider.env_key.as_deref(), Some("ZAI_API_KEY"));
assert!(!provider.inline_bearer_token);
assert_eq!(provider.host().as_deref(), Some("api.z.ai"));
}
#[test]
fn inline_bearer_token_provider_is_accepted_without_an_env_key() {
let provider = parse(
"model_provider = \"zai\"\n\
[model_providers.zai]\n\
base_url = \"https://api.z.ai/api/v1\"\n\
experimental_bearer_token = \"secret\"\n\
wire_api = \"responses\"\n",
)
.expect("parse")
.expect("provider");
assert_eq!(provider.env_key, None);
assert!(provider.inline_bearer_token);
}
#[test]
fn chat_wire_api_is_rejected_with_the_supported_value() {
let error = parse(
"model_provider = \"zai\"\n\
[model_providers.zai]\n\
base_url = \"https://api.z.ai/api/v1\"\n\
env_key = \"ZAI_API_KEY\"\n\
wire_api = \"chat\"\n",
)
.expect_err("chat is rejected")
.to_string();
assert!(error.contains("responses"), "{error}");
}
#[test]
fn missing_provider_table_is_rejected_by_name() {
let error = parse("model_provider = \"zai\"\n")
.expect_err("missing table")
.to_string();
assert!(error.contains("model_providers.zai"), "{error}");
}
#[test]
fn provider_without_any_key_source_is_rejected() {
let error = parse(
"model_provider = \"zai\"\n\
[model_providers.zai]\n\
base_url = \"https://api.z.ai/api/v1\"\n\
wire_api = \"responses\"\n",
)
.expect_err("no key")
.to_string();
assert!(error.contains("env_key"), "{error}");
}
#[test]
fn user_supplied_model_catalog_path_is_reported() {
let provider = parse(
"model_provider = \"zai\"\n\
model_catalog_json = \"mine.json\"\n\
[model_providers.zai]\n\
base_url = \"https://api.z.ai/api/v1\"\n\
env_key = \"ZAI_API_KEY\"\n\
wire_api = \"responses\"\n",
)
.expect("parse")
.expect("provider");
assert_eq!(
provider.model_catalog_json.as_deref(),
Some(Path::new("mine.json"))
);
}
#[test]
fn provider_kind_follows_the_base_url_host() {
let provider = |base_url: &str| CodexProvider {
id: "p".to_owned(),
base_url: base_url.to_owned(),
env_key: None,
inline_bearer_token: true,
model_catalog_json: None,
};
assert_eq!(
provider("https://api.z.ai/api/v1").kind(),
CodexProviderKind::Zai
);
assert_eq!(
provider("https://open.bigmodel.cn/api/coding/paas/v4").kind(),
CodexProviderKind::Zai
);
assert_eq!(
provider("https://api.deepseek.com/v1").kind(),
CodexProviderKind::DeepSeek
);
assert_eq!(
provider("https://example.invalid/v1").kind(),
CodexProviderKind::Other
);
assert_eq!(provider("not a url").kind(), CodexProviderKind::Other);
}
}