use super::{
CODEX_PROVIDER, ClientError, ClientKind, ClientManager, DocumentMut, Item, OWNERSHIP_MARKER,
PathBuf, SetupResult, Table, fs, read_codex_marker, read_or_empty, unchanged, value,
write_codex_marker, write_if_changed,
};
impl ClientManager {
#[must_use]
pub fn codex_loopback_bridge_state_path(&self) -> PathBuf {
self.managed_path(ClientKind::Codex, "loopback-bridge.json")
}
pub(crate) fn codex_backend_matches(&self, expected: &str) -> Result<bool, ClientError> {
let path = self.config_path(ClientKind::Codex);
let source = read_or_empty(&path)?;
if source.trim().is_empty() {
return Ok(false);
}
let document = source.parse::<DocumentMut>().map_err(|error| {
ClientError::message(format!("invalid TOML in {}: {error}", path.display()))
})?;
Ok(document.get("chatgpt_base_url").and_then(Item::as_str) == Some(expected))
}
pub(super) fn setup_codex(
&self,
base_url: &str,
backend_base_url: Option<&str>,
) -> Result<SetupResult, ClientError> {
let path = self.config_path(ClientKind::Codex);
let source = read_or_empty(&path)?;
let marker = self.codex_home.join(OWNERSHIP_MARKER);
let existing_marker = marker
.exists()
.then(|| read_codex_marker(&marker))
.transpose()?;
let mut document = if source.trim().is_empty() {
DocumentMut::new()
} else {
source.parse::<DocumentMut>().map_err(|error| {
ClientError::message(format!("invalid TOML in {}: {error}", path.display()))
})?
};
let previous_provider = document
.get("model_provider")
.and_then(Item::as_str)
.map(str::to_string);
let previous_chatgpt_base_url = document
.get("chatgpt_base_url")
.and_then(Item::as_str)
.map(str::to_string);
let previous_realtime_ws_base_url = document
.get("experimental_realtime_ws_base_url")
.and_then(Item::as_str)
.map(str::to_string);
let previous_realtime_call_base_url = document
.get("experimental_realtime_webrtc_call_base_url")
.and_then(Item::as_str)
.map(str::to_string);
document["model_provider"] = value(CODEX_PROVIDER);
if document.get("model_providers").is_none() {
document["model_providers"] = Item::Table(Table::new());
}
let providers = document["model_providers"]
.as_table_like_mut()
.ok_or_else(|| ClientError::message("model_providers must be a TOML table"))?;
if providers.get(CODEX_PROVIDER).is_none() {
providers.insert(CODEX_PROVIDER, Item::Table(Table::new()));
}
let provider = providers
.get_mut(CODEX_PROVIDER)
.and_then(Item::as_table_like_mut)
.ok_or_else(|| {
ClientError::message("model_providers.link-assistant must be a TOML table")
})?;
provider.insert("name", value("OpenAI"));
provider.insert("base_url", value(base_url));
provider.remove("env_key");
provider.insert("wire_api", value("responses"));
provider.insert("requires_openai_auth", value(true));
provider.insert("supports_websockets", value(true));
provider.insert("supports_standalone_web_search", value(true));
let backend_base = backend_base_url.map_or_else(
|| base_url.strip_suffix("/v1").unwrap_or(base_url).to_string() + "/backend-api",
str::to_string,
);
document["chatgpt_base_url"] = value(&backend_base);
document["experimental_realtime_ws_base_url"] = value(base_url);
document["experimental_realtime_webrtc_call_base_url"] = value(base_url);
let result = write_if_changed(&path, &source, &document.to_string())?;
if let Some(existing) = existing_marker {
let original_backend = if previous_chatgpt_base_url.as_deref()
== existing.managed_chatgpt_base_url.as_deref()
{
existing.previous_chatgpt_base_url
} else {
previous_chatgpt_base_url
};
let original_ws = if previous_realtime_ws_base_url.as_deref()
== existing.managed_realtime_base_url.as_deref()
{
existing.previous_realtime_ws_base_url
} else {
previous_realtime_ws_base_url
};
let original_call = if previous_realtime_call_base_url.as_deref()
== existing.managed_realtime_base_url.as_deref()
{
existing.previous_realtime_call_base_url
} else {
previous_realtime_call_base_url
};
write_codex_marker(
&marker,
existing.previous_provider.as_deref(),
&backend_base,
original_backend.as_deref(),
base_url,
original_ws.as_deref(),
original_call.as_deref(),
)?;
} else {
let previous_provider = previous_provider.filter(|value| value != CODEX_PROVIDER);
write_codex_marker(
&marker,
previous_provider.as_deref(),
&backend_base,
previous_chatgpt_base_url.as_deref(),
base_url,
previous_realtime_ws_base_url.as_deref(),
previous_realtime_call_base_url.as_deref(),
)?;
}
Ok(result)
}
pub(super) fn remove_codex(&self) -> Result<SetupResult, ClientError> {
let path = self.config_path(ClientKind::Codex);
let source = read_or_empty(&path)?;
if source.trim().is_empty() {
return Ok(unchanged(path));
}
let marker_path = self.codex_home.join(OWNERSHIP_MARKER);
if !marker_path.exists() {
return Ok(unchanged(path));
}
let mut document = source.parse::<DocumentMut>().map_err(|error| {
ClientError::message(format!("invalid TOML in {}: {error}", path.display()))
})?;
let marker = read_codex_marker(&marker_path)?;
if document.get("model_provider").and_then(Item::as_str) == Some(CODEX_PROVIDER) {
if let Some(previous_provider) = marker.previous_provider {
document["model_provider"] = value(previous_provider);
} else {
document.as_table_mut().remove("model_provider");
}
}
if let Some(providers) = document
.get_mut("model_providers")
.and_then(Item::as_table_like_mut)
{
providers.remove(CODEX_PROVIDER);
}
if document.get("chatgpt_base_url").and_then(Item::as_str)
== marker.managed_chatgpt_base_url.as_deref()
{
if let Some(previous) = marker.previous_chatgpt_base_url {
document["chatgpt_base_url"] = value(previous);
} else {
document.as_table_mut().remove("chatgpt_base_url");
}
}
restore_owned_value(
&mut document,
"experimental_realtime_ws_base_url",
marker.managed_realtime_base_url.as_deref(),
marker.previous_realtime_ws_base_url,
);
restore_owned_value(
&mut document,
"experimental_realtime_webrtc_call_base_url",
marker.managed_realtime_base_url.as_deref(),
marker.previous_realtime_call_base_url,
);
let result = write_if_changed(&path, &source, &document.to_string())?;
if marker_path.exists() {
fs::remove_file(marker_path)?;
}
Ok(result)
}
}
fn restore_owned_value(
document: &mut DocumentMut,
key: &str,
managed: Option<&str>,
previous: Option<String>,
) {
if document.get(key).and_then(Item::as_str) != managed {
return;
}
if let Some(previous) = previous {
document[key] = value(previous);
} else {
document.as_table_mut().remove(key);
}
}