Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ title: Changelog
description: Release notes for claude-code-proxy.
---

## Unreleased

- Optional Codex Images API routes reuse the existing ChatGPT OAuth session for
`gpt-image-2` generation and JSON or multipart image edits, with strict size,
concurrency, upstream-host, header, and diagnostic privacy boundaries.

## v0.1.27 (2026-07-29)

- Grok streams remain reliable during long responses, keepalive events, and
Expand Down
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
thiserror = "2.0"
axum = { version = "0.8", features = ["macros"] }
axum = { version = "0.8", features = ["macros", "multipart"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "time", "fs"] }
tower = { version = "0.5", features = ["util"] }
http = "1"
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK=1 \
See [Getting started](https://claude-code-proxy.raine.dev/getting-started/)
for the complete first session.

Optional Codex image generation and editing can reuse the same ChatGPT login:

```sh
CCP_CODEX_IMAGES_API=1 claude-code-proxy serve
curl http://127.0.0.1:18765/v1/images/generations \
-H 'Content-Type: application/json' \
-d '{"prompt":"A paper-cut fox","model":"gpt-image-2"}'
```

The opt-in Images API returns base64 image data and consumes the signed-in account's image quota. Image prompts and payloads are excluded from traffic captures. See the [HTTP API](https://claude-code-proxy.raine.dev/reference/http-api/) for generation and edit schemas.

## Providers

| Provider | Account | Model selection |
Expand Down
16 changes: 15 additions & 1 deletion docs/src/content/docs/providers/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,20 @@ While the native request is active, the monitor shows `compacting`. Structured l

The Responses route preserves native JSON or SSE response bodies for registered Codex models. The Chat Completions route translates standard text messages, reasoning effort, JSON object or JSON Schema output, and buffered or streaming responses. Its omitted reasoning effort defaults to `medium`; the proxy-wide Codex effort override still takes precedence.

The proxy replaces incoming credentials with stored Codex auth for both routes. Images API, response retrieval or deletion, function calling through Chat Completions, and WebSocket ingress are outside their scope. See [HTTP API](/reference/http-api/) for supported Chat Completions fields and error behavior.
The proxy replaces incoming credentials with stored Codex auth for both routes. Response retrieval or deletion, function calling through Chat Completions, and WebSocket ingress are outside their scope. See [HTTP API](/reference/http-api/) for supported Chat Completions fields and error behavior.

## Images API

`CCP_CODEX_IMAGES_API=1` separately enables `POST /v1/images/generations` and `POST /v1/images/edits`. The routes reuse the proxy's stored ChatGPT OAuth session and target the ChatGPT Codex image backend; no OpenAI Platform API key is required.

```sh
CCP_CODEX_IMAGES_API=1 claude-code-proxy serve
```

The model defaults to and is restricted to `gpt-image-2`. Generation accepts JSON. Editing accepts either Codex JSON data URLs or OpenAI-style multipart uploads, which the proxy validates and converts into the Codex JSON contract. Results are returned as `data[].b64_json`. Masks, remote URLs, URL-formatted output, and image variations are not supported.

This is an internal ChatGPT Codex interface rather than the public Platform Images API. It consumes the signed-in account's image quota and can change without public API compatibility guarantees. Image prompts, uploads, generated base64, and upstream error bodies are excluded from traffic captures and persistent error diagnostics.

Because callers are not authenticated, binding to a LAN address lets every firewall-admitted host consume the signed-in account's quota. Restrict the listener to a trusted interface/subnet and never expose it through router forwarding, UPnP, a public tunnel, or permissive IPv6 rules.

See [Configuration](/reference/configuration/) for every Codex setting and [Troubleshooting](/using/troubleshooting/) for auth, model, and transport failures.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ claude-code-proxy targets Claude Code's practical Anthropic API usage rather tha
- Claude Code title generation and other structured background requests are forwarded and consume provider tokens.
- Anthropic-specific fields without a provider mapping can be dropped.
- Native OpenAI Responses passthrough is opt-in and limited to registered Codex models and response creation.
- Codex Images passthrough is separately opt-in, restricted to `gpt-image-2`, and supports generation plus JSON or multipart edits. Variations, masks, remote image URLs, and URL-formatted outputs are unsupported.

## Models and context

Expand Down Expand Up @@ -73,7 +74,8 @@ claude-code-proxy targets Claude Code's practical Anthropic API usage rather tha

- Structured logs redact known credential keys, but user-provided strings can still contain secrets.
- Error captures contain complete redacted failed responses.
- Traffic captures intentionally preserve prompts and tool content.
- Traffic captures intentionally preserve prompts and tool content for message/Responses diagnostics.
- Image generation and edit routes never create traffic captures or persist prompts, uploads, data URLs, generated base64, or upstream error bodies.
- Verbose logging and traffic capture should be scoped to a focused local investigation.

For provider-specific behavior, use the [provider pages](/providers/choosing-a-provider/). For released changes, see the [Changelog](/reference/changelog/).
6 changes: 5 additions & 1 deletion docs/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ These settings configure the proxy process. Claude Code client settings such as
"transport": "websocket",
"previousResponseId": false,
"serverCompaction": false,
"responsesApi": false
"responsesApi": false,
"imagesApi": false,
"imagesBaseUrl": "https://chatgpt.com/backend-api/codex"
},
"kimi": {
"userAgent": "KimiCLI/1.37.0",
Expand Down Expand Up @@ -105,6 +107,8 @@ Proxy URLs may use `http`, `https`, `socks4`, `socks4a`, `socks5`, or `socks5h`.
| `CCP_CODEX_PREVIOUS_RESPONSE_ID` | `codex.previousResponseId` | `false` | Enables append-only WebSocket continuation for `1`, `true`, or `yes`. |
| `CCP_CODEX_SERVER_COMPACTION` | `codex.serverCompaction` | `false` | Enables or disables native compaction for standard boolean words. |
| `CCP_CODEX_RESPONSES_API` | `codex.responsesApi` | `false` | Enables `/v1/responses` and `/v1/chat/completions` for `1`, `true`, or `yes`. |
| `CCP_CODEX_IMAGES_API` | `codex.imagesApi` | `false` | Enables `/v1/images/generations` and `/v1/images/edits` for `1`, `true`, or `yes`. |
| `CCP_CODEX_IMAGES_BASE_URL` | `codex.imagesBaseUrl` | `https://chatgpt.com/backend-api/codex` | Sets the trusted Codex Images API root; production use is restricted to HTTPS `chatgpt.com/backend-api/codex`. |
| `CCP_CODEX_ORIGINATOR` | `codex.originator` | `claude-code-proxy` | Changes the Codex `originator` header. |
| `CCP_CODEX_USER_AGENT` | `codex.userAgent` | `claude-code-proxy/<version>` | Changes the Codex user-agent. |

Expand Down
29 changes: 28 additions & 1 deletion docs/src/content/docs/reference/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@ A buffered response uses the standard `chat.completion` object. A streaming resp

Function calls, hosted tools, images, audio, log probabilities, multiple choices, storage, and output token limits are outside this compatibility surface. Unsupported fields, including `max_tokens` and `max_completion_tokens`, return an OpenAI `invalid_request_error` with the field in `error.param`.

## `POST /v1/images/generations`

This route exists only when `CCP_CODEX_IMAGES_API=1` or `codex.imagesApi` is true. It reuses the proxy-owned ChatGPT/Codex OAuth session and forwards a bounded JSON request to the Codex image service:

```json
{
"prompt": "A paper-cut fox in a moonlit forest",
"model": "gpt-image-2",
"background": "auto",
"quality": "auto",
"size": "auto"
}
```

`prompt` is required. `model` defaults to and is restricted to `gpt-image-2`; `background`, `quality`, and `size` default to `auto`. Optional `n` must be between 1 and 10. Unknown fields and URL response formats are rejected rather than silently forwarded. Successful responses contain `data[].b64_json`; the proxy never writes generated image data to traffic captures.

## `POST /v1/images/edits`

This route uses the same opt-in gate and accepts either:

- Codex JSON with `images: [{"image_url":"data:image/png;base64,..."}]`; or
- OpenAI-style `multipart/form-data` with one to five repeated `image` or `image[]` files and text fields `prompt`, `model`, `background`, `quality`, `size`, and `n`.

Multipart PNG, JPEG, WebP, and GIF signatures are validated and translated to Codex data URLs. The internal Codex edit contract is JSON, so multipart is an ingress compatibility adapter. Masks, remote image URLs, variations, unsupported fields, and other media types return a 4xx OpenAI error. Request bodies, individual files, aggregate inputs, responses, and concurrency are bounded to protect the proxy process.

The Images API is an internal ChatGPT Codex integration, not the public OpenAI Platform Images API. It consumes the signed-in ChatGPT account's entitlement and quota, and the internal contract can change independently of the public API.

## `POST /v1/responses`

This route exists only when `CCP_CODEX_RESPONSES_API=1` or `codex.responsesApi` is true. It accepts a native OpenAI Responses request for a registered Codex model.
Expand All @@ -94,7 +121,7 @@ The proxy:
- preserves native JSON responses and SSE bodies
- records the request in the monitor and optional traffic capture

It does not implement Images API, stored response retrieval or deletion, or WebSocket client ingress.
It does not implement stored response retrieval or deletion, image variations, or WebSocket client ingress.

## Other routes

Expand Down
77 changes: 77 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ struct CodexConfig {
pub server_compaction: Option<bool>,
#[serde(rename = "responsesApi")]
pub responses_api: Option<bool>,
#[serde(rename = "imagesApi")]
pub images_api: Option<bool>,
#[serde(rename = "imagesBaseUrl")]
pub images_base_url: Option<String>,
#[serde(rename = "serviceTier")]
pub service_tier: Option<String>,
#[serde(rename = "reasoningSummary")]
Expand Down Expand Up @@ -226,6 +230,12 @@ pub fn config_override_summary_lines(cfg: &LoadedConfig) -> Vec<String> {
if env.contains_key("CCP_CODEX_RESPONSES_API") {
out.push("codex.responsesApi (env)".to_string());
}
if env.contains_key("CCP_CODEX_IMAGES_API") {
out.push("codex.imagesApi (env)".to_string());
}
if env.contains_key("CCP_CODEX_IMAGES_BASE_URL") {
out.push("codex.imagesBaseUrl (env)".to_string());
}
if env.contains_key("CCP_KIMI_OAUTH_HOST") {
out.push("kimi.oauthHost (env)".to_string());
}
Expand Down Expand Up @@ -299,6 +309,12 @@ pub fn config_override_summary_lines(cfg: &LoadedConfig) -> Vec<String> {
if codex.responses_api == Some(true) {
out.push("codex.responsesApi: true".to_string());
}
if codex.images_api == Some(true) {
out.push("codex.imagesApi: true".to_string());
}
if codex.images_base_url.is_some() {
out.push("codex.imagesBaseUrl (config)".to_string());
}
}
}
out
Expand Down Expand Up @@ -542,6 +558,36 @@ pub fn codex_responses_api() -> bool {
false
}

pub fn codex_images_api() -> bool {
let env: HashMap<_, _> = std::env::vars().collect();
if let Some(raw) = env.get("CCP_CODEX_IMAGES_API") {
return matches!(raw.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
}
let config_dir = paths::config_dir();
if let Some(file) = read_file_config(&config_dir)
&& let Some(codex) = file.codex
&& let Some(enabled) = codex.images_api
{
return enabled;
}
false
}

pub fn codex_images_base_url() -> String {
let env: HashMap<_, _> = std::env::vars().collect();
if let Some(raw) = env.get("CCP_CODEX_IMAGES_BASE_URL") {
return raw.clone();
}
let config_dir = paths::config_dir();
if let Some(file) = read_file_config(&config_dir)
&& let Some(codex) = file.codex
&& let Some(url) = codex.images_base_url
{
return url;
}
"https://chatgpt.com/backend-api/codex".to_string()
}

pub fn codex_service_tier() -> Option<String> {
let env: HashMap<_, _> = std::env::vars().collect();
if let Some(raw) = env.get("CCP_CODEX_SERVICE_TIER") {
Expand Down Expand Up @@ -749,6 +795,8 @@ mod tests {
std::env::remove_var("CCP_CODEX_REASONING_SUMMARY");
std::env::remove_var("CCP_CODEX_SERVER_COMPACTION");
std::env::remove_var("CCP_CODEX_RESPONSES_API");
std::env::remove_var("CCP_CODEX_IMAGES_API");
std::env::remove_var("CCP_CODEX_IMAGES_BASE_URL");
std::env::remove_var("CCP_AUTO_REVIEW_MODEL");
}
}
Expand Down Expand Up @@ -945,6 +993,35 @@ mod tests {
}
}

#[test]
fn codex_images_api_defaults_to_disabled_and_env_overrides_config() {
let _guard = ENV_LOCK.lock().unwrap();
clear_env();
let config = tempfile::TempDir::new().unwrap();
std::fs::write(
config.path().join("config.json"),
r#"{"codex":{"imagesApi":true,"imagesBaseUrl":"https://chatgpt.com/backend-api/codex-custom"}}"#,
)
.unwrap();
let _config_env = EnvGuard::set("CCP_CONFIG_DIR", config.path());

assert!(codex_images_api());
assert_eq!(
codex_images_base_url(),
"https://chatgpt.com/backend-api/codex-custom"
);
let _enabled_env = EnvGuard::set("CCP_CODEX_IMAGES_API", "false");
let _base_env = EnvGuard::set(
"CCP_CODEX_IMAGES_BASE_URL",
"https://chatgpt.com/backend-api/codex",
);
assert!(!codex_images_api());
assert_eq!(
codex_images_base_url(),
"https://chatgpt.com/backend-api/codex"
);
}

#[test]
fn codex_reasoning_summary_reads_config() {
let _guard = ENV_LOCK.lock().unwrap();
Expand Down
2 changes: 2 additions & 0 deletions src/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub enum EndpointKind {
CountTokens,
Responses,
ChatCompletions,
Images,
}

impl EndpointKind {
Expand All @@ -27,6 +28,7 @@ impl EndpointKind {
Self::CountTokens => "count_tokens",
Self::Responses => "responses",
Self::ChatCompletions => "chat_completions",
Self::Images => "images",
}
}
}
Expand Down
Loading