Add ChatGPT Codex provider
CI / Build (push) Waiting to run
CI / Test (push) Waiting to run

This commit is contained in:
2026-06-25 18:21:03 -05:00
parent 6767bef88e
commit b37bc677bd
23 changed files with 1449 additions and 89 deletions
+234
View File
@@ -0,0 +1,234 @@
# v0.3.0 ChatGPT Codex Provider Implementation Plan
## Goal
This release adds a first-class `ChatGPT Codex` provider preset so users who are already signed in to Codex with a ChatGPT subscription can run Cassady without creating a separate API-key environment variable. The preset should call `https://chatgpt.com/backend-api/codex/responses` and resolve its bearer token from the local Codex auth config by default.
Success statement:
> A user who has already run `codex login` or signed in through the Codex app can select `ChatGPT Codex` in `cass login`, pass `cass check`, and send Cassady turns through their Codex subscription without copying tokens into Cassady config.
## Scope
### In scope
- Add `ChatGPT Codex` as a built-in provider preset in the setup/login catalog.
- Add a provider kind/client for the ChatGPT Codex responses endpoint rather than forcing it through `/chat/completions` URL construction.
- Read the default access token from the local Codex auth file, normally `$CODEX_HOME/auth.json` or `~/.codex/auth.json`.
- Support the observed Codex auth shape with `tokens.access_token`, while keeping token values out of Cassady config, logs, check output, and error text.
- Prefer the Codex-configured model from `$CODEX_HOME/config.toml` when available, with a safe manual model fallback.
- Teach `cass check` to validate that local Codex auth is present and usable for the active `ChatGPT Codex` provider.
- Add docs explaining prerequisites, setup flow, token-source behavior, expiration troubleshooting, and the distinction between ChatGPT subscription access and API-key providers.
- Add focused tests with temporary Codex-home fixtures and mocked streaming responses.
### Out of scope
- Implementing Cassady's own browser OAuth/device-login flow for ChatGPT.
- Storing or refreshing ChatGPT/Codex tokens in Cassady-owned config files.
- Reverse engineering unrelated ChatGPT backend endpoints beyond the requested Codex responses endpoint.
- Guaranteeing compatibility if the private ChatGPT backend endpoint or Codex auth file format changes.
- Replacing OpenAI-compatible provider support or changing existing provider presets.
- Release tagging, packaging, or GitHub release creation.
## Context or Current State
Cassady's provider stack is currently centered on OpenAI-compatible chat completions:
- `src/setup.rs` owns the built-in provider catalog, setup/login prompts, model discovery via `GET {base_url}/models`, and writes to `providers.json`/`models.json`/`config.json`.
- `src/config.rs` defines `ProviderDefinition`, validates provider registries, resolves `api_key` values from literals or environment-variable references, and currently accepts only `kind = "openai-compatible"`.
- `src/agent.rs` constructs `OpenAiCompatibleProvider` directly from resolved config.
- `src/providers/openai_compatible.rs` appends `/chat/completions`, sends OpenAI-compatible chat payloads, and parses OpenAI-compatible streaming deltas.
- `docs/providers.md`, `docs/configuration.md`, `docs/commands.md`, `docs/workflows.md`, and `README.md` describe provider setup as API-key/environment-variable based.
The new preset differs in two important ways:
1. Authentication should come from Codex's local login state, not from a Cassady environment-variable API key.
2. The endpoint is a Codex-specific responses endpoint (`https://chatgpt.com/backend-api/codex/responses`), so Cassady needs an endpoint-specific provider client or a more general provider dispatch layer.
Local Codex auth is expected to live under Codex home, normally `~/.codex/auth.json`, with a shape like:
```json
{
"auth_mode": "chatgpt",
"tokens": {
"access_token": "...",
"refresh_token": "...",
"account_id": "..."
},
"last_refresh": "..."
}
```
Cassady should treat this file as sensitive input: read it only when resolving the active provider token, never copy the access token into Cassady-owned JSON, and never print token contents.
## Design Principles
1. **Use Codex login state, do not own ChatGPT auth.** Cassady should integrate with an existing Codex login and tell users to run `codex login` or sign in to Codex when auth is missing or expired.
2. **Keep provider protocols explicit.** Do not pretend the ChatGPT Codex endpoint is OpenAI-compatible if it needs different URL construction, request shape, or stream parsing.
3. **Avoid token leakage.** Token values must not be stored in `~/.cass`, included in transcripts, surfaced in `cass check`, or embedded in test snapshots.
4. **Keep existing setup stable.** Existing providers should continue to use environment-variable API keys and `/models` discovery without extra Codex dependencies.
5. **Fail with clear recovery steps.** Missing Codex auth should produce actionable messages, not generic provider errors.
## Design
### Provider catalog and setup UX
Add a built-in catalog entry:
| Provider | Provider id | Kind | Endpoint | Token source |
| --- | --- | --- | --- | --- |
| ChatGPT Codex | `chatgpt-codex` | `chatgpt-codex` | `https://chatgpt.com/backend-api/codex/responses` | Local Codex auth |
In `cass login`/`cass setup`, selecting this provider should skip the normal `API key environment variable` prompt and instead show a prerequisite check:
```text
ChatGPT Codex uses your local Codex login.
✓ Found Codex auth at ~/.codex/auth.json
```
If the file or access token is missing:
```text
ChatGPT Codex needs a local Codex login.
Run `codex login` or sign in with the Codex app, then run `cass login` again.
```
Model selection should prefer, in order:
1. The `model` value from `$CODEX_HOME/config.toml` when present.
2. A known default Codex model constant only if the project already has a current default available.
3. Manual model id entry.
Do not call `GET /models` for `chatgpt-codex` unless a verified endpoint is added later; model discovery remains an OpenAI-compatible setup behavior.
### Provider configuration shape
Extend provider config without breaking existing files. One possible JSON shape is:
```json
{
"id": "chatgpt-codex",
"name": "ChatGPT Codex",
"kind": "chatgpt-codex",
"base_url": "https://chatgpt.com/backend-api/codex/responses",
"auth": { "type": "codex_local" },
"default_model": "gpt-5.5",
"models": ["gpt-5.5"]
}
```
Implementation may choose an equivalent internal representation, but it should preserve these properties:
- Existing `api_key` string behavior remains valid for OpenAI-compatible providers.
- `chatgpt-codex` providers can omit environment-variable API keys.
- `cass check` can distinguish missing local Codex auth from missing API-key env vars.
- Serialized config does not contain the Codex access token.
### Codex auth resolution
Add a small resolver module, for example `src/codex_auth.rs`, with helpers like:
- `codex_home() -> PathBuf`: `$CODEX_HOME` when set, otherwise `~/.codex`.
- `codex_auth_path() -> PathBuf`: `$CODEX_AUTH_FILE` for tests/overrides when set, otherwise `{codex_home}/auth.json`.
- `load_codex_access_token() -> Result<CodexAccessToken>`: parse `tokens.access_token` and return a redacted/display-safe token wrapper.
- `check_codex_auth() -> CodexAuthStatus`: report path found, auth mode, access-token presence, optional JWT expiration, and recovery hints.
If the access token looks like a JWT, parse the `exp` claim without validating the signature so Cassady can warn or fail early when the token is expired. Token refresh itself should stay out of scope unless Codex exposes a stable documented local refresh interface.
Read the token at request time rather than caching it during setup. This allows a separate Codex process to refresh `auth.json` between Cassady turns.
### Provider dispatch
Refactor provider construction so `src/agent.rs` does not instantiate only `OpenAiCompatibleProvider`. A simple first step is an enum:
```rust
enum ProviderClient {
OpenAiCompatible(OpenAiCompatibleProvider),
ChatGptCodex(ChatGptCodexProvider),
}
```
Both variants should expose a common `complete(messages, tools, tx)` async method returning the existing `CompletionResult`. This preserves the current agent loop, tool execution, conversation storage, and TUI behavior.
### ChatGPT Codex responses client
Add a new provider module, for example `src/providers/chatgpt_codex.rs`, that:
- Posts to the exact configured endpoint, defaulting to `https://chatgpt.com/backend-api/codex/responses`.
- Sends `Authorization: Bearer <local Codex access token>`.
- Includes the active model and converted message/tool context in the endpoint's expected responses format.
- Streams assistant text into `AgentEvent::AssistantChunk`.
- Streams reasoning summaries into `AgentEvent::ReasoningChunk` only when the endpoint provides a safe reasoning summary field.
- Converts function/tool call deltas into Cassady `StoredToolCall` values.
- Converts Cassady tool results back into the endpoint's function-call-output input shape on the next turn.
- Redacts authentication details from non-success response errors.
The exact request/stream schema should be verified against the endpoint during implementation and captured in mocked fixtures. If the endpoint rejects a field used by OpenAI-compatible providers, keep the Codex payload minimal rather than adding compatibility shims that risk breaking the flow.
### Check and troubleshooting behavior
For active `chatgpt-codex` providers, `cass check` should report status like:
```text
✓ active provider: chatgpt-codex
✓ endpoint: https://chatgpt.com/backend-api/codex/responses
✓ Codex auth: ~/.codex/auth.json contains an access token
```
Failure should point to recovery:
```text
✗ Codex auth: no access token found in ~/.codex/auth.json
Run `codex login` or sign in with the Codex app, then rerun `cass check`.
```
Do not print the token, account id, refresh token, or full auth JSON.
## Implementation Steps
1. Add the v0.3.0 roadmap entry and this implementation plan.
2. Extend provider config types/validation to support `kind = "chatgpt-codex"` and a non-env local Codex auth source while preserving existing OpenAI-compatible files.
3. Add `src/codex_auth.rs` for Codex home discovery, auth-file parsing, redacted status reporting, and optional JWT expiration checks.
4. Add `ChatGPT Codex` to `src/setup.rs` provider catalog and branch setup behavior so it skips API-key env prompts and `/models` discovery.
5. Refactor provider construction in `src/agent.rs` behind a small provider dispatch enum or trait.
6. Implement `src/providers/chatgpt_codex.rs` with endpoint-specific request conversion, streaming parsing, tool-call conversion, and redacted errors.
7. Update `cass check` so active and inactive provider checks understand Codex-local auth separately from environment-variable API keys.
8. Update README and bundled docs for the new preset, prerequisites, config example, troubleshooting, and known endpoint/auth caveats.
9. Add unit tests for config parsing/validation, Codex auth fixtures, setup catalog behavior, and provider dispatch.
10. Add mocked streaming tests for the ChatGPT Codex client, including text, tool calls, tool outputs, auth failures, and redaction.
## Tests
- `providers.json` with existing OpenAI-compatible providers still parses and validates.
- A `chatgpt-codex` provider with local Codex auth validates without `api_key`/env-var availability.
- Missing `~/.codex/auth.json` produces a clear `cass check` error for an active `chatgpt-codex` provider.
- A fixture `auth.json` with `tokens.access_token` resolves a token but redacts it in display and errors.
- Expired JWT-like access tokens are detected when possible and produce a recovery hint.
- Setup/login catalog includes `ChatGPT Codex` and skips the API-key env-var prompt for that provider.
- Model selection uses `$CODEX_HOME/config.toml` `model` when available, with manual fallback.
- Provider dispatch selects `ChatGptCodexProvider` only for `kind = "chatgpt-codex"`.
- Mocked Codex streaming responses produce assistant chunks and final `CompletionResult.content`.
- Mocked Codex function-call streams produce Cassady `StoredToolCall` values and accept subsequent tool output messages.
- Provider error messages never include access tokens, refresh tokens, or raw auth JSON.
- `cargo fmt` passes.
- `cargo test --locked --all-targets` passes when practical.
## Documentation
- Update `README.md` setup/provider sections with `ChatGPT Codex` as a subscription-backed option.
- Update `docs/providers.md` with the new preset, endpoint, token-source behavior, and private-endpoint caveat.
- Update `docs/configuration.md` with the extended provider schema and a safe example that uses local Codex auth.
- Update `docs/commands.md` and `docs/workflows.md` for `cass login`, `cass check`, and troubleshooting steps.
- Update `docs/troubleshooting.md` with missing/expired Codex auth, unsupported model, and backend endpoint failure guidance.
## Acceptance Criteria
- `cass login` offers `ChatGPT Codex` as a provider preset.
- Selecting `ChatGPT Codex` does not ask for an API-key environment variable by default.
- Cassady reads the access token from local Codex auth at request/check time and never stores that token under `~/.cass`.
- Active `chatgpt-codex` sessions call `https://chatgpt.com/backend-api/codex/responses` instead of appending `/chat/completions`.
- Normal OpenAI-compatible providers continue to work unchanged.
- `cass check` gives clear success/failure output for local Codex auth without leaking secrets.
- README and bundled docs explain the prerequisite of signing in to Codex first.
- `cargo fmt` and `cargo test --locked --all-targets` pass.