OpenAI Codex
Use your ChatGPT/Codex subscription with Pydantic AI instead of a pay-per-token API key. The openai-codex provider logs in with the same OAuth flow as the official Codex CLI; for API keys, use the openai provider instead. Your use of the Codex backend is governed by your agreement with OpenAI; check the applicable usage policies for your subscription.
To use the Codex provider, you need to either install pydantic-ai, or install pydantic-ai-slim with the openai optional group:
pip install "pydantic-ai-slim[openai]"
uv add "pydantic-ai-slim[openai]"
Run codex login once with the Codex CLI, then use the openai-codex: prefix:
from pydantic_ai import Agent
agent = Agent('openai-codex:gpt-5.6-luna')
...
This resolves to OpenAICodexModel backed by OpenAICodexProvider, which reads the CLI’s credentials from ~/.codex/auth.json (or $CODEX_HOME/auth.json). The file is never written to; refreshed tokens live in memory for the rest of the process.
If you don’t want to depend on the Codex CLI, OpenAICodexOAuthFlow runs the same browser login. The Codex client pins its redirect URI to http://localhost:1455/auth/callback, so exchange_code_from_callback() listens on that port until the browser redirects there, then exchanges the code for credentials:
import webbrowser
from pydantic_ai import Agent
from pydantic_ai.models.openai_codex import OpenAICodexModel
from pydantic_ai.providers.openai_codex import OpenAICodexOAuthFlow, OpenAICodexProvider
async def main():
flow = OpenAICodexOAuthFlow()
webbrowser.open(flow.authorization_url())
credentials = await flow.exchange_code_from_callback()
provider = OpenAICodexProvider(credentials=credentials)
agent = Agent(OpenAICodexModel('gpt-5.6-luna', provider=provider))
result = await agent.run('Where does "hello world" come from?')
print(result.output)
Passing credentials keeps them in memory only, so the next process has to log in again. To log in once, persist them as described below.
The provider refreshes expired tokens automatically, and refresh tokens are single-use, so the stored copy has to keep up. Give the provider an OpenAICodexCredentialSource and it calls load() on first use and save() after every refresh. Run the login flow only when the store is empty:
import json
import webbrowser
from dataclasses import asdict
from pathlib import Path
from pydantic_ai import Agent
from pydantic_ai.models.openai_codex import OpenAICodexModel
from pydantic_ai.providers.openai_codex import (
OpenAICodexCredentials,
OpenAICodexCredentialSource,
OpenAICodexOAuthFlow,
OpenAICodexProvider,
)
class FileCredentialSource(OpenAICodexCredentialSource):
def __init__(self, path: Path):
self.path = path
async def load(self) -> OpenAICodexCredentials:
return OpenAICodexCredentials(**json.loads(self.path.read_text()))
async def save(self, credentials: OpenAICodexCredentials) -> None:
self.path.write_text(json.dumps(asdict(credentials)))
async def main():
source = FileCredentialSource(Path('codex-credentials.json'))
if not source.path.exists():
flow = OpenAICodexOAuthFlow()
webbrowser.open(flow.authorization_url())
await source.save(await flow.exchange_code_from_callback())
provider = OpenAICodexProvider(credential_source=source)
agent = Agent(OpenAICodexModel('gpt-5.6-luna', provider=provider))
result = await agent.run('Where does "hello world" come from?')
print(result.output)
If save() raises, the refreshed credentials stay live in memory and a CredentialsPersistenceError is raised. Both it and CredentialsRefreshError subclass ModelAPIError, so a FallbackModel treats an unusable login like any other provider failure.
Logfire instrumentation can trace agent runs without capturing OAuth credentials. Leave HTTP body capture disabled unless you need it: logfire.instrument_httpx(capture_all=True) captures authorization codes and token responses, which require additional scrubbing patterns.
If you enable full HTTP capture, configure scrubbing before starting the OAuth flow:
import logfire
logfire.configure(
scrubbing=logfire.ScrubbingOptions(
extra_patterns=[
'access_token',
'refresh_token',
'id_token',
'code_verifier',
'^code$',
'chatgpt-account-id',
'^account_id$',
'safety_identifier',
]
),
)
logfire.instrument_pydantic_ai()
logfire.instrument_httpx(capture_all=True)
The additional patterns redact the OAuth credentials and account identifiers; Logfire’s default patterns already redact the authorization header.
To mirror the official Codex client’s prompt-cache affinity, OpenAICodexModel sends the session-id, thread-id, and x-client-request-id headers and the prompt_cache_key request field. All four are derived from the conversation_id of the message history, so runs continuing the same conversation reuse a stable identity. An explicit openai_prompt_cache_key model setting or explicitly supplied extra_headers always win over the derived values. This does not guarantee a cache hit.
- The Codex backend is streaming-only; for non-streaming runs the library transparently drains a stream, so
agent.run_sync()and friends work as usual. - Unsupported generic settings (
max_tokens,temperature, andtop_p) are dropped before sending. The Codex profile leaves explicitopenai_top_logprobs,openai_truncation, andopenai_usersettings to the standard OpenAI handling, so backend incompatibilities surface as errors. The usual reasoning-related restrictions on log probabilities still apply. - The backend requires
store=false, so every request is sent with it and an explicitopenai_store=Trueis silently overridden: responses are never persisted server-side. Consequently, resuming a suspended run raisesUserError, since there is no stored response to continue from. count_tokens()raisesUserError: the input-tokens endpoint is not served under subscription auth.- There is no device flow: the browser login above is the only login flow the Codex client supports.