Allow Guardian Decisions to fall back to OPENAI_API_KEY (#51133)

## What changed

Use `OPENAI_API_KEY` for the Guardian Decisions sampler when `CODEX_GUARDIAN_DECISIONS_API_KEY` is absent, the provider is `openai`, and no custom base URL is configured. Preserve dedicated-key precedence, including empty values, and do not fall back for other providers or custom base URLs.

## Testing

Add a credential-selection test covering fallback, dedicated-key precedence, empty dedicated keys, and provider and base URL restrictions.

GitOrigin-RevId: a2a62c28789597f013544b51e142b21f9e214080
This commit is contained in:
swastik-oai
2026-10-05 17:24:06 +00:00
committed by copyberry
parent 28a264fbc7
commit 4c9f42f4f8
2 changed files with 63 additions and 10 deletions
@@ -23,16 +23,20 @@ pub(super) fn decisions_sampler(
use super::decisions::DecisionsSampler;
use super::decisions::URL;
use codex_http_client::ClientRouteClass;
let result = std::env::var("CODEX_GUARDIAN_DECISIONS_API_KEY")
.map_err(|_| DecisionsError::Credentials)
.and_then(|key| {
let client = input
.config
.http_client_factory()
.build_client_without_request_logging(URL, ClientRouteClass::Api)
.map_err(|_| DecisionsError::ClientSetup)?;
DecisionsSampler::new(client, key, URL.to_owned())
});
let result = decisions_api_key(
&input.config.model_provider_id,
input.config.model_provider.base_url.as_deref(),
|name| std::env::var(name),
)
.map_err(|_| DecisionsError::Credentials)
.and_then(|key| {
let client = input
.config
.http_client_factory()
.build_client_without_request_logging(URL, ClientRouteClass::Api)
.map_err(|_| DecisionsError::ClientSetup)?;
DecisionsSampler::new(client, key, URL.to_owned())
});
match result {
Ok(sampler) => Some(sampler),
Err(error) => {
@@ -48,6 +52,19 @@ pub(super) fn decisions_sampler(
}
}
fn decisions_api_key(
provider_id: &str,
base_url: Option<&str>,
read_env: impl Fn(&str) -> Result<String, std::env::VarError>,
) -> Result<String, std::env::VarError> {
read_env("CODEX_GUARDIAN_DECISIONS_API_KEY").or_else(|error| match error {
std::env::VarError::NotPresent if provider_id == "openai" && base_url.is_none() => {
read_env("OPENAI_API_KEY")
}
std::env::VarError::NotPresent | std::env::VarError::NotUnicode(_) => Err(error),
})
}
pub(super) async fn sampler_config(
input: &ThreadStartInput<'_, Config>,
auth_manager: Arc<AuthManager>,
@@ -100,3 +117,7 @@ pub(super) async fn sampler_config(
metrics: input.extension_metrics.clone(),
}
}
#[cfg(test)]
#[path = "startup_tests.rs"]
mod tests;
@@ -0,0 +1,32 @@
//! Credential selection honors dedicated keys and restricts ambient-key fallback.
use super::decisions_api_key;
use pretty_assertions::assert_eq;
use std::env::VarError;
#[test]
fn decisions_key_fallback_preserves_precedence_and_provider_boundary() {
for (provider, base_url, dedicated, expected) in [
("openai", None, None, Some("standard")),
("openai", None, Some("dedicated"), Some("dedicated")),
("openai", None, Some(""), Some("")),
("custom", None, None, None),
("openai", Some("https://gateway.example/v1"), None, None),
("custom", None, Some("dedicated"), Some("dedicated")),
] {
let result = decisions_api_key(provider, base_url, |name| match name {
"CODEX_GUARDIAN_DECISIONS_API_KEY" => {
dedicated.map(str::to_owned).ok_or(VarError::NotPresent)
}
"OPENAI_API_KEY" => {
assert_eq!((provider, base_url, dedicated), ("openai", None, None));
Ok("standard".into())
}
_ => panic!("unexpected environment variable: {name}"),
});
assert_eq!(
result,
expected.map(str::to_owned).ok_or(VarError::NotPresent)
);
}
}