Skip to content

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.

Install

To use the Codex provider, you need to either install pydantic-ai, or install pydantic-ai-slim with the openai optional group:

Terminal
pip install "pydantic-ai-slim[openai]"

Usage

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.

Logging in without the Codex CLI

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:

codex_login.py
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.

Persisting credentials

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:

codex_persist.py
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.

Tracing without exposing credentials

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:

codex_tracing.py
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.

Prompt caching

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.

Limitations

  • 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, and top_p) are dropped before sending. The Codex profile leaves explicit openai_top_logprobs, openai_truncation, and openai_user settings 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 explicit openai_store=True is silently overridden: responses are never persisted server-side. Consequently, resuming a suspended run raises UserError, since there is no stored response to continue from.
  • count_tokens() raises UserError: 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.