Preserve model catalog caches across header credential rotation (#54946)

## Why

Rotating header credentials should not discard the model catalog when a stable account ID is available.

## What changed

- Use the account ID from `CodexAuth::Headers` instead of opaque auth headers in the catalog cache identity. Provider settings remain part of the key.
- Retain credential-sensitive keys when the account ID is missing or the provider uses an explicit API key, bearer token, or command auth.
- Document that hosts using header auth must isolate model managers and cache storage by user and catalog-varying request context, since an account ID can represent a shared workspace.

## Testing

Update the auth-rotation test fixture to omit stable account metadata, preserving coverage for catalog invalidation when opaque credentials change.

GitOrigin-RevId: f96a20c5baf76b9235e131140716731da4dbee81
This commit is contained in:
Henry Levy
2026-10-17 23:22:14 +00:00
committed by copyberry
parent 1cc7dc8f2a
commit ffd3429d75
4 changed files with 29 additions and 14 deletions
@@ -78,13 +78,11 @@ impl ExternalAuth for StallingAuth {
}
fn header_auth(token: &'static str) -> CodexAuth {
CodexAuth::Headers(AuthHeaders::new(HeaderMap::from_iter([
(http::header::AUTHORIZATION, HeaderValue::from_static(token)),
(
http::header::HeaderName::from_static("chatgpt-account-id"),
HeaderValue::from_static("same-account"),
),
])))
// Without stable account metadata, credential rotation must invalidate the catalog.
CodexAuth::Headers(AuthHeaders::new(HeaderMap::from_iter([(
http::header::AUTHORIZATION,
HeaderValue::from_static(token),
)])))
}
#[derive(Clone, Copy)]
+14 -6
View File
@@ -1,6 +1,7 @@
//! Model catalog cache identity follows provider routing, effective auth, and gateway OAuth config.
//! Only a digest is persisted; access tokens for ChatGPT are excluded so token
//! rotation does not discard a catalog for the same account, user, and plan.
//! Header auth with an account ID uses that identity instead of opaque auth headers.
use codex_login::CodexAuth;
use codex_model_provider_info::ModelProviderInfo;
@@ -57,16 +58,23 @@ pub(crate) fn identity(
field(&[u8::from(auth.is_fedramp_account())]);
}
let has_stable_account = auth.is_some_and(|auth| {
matches!(
auth,
CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) | CodexAuth::AgentIdentity(_)
) && auth.get_account_id().is_some()
&& (auth.get_chatgpt_user_id().is_some() || auth.get_account_email().is_some())
auth.get_account_id().is_some()
&& if matches!(auth, CodexAuth::Headers(_)) {
!provider_info.has_command_auth()
} else {
matches!(
auth,
CodexAuth::Chatgpt(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::AgentIdentity(_)
) && (auth.get_chatgpt_user_id().is_some() || auth.get_account_email().is_some())
}
});
let explicit_bearer =
provider_info.api_key()?.is_some() || provider_info.experimental_bearer_token.is_some();
// Command auth also returns opaque API keys. A changed key may belong to a
// different account, so only credentials with stable owner metadata can be reused.
// different account, so reuse requires stable owner metadata. Explicit provider
// keys take precedence over the header auth's account identity.
if !has_stable_account || explicit_bearer {
let headers = resolve_provider_auth(auth, provider_info)?.to_auth_headers();
api_provider.headers.extend(headers);
+8
View File
@@ -105,6 +105,14 @@ pub const DEFAULT_MEMORY_CONSOLIDATION_PREFERRED_MODEL: &str = "gpt-5.6-terra";
/// Implementations own provider-specific behavior for a model backend. The
/// `ModelProviderInfo` returned by `info` is the serialized/configured provider
/// metadata used by the default OpenAI-compatible implementation.
///
/// OpenAI-compatible catalog caches use header auth's account ID instead of auth
/// headers when available. Missing IDs and explicit provider API key, bearer, or
/// command auth retain credential-sensitive keys. Provider settings remain in the key.
/// Hosts using header auth must isolate model managers and cache storage by user and
/// catalog-varying request context, such as country: an account ID may identify a
/// workspace shared by multiple users. Request authentication, credential refresh,
/// and permission checks are unchanged.
pub trait ModelProvider: fmt::Debug + Send + Sync {
/// Returns the configured provider metadata.
fn info(&self) -> &ModelProviderInfo;
+2 -1
View File
@@ -42,7 +42,8 @@ const DEFAULT_MODEL_CACHE_TTL: Duration = Duration::from_secs(300);
pub trait ModelsEndpointClient: fmt::Debug + Send + Sync {
/// Opaque identity of the current provider and credentials, without resolving auth.
///
/// Must change when account, email, plan, provider, or API credentials change.
/// Must distinguish provider routing and authenticated principals within the
/// manager and cache's caller-established scope.
/// Return `None` when identity is unavailable; cached catalog reuse is then disabled.
fn identity(&self) -> Option<String>;