fix(core/config): allow overriding OpenAI wire API via env

Fixes #234
This commit is contained in:
James Peter
2025-09-25 15:28:39 +10:00
parent 3302d501f7
commit a49cd2cdcb
6 changed files with 103 additions and 43 deletions
+1
View File
@@ -217,6 +217,7 @@ model_reasoning_summary = "detailed"
- `CODEX_HOME`: Override config directory location - `CODEX_HOME`: Override config directory location
- `OPENAI_API_KEY`: Use API key instead of ChatGPT auth - `OPENAI_API_KEY`: Use API key instead of ChatGPT auth
- `OPENAI_BASE_URL`: Use alternative API endpoints - `OPENAI_BASE_URL`: Use alternative API endpoints
- `OPENAI_WIRE_API`: Force the built-in OpenAI provider to use `chat` or `responses` wiring
   
## FAQ ## FAQ
+1 -1
View File
@@ -123,7 +123,7 @@ How long Codex will wait for activity on a streaming response before treating th
## model_provider ## model_provider
Identifies which provider to use from the `model_providers` map. Defaults to `"openai"`. You can override the `base_url` for the built-in `openai` provider via the `OPENAI_BASE_URL` environment variable. Identifies which provider to use from the `model_providers` map. Defaults to `"openai"`. You can override the `base_url` for the built-in `openai` provider via the `OPENAI_BASE_URL` environment variable and force the wire protocol (`"responses"` or `"chat"`) with `OPENAI_WIRE_API`.
Note that if you override `model_provider`, then you likely want to override Note that if you override `model_provider`, then you likely want to override
`model`, as well. For example, if you are running ollama with Mistral locally, `model`, as well. For example, if you are running ollama with Mistral locally,
+13 -1
View File
@@ -15,6 +15,7 @@ use tokio::time::timeout;
use tracing::debug; use tracing::debug;
use tracing::trace; use tracing::trace;
use crate::auth::AuthManager;
use crate::ModelProviderInfo; use crate::ModelProviderInfo;
use crate::client_common::Prompt; use crate::client_common::Prompt;
use crate::client_common::ResponseEvent; use crate::client_common::ResponseEvent;
@@ -26,6 +27,7 @@ use crate::model_family::ModelFamily;
use crate::openai_tools::create_tools_json_for_chat_completions_api; use crate::openai_tools::create_tools_json_for_chat_completions_api;
use crate::util::backoff; use crate::util::backoff;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use codex_protocol::mcp_protocol::AuthMode;
use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItem;
use codex_protocol::models::ReasoningItemContent; use codex_protocol::models::ReasoningItemContent;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
@@ -38,6 +40,7 @@ pub(crate) async fn stream_chat_completions(
client: &reqwest::Client, client: &reqwest::Client,
provider: &ModelProviderInfo, provider: &ModelProviderInfo,
debug_logger: &Arc<Mutex<DebugLogger>>, debug_logger: &Arc<Mutex<DebugLogger>>,
auth_manager: Option<Arc<AuthManager>>,
) -> Result<ResponseStream> { ) -> Result<ResponseStream> {
if prompt.output_schema.is_some() { if prompt.output_schema.is_some() {
return Err(CodexErr::UnsupportedOperation( return Err(CodexErr::UnsupportedOperation(
@@ -333,7 +336,16 @@ pub(crate) async fn stream_chat_completions(
loop { loop {
attempt += 1; attempt += 1;
let req_builder = provider.create_request_builder(client, &None).await?; let auth = auth_manager.as_ref().and_then(|m| m.auth());
let mut req_builder = provider.create_request_builder(client, &auth).await?;
if let Some(auth) = auth.as_ref() {
if auth.mode == AuthMode::ChatGPT {
if let Some(account_id) = auth.get_account_id() {
req_builder = req_builder.header("chatgpt-account-id", account_id);
}
}
}
let res = req_builder let res = req_builder
.header(reqwest::header::ACCEPT, "text/event-stream") .header(reqwest::header::ACCEPT, "text/event-stream")
+1
View File
@@ -160,6 +160,7 @@ impl ModelClient {
&self.client, &self.client,
&self.provider, &self.provider,
&self.debug_logger, &self.debug_logger,
self.auth_manager.clone(),
) )
.await?; .await?;
+86 -40
View File
@@ -13,6 +13,7 @@ use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::env::VarError; use std::env::VarError;
use std::time::Duration; use std::time::Duration;
use tracing::warn;
use crate::error::EnvVarError; use crate::error::EnvVarError;
const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000; const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000;
@@ -23,6 +24,31 @@ const MAX_STREAM_MAX_RETRIES: u64 = 100;
/// Hard cap for user-configured `request_max_retries`. /// Hard cap for user-configured `request_max_retries`.
const MAX_REQUEST_MAX_RETRIES: u64 = 100; const MAX_REQUEST_MAX_RETRIES: u64 = 100;
fn openai_wire_api_override() -> Option<WireApi> {
let raw = std::env::var("OPENAI_WIRE_API").ok()?;
parse_openai_wire_api(&raw)
}
fn parse_openai_wire_api(raw: &str) -> Option<WireApi> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let normalized = trimmed.to_ascii_lowercase();
match normalized.as_str() {
"chat" => Some(WireApi::Chat),
"responses" => Some(WireApi::Responses),
_ => {
warn!(
override_value = trimmed,
"Ignoring invalid OPENAI_WIRE_API; expected 'chat' or 'responses'"
);
None
}
}
}
/// Wire protocol that the provider speaks. Most third-party services only /// Wire protocol that the provider speaks. Most third-party services only
/// implement the classic OpenAI Chat Completions JSON schema, whereas OpenAI /// implement the classic OpenAI Chat Completions JSON schema, whereas OpenAI
/// itself (and a handful of others) additionally expose the more modern /// itself (and a handful of others) additionally expose the more modern
@@ -278,46 +304,54 @@ pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
[ [
( (
"openai", "openai",
P { {
name: "OpenAI".into(), let mut provider = P {
// Allow users to override the default OpenAI endpoint by name: "OpenAI".into(),
// exporting `OPENAI_BASE_URL`. This is useful when pointing // Allow users to override the default OpenAI endpoint by
// Codex at a proxy, mock server, or Azure-style deployment // exporting `OPENAI_BASE_URL`. This is useful when pointing
// without requiring a full TOML override for the built-in // Codex at a proxy, mock server, or Azure-style deployment
// OpenAI provider. // without requiring a full TOML override for the built-in
base_url: std::env::var("OPENAI_BASE_URL") // OpenAI provider.
.ok() base_url: std::env::var("OPENAI_BASE_URL")
.filter(|v| !v.trim().is_empty()), .ok()
env_key: None, .filter(|v| !v.trim().is_empty()),
env_key_instructions: None, env_key: None,
wire_api: WireApi::Responses, env_key_instructions: None,
query_params: None, wire_api: WireApi::Responses,
http_headers: Some( query_params: None,
[ http_headers: Some(
( [
"version".to_string(), (
codex_version::version().to_string(), "version".to_string(),
), codex_version::version().to_string(),
] ),
.into_iter() ]
.collect(), .into_iter()
), .collect(),
env_http_headers: Some( ),
[ env_http_headers: Some(
( [
"OpenAI-Organization".to_string(), (
"OPENAI_ORGANIZATION".to_string(), "OpenAI-Organization".to_string(),
), "OPENAI_ORGANIZATION".to_string(),
("OpenAI-Project".to_string(), "OPENAI_PROJECT".to_string()), ),
] ("OpenAI-Project".to_string(), "OPENAI_PROJECT".to_string()),
.into_iter() ]
.collect(), .into_iter()
), .collect(),
// Use global defaults for retry/timeout unless overridden in config.toml. ),
request_max_retries: None, // Use global defaults for retry/timeout unless overridden in config.toml.
stream_max_retries: None, request_max_retries: None,
stream_idle_timeout_ms: None, stream_max_retries: None,
requires_openai_auth: true, stream_idle_timeout_ms: None,
requires_openai_auth: true,
};
if let Some(wire_api) = openai_wire_api_override() {
provider.wire_api = wire_api;
}
provider
}, },
), ),
(BUILT_IN_OSS_MODEL_PROVIDER_ID, create_oss_provider()), (BUILT_IN_OSS_MODEL_PROVIDER_ID, create_oss_provider()),
@@ -468,6 +502,18 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" }
assert_eq!(expected_provider, provider); assert_eq!(expected_provider, provider);
} }
#[test]
fn openai_wire_api_override_parser_handles_values() {
assert_eq!(super::parse_openai_wire_api("chat"), Some(WireApi::Chat));
assert_eq!(
super::parse_openai_wire_api(" responses "),
Some(WireApi::Responses)
);
assert_eq!(super::parse_openai_wire_api(""), None);
assert_eq!(super::parse_openai_wire_api(" \t"), None);
assert_eq!(super::parse_openai_wire_api("invalid"), None);
}
#[test] #[test]
fn detects_azure_responses_base_urls() { fn detects_azure_responses_base_urls() {
fn provider_for(base_url: &str) -> ModelProviderInfo { fn provider_for(base_url: &str) -> ModelProviderInfo {
+1 -1
View File
@@ -133,7 +133,7 @@ How long Codex will wait for activity on a streaming response before treating th
## model_provider ## model_provider
Identifies which provider to use from the `model_providers` map. Defaults to `"openai"`. You can override the `base_url` for the built-in `openai` provider via the `OPENAI_BASE_URL` environment variable. Identifies which provider to use from the `model_providers` map. Defaults to `"openai"`. You can override the `base_url` for the built-in `openai` provider via the `OPENAI_BASE_URL` environment variable and force the wire protocol (`"responses"` or `"chat"`) with `OPENAI_WIRE_API`.
Note that if you override `model_provider`, then you likely want to override Note that if you override `model_provider`, then you likely want to override
`model`, as well. For example, if you are running ollama with Mistral locally, `model`, as well. For example, if you are running ollama with Mistral locally,