mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
20f43c1e05
## Summary Fixes #15189. Custom model providers that set `requires_openai_auth = false` could only use static credentials via `env_key` or `experimental_bearer_token`. That is not enough for providers that mint short-lived bearer tokens, because Codex had no way to run a command to obtain a bearer token, cache it briefly in memory, and retry with a refreshed token after a `401`. This PR adds that provider config and wires it through the existing auth design: request paths still go through `AuthManager.auth()` and `UnauthorizedRecovery`, with `core` only choosing when to use a provider-backed bearer-only `AuthManager`. ## Scope To keep this PR reviewable, `/models` only uses provider auth for the initial request in this change. It does **not** add a dedicated `401` retry path for `/models`; that can be follow-up work if we still need it after landing the main provider-token support. ## Example Usage ```toml model_provider = "corp-openai" [model_providers.corp-openai] name = "Corp OpenAI" base_url = "https://gateway.example.com/openai" requires_openai_auth = false [model_providers.corp-openai.auth] command = "gcloud" args = ["auth", "print-access-token"] timeout_ms = 5000 refresh_interval_ms = 300000 ``` The command contract is intentionally small: - write the bearer token to `stdout` - exit `0` - any leading or trailing whitespace is trimmed before the token is used ## What Changed - add `model_providers.<id>.auth` to the config model and generated schema - validate that command-backed provider auth is mutually exclusive with `env_key`, `experimental_bearer_token`, and `requires_openai_auth` - build a bearer-only `AuthManager` for `ModelClient` and `ModelsManager` when a provider configures `auth` - let normal Responses requests and realtime websocket connects use the provider-backed bearer source through the same `AuthManager.auth()` path - allow `/models` online refresh for command-auth providers and attach the provider token to the initial `/models` request - keep `auth.cwd` available as an advanced escape hatch and include it in the generated config schema ## Testing - `cargo test -p codex-core provider_auth_command` - `cargo test -p codex-core refresh_available_models_uses_provider_auth_token` - `cargo test -p codex-core test_deserialize_provider_auth_config_defaults` ## Docs - `developers.openai.com/codex` should document the new `[model_providers.<id>.auth]` block and the token-command contract
89 lines
3.2 KiB
Rust
89 lines
3.2 KiB
Rust
use codex_otel::AuthEnvTelemetryMetadata;
|
|
|
|
use crate::auth::CODEX_API_KEY_ENV_VAR;
|
|
use crate::auth::OPENAI_API_KEY_ENV_VAR;
|
|
use crate::auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
|
use crate::model_provider_info::ModelProviderInfo;
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub(crate) struct AuthEnvTelemetry {
|
|
pub(crate) openai_api_key_env_present: bool,
|
|
pub(crate) codex_api_key_env_present: bool,
|
|
pub(crate) codex_api_key_env_enabled: bool,
|
|
pub(crate) provider_env_key_name: Option<String>,
|
|
pub(crate) provider_env_key_present: Option<bool>,
|
|
pub(crate) refresh_token_url_override_present: bool,
|
|
}
|
|
|
|
impl AuthEnvTelemetry {
|
|
pub(crate) fn to_otel_metadata(&self) -> AuthEnvTelemetryMetadata {
|
|
AuthEnvTelemetryMetadata {
|
|
openai_api_key_env_present: self.openai_api_key_env_present,
|
|
codex_api_key_env_present: self.codex_api_key_env_present,
|
|
codex_api_key_env_enabled: self.codex_api_key_env_enabled,
|
|
provider_env_key_name: self.provider_env_key_name.clone(),
|
|
provider_env_key_present: self.provider_env_key_present,
|
|
refresh_token_url_override_present: self.refresh_token_url_override_present,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn collect_auth_env_telemetry(
|
|
provider: &ModelProviderInfo,
|
|
codex_api_key_env_enabled: bool,
|
|
) -> AuthEnvTelemetry {
|
|
AuthEnvTelemetry {
|
|
openai_api_key_env_present: env_var_present(OPENAI_API_KEY_ENV_VAR),
|
|
codex_api_key_env_present: env_var_present(CODEX_API_KEY_ENV_VAR),
|
|
codex_api_key_env_enabled,
|
|
// Custom provider `env_key` is arbitrary config text, so emit only a safe bucket.
|
|
provider_env_key_name: provider.env_key.as_ref().map(|_| "configured".to_string()),
|
|
provider_env_key_present: provider.env_key.as_deref().map(env_var_present),
|
|
refresh_token_url_override_present: env_var_present(REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR),
|
|
}
|
|
}
|
|
|
|
fn env_var_present(name: &str) -> bool {
|
|
match std::env::var(name) {
|
|
Ok(value) => !value.trim().is_empty(),
|
|
Err(std::env::VarError::NotUnicode(_)) => true,
|
|
Err(std::env::VarError::NotPresent) => false,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn collect_auth_env_telemetry_buckets_provider_env_key_name() {
|
|
let provider = ModelProviderInfo {
|
|
name: "Custom".to_string(),
|
|
base_url: None,
|
|
env_key: Some("sk-should-not-leak".to_string()),
|
|
env_key_instructions: None,
|
|
experimental_bearer_token: None,
|
|
auth: None,
|
|
wire_api: crate::model_provider_info::WireApi::Responses,
|
|
query_params: None,
|
|
http_headers: None,
|
|
env_http_headers: None,
|
|
request_max_retries: None,
|
|
stream_max_retries: None,
|
|
stream_idle_timeout_ms: None,
|
|
websocket_connect_timeout_ms: None,
|
|
requires_openai_auth: false,
|
|
supports_websockets: false,
|
|
};
|
|
|
|
let telemetry =
|
|
collect_auth_env_telemetry(&provider, /*codex_api_key_env_enabled*/ false);
|
|
|
|
assert_eq!(
|
|
telemetry.provider_env_key_name,
|
|
Some("configured".to_string())
|
|
);
|
|
}
|
|
}
|