Skip to content

core: support dynamic auth tokens for model providers#16288

Merged
bolinfest merged 1 commit intomainfrom
pr16288
Mar 31, 2026
Merged

core: support dynamic auth tokens for model providers#16288
bolinfest merged 1 commit intomainfrom
pr16288

Conversation

@bolinfest
Copy link
Copy Markdown
Collaborator

@bolinfest bolinfest commented Mar 31, 2026

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

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

@bolinfest bolinfest force-pushed the pr16288 branch 2 times, most recently from f042fd7 to 0b30a7a Compare March 31, 2026 08:01
bolinfest added a commit that referenced this pull request Mar 31, 2026
## Summary

`ExternalAuthRefresher` was still shaped around external ChatGPT auth:
`ExternalAuthTokens` always implied ChatGPT account metadata even when a
caller only needed a bearer token.

This PR generalizes that contract so bearer-only sources are
first-class, while keeping the existing ChatGPT paths strict anywhere we
persist or rebuild ChatGPT auth state.

## Motivation

This is the first step toward #15189.

The follow-on provider-auth work needs one shared external-auth contract
that can do both of these things:

- resolve the current bearer token before a request is sent
- return a refreshed bearer token after a `401`

That should not require a second token result type just because there is
no ChatGPT account metadata attached.

## What Changed

- change `ExternalAuthTokens` to carry `access_token` plus optional
`ExternalAuthChatgptMetadata`
- add helper constructors for bearer-only tokens and ChatGPT-backed
tokens
- add `ExternalAuthRefresher::resolve()` with a default no-op
implementation so refreshers can optionally provide the current token
before a request is sent
- keep ChatGPT-only persistence strict by continuing to require ChatGPT
metadata anywhere the login layer seeds or reloads ChatGPT auth state
- update the app-server bridge to construct the new token shape for
external ChatGPT auth refreshes

## Testing

- `cargo test -p codex-login`


---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16286).
* #16288
* #16287
* __->__ #16286
@bolinfest bolinfest force-pushed the pr16288 branch 3 times, most recently from 2bb3ea6 to e13e00c Compare March 31, 2026 08:19
bolinfest added a commit that referenced this pull request Mar 31, 2026
## Summary

`AuthManager` and `UnauthorizedRecovery` already own token resolution
and staged `401` recovery. The missing piece for provider auth was a
bearer-only mode that still fit that design, instead of pushing a second
auth abstraction into `codex-core`.

This PR keeps the design centered on `AuthManager`: it teaches
`codex-login` how to own external bearer auth directly so later provider
work can keep calling `AuthManager.auth()` and `UnauthorizedRecovery`.

## Motivation

This is the middle layer for #15189.

The intended design is still:

- `AuthManager` encapsulates token storage and refresh
- `UnauthorizedRecovery` powers staged `401` recovery
- all request tokens go through `AuthManager.auth()`

This PR makes that possible for provider-backed bearer tokens by adding
a bearer-only auth mode inside `AuthManager` instead of building
parallel request-auth plumbing in `core`.

## What Changed

- move `ModelProviderAuthInfo` into `codex-protocol` so `core` and
`login` share one config shape
- add `login/src/auth/external_bearer.rs`, which runs the configured
command, caches the bearer token in memory, and refreshes it after `401`
- add `AuthManager::external_bearer_only(...)` for provider-scoped
request paths that should use command-backed bearer auth without
mutating the shared OpenAI auth manager
- add `AuthManager::shared_with_external_chatgpt_auth_refresher(...)`
and rename the other `AuthManager` helpers that only apply to external
ChatGPT auth so the ChatGPT-only path is explicit at the call site
- keep external ChatGPT refresh behavior unchanged while ensuring
bearer-only external auth never persists to `auth.json`

## Testing

- `cargo test -p codex-login`
- `cargo test -p codex-protocol`





---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16287).
* #16288
* __->__ #16287
Base automatically changed from pr16287 to main March 31, 2026 08:26
@bolinfest bolinfest merged commit 20f43c1 into main Mar 31, 2026
36 checks passed
@bolinfest bolinfest deleted the pr16288 branch March 31, 2026 08:37
@github-actions github-actions bot locked and limited conversation to collaborators Mar 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support dynamic bearer token refresh for custom model providers

1 participant