From 404cae7d40114ae78d71f7bebd26b674bd5222bb Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 21 Oct 2025 14:02:56 -0700 Subject: [PATCH] feat: add experimental_bearer_token option to model provider definition (#5467) While we do not want to encourage users to hardcode secrets in their `config.toml` file, it should be possible to pass an API key programmatically. For example, when using `codex app-server`, it is possible to pass a "bag of configuration" as part of the `NewConversationParams`: https://github.com/openai/codex/blob/682d05512f2992dd0657f1078d4146f15c744d7a/codex-rs/app-server-protocol/src/protocol.rs#L248-L251 When using `codex app-server`, it's not practical to change env vars of the `codex app-server` process on the fly (which is how we usually read API key values), so this helps with that. --- codex-rs/core/src/client.rs | 6 ++++ codex-rs/core/src/config.rs | 1 + codex-rs/core/src/model_provider_info.rs | 32 ++++++++++++++----- .../core/tests/chat_completions_payload.rs | 1 + codex-rs/core/tests/chat_completions_sse.rs | 1 + codex-rs/core/tests/responses_headers.rs | 1 + codex-rs/core/tests/suite/client.rs | 3 ++ .../suite/stream_error_allows_next_turn.rs | 1 + .../core/tests/suite/stream_no_completed.rs | 1 + 9 files changed, 39 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 78e7d03e..2d614e37 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -1093,6 +1093,7 @@ mod tests { base_url: Some("https://test.com".to_string()), env_key: Some("TEST_API_KEY".to_string()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -1156,6 +1157,7 @@ mod tests { base_url: Some("https://test.com".to_string()), env_key: Some("TEST_API_KEY".to_string()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -1192,6 +1194,7 @@ mod tests { base_url: Some("https://test.com".to_string()), env_key: Some("TEST_API_KEY".to_string()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -1230,6 +1233,7 @@ mod tests { base_url: Some("https://test.com".to_string()), env_key: Some("TEST_API_KEY".to_string()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -1264,6 +1268,7 @@ mod tests { base_url: Some("https://test.com".to_string()), env_key: Some("TEST_API_KEY".to_string()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -1367,6 +1372,7 @@ mod tests { base_url: Some("https://test.com".to_string()), env_key: Some("TEST_API_KEY".to_string()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 49bb23fa..993a6832 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -2801,6 +2801,7 @@ model_verbosity = "high" env_key: Some("OPENAI_API_KEY".to_string()), wire_api: crate::WireApi::Chat, env_key_instructions: None, + experimental_bearer_token: None, query_params: None, http_headers: None, env_http_headers: None, diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index a5707b34..429a2606 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -53,6 +53,11 @@ pub struct ModelProviderInfo { /// variable and set it. pub env_key_instructions: Option, + /// Value to use with `Authorization: Bearer ` header. Use of this + /// config is discouraged in favor of `env_key` for security reasons, but + /// this may be necessary when using this programmatically. + pub experimental_bearer_token: Option, + /// Which wire protocol this provider expects. #[serde(default)] pub wire_api: WireApi, @@ -102,14 +107,18 @@ impl ModelProviderInfo { client: &'a reqwest::Client, auth: &Option, ) -> crate::error::Result { - let effective_auth = match self.api_key() { - Ok(Some(key)) => Some(CodexAuth::from_api_key(&key)), - Ok(None) => auth.clone(), - Err(err) => { - if auth.is_some() { - auth.clone() - } else { - return Err(err); + let effective_auth = if let Some(secret_key) = &self.experimental_bearer_token { + Some(CodexAuth::from_api_key(secret_key)) + } else { + match self.api_key() { + Ok(Some(key)) => Some(CodexAuth::from_api_key(&key)), + Ok(None) => auth.clone(), + Err(err) => { + if auth.is_some() { + auth.clone() + } else { + return Err(err); + } } } }; @@ -274,6 +283,7 @@ pub fn built_in_model_providers() -> HashMap { .filter(|v| !v.trim().is_empty()), env_key: None, env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: Some( @@ -333,6 +343,7 @@ pub fn create_oss_provider_with_base_url(base_url: &str) -> ModelProviderInfo { base_url: Some(base_url.into()), env_key: None, env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Chat, query_params: None, http_headers: None, @@ -372,6 +383,7 @@ base_url = "http://localhost:11434/v1" base_url: Some("http://localhost:11434/v1".into()), env_key: None, env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Chat, query_params: None, http_headers: None, @@ -399,6 +411,7 @@ query_params = { api-version = "2025-04-01-preview" } base_url: Some("https://xxxxx.openai.azure.com/openai".into()), env_key: Some("AZURE_OPENAI_API_KEY".into()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Chat, query_params: Some(maplit::hashmap! { "api-version".to_string() => "2025-04-01-preview".to_string(), @@ -429,6 +442,7 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" } base_url: Some("https://example.com".into()), env_key: Some("API_KEY".into()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Chat, query_params: None, http_headers: Some(maplit::hashmap! { @@ -455,6 +469,7 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" } base_url: Some(base_url.into()), env_key: None, env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -487,6 +502,7 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" } base_url: Some("https://example.com".into()), env_key: None, env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index 0d0d60d4..da0c2bcd 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -50,6 +50,7 @@ async fn run_request(input: Vec) -> Value { base_url: Some(format!("{}/v1", server.uri())), env_key: None, env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Chat, query_params: None, http_headers: None, diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index dffc9e42..795bd9f9 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -49,6 +49,7 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { base_url: Some(format!("{}/v1", server.uri())), env_key: None, env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Chat, query_params: None, http_headers: None, diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 4a086cf1..f5887ebc 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -38,6 +38,7 @@ async fn responses_stream_includes_task_type_header() { base_url: Some(format!("{}/v1", server.uri())), env_key: None, env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index ed16cdb3..088a723e 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -632,6 +632,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { base_url: Some(format!("{}/openai", server.uri())), env_key: None, env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, @@ -1115,6 +1116,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() { base_url: Some(format!("{}/openai", server.uri())), // Reuse the existing environment variable to avoid using unsafe code env_key: Some(existing_env_var_with_random_value.to_string()), + experimental_bearer_token: None, query_params: Some(std::collections::HashMap::from([( "api-version".to_string(), "2025-04-01-preview".to_string(), @@ -1197,6 +1199,7 @@ async fn env_var_overrides_loaded_auth() { "2025-04-01-preview".to_string(), )])), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, http_headers: Some(std::collections::HashMap::from([( "Custom-Header".to_string(), diff --git a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs index 03f1be0f..ba86f8c1 100644 --- a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs +++ b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs @@ -66,6 +66,7 @@ async fn continue_after_stream_error() { base_url: Some(format!("{}/v1", server.uri())), env_key: Some("PATH".into()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, diff --git a/codex-rs/core/tests/suite/stream_no_completed.rs b/codex-rs/core/tests/suite/stream_no_completed.rs index 19c2e24f..550bb3f9 100644 --- a/codex-rs/core/tests/suite/stream_no_completed.rs +++ b/codex-rs/core/tests/suite/stream_no_completed.rs @@ -73,6 +73,7 @@ async fn retries_on_early_close() { // provider is not set. env_key: Some("PATH".into()), env_key_instructions: None, + experimental_bearer_token: None, wire_api: WireApi::Responses, query_params: None, http_headers: None,