Skip to content
Closed
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ The opt-in Images API returns base64 image data and consumes the signed-in accou

## Providers

To keep Claude models on Anthropic while switching GPT models through Codex in the
same session, opt in with `CCP_ALIAS_PROVIDER=anthropic`. See
[Anthropic passthrough](https://claude-code-proxy.raine.dev/providers/anthropic/)
for subscription authentication and thinking compatibility.


| Provider | Account | Model selection |
| ------------ | ------------------------------ | ----------------------------------------------- |
| Codex | ChatGPT Plus or Pro | Registered `gpt-*` models and `-fast` variants |
Expand Down
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default defineConfig({
label: 'Providers',
items: [
{ label: 'Choosing a provider', slug: 'providers/choosing-a-provider' },
{ label: 'Anthropic', slug: 'providers/anthropic' },
{ label: 'Codex', slug: 'providers/codex' },
{ label: 'Kimi', slug: 'providers/kimi' },
{ label: 'Grok', slug: 'providers/grok' },
Expand Down
43 changes: 43 additions & 0 deletions docs/src/content/docs/providers/anthropic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: Anthropic
description: Keep Claude models on Anthropic while switching other models through the proxy in the same Claude Code session.
---

Anthropic passthrough is opt-in. Set `aliasProvider` to `anthropic` in `config.json`, or start the proxy with:

```sh
CCP_ALIAS_PROVIDER=anthropic claude-code-proxy serve
```

This sends `claude-*` models and the built-in `haiku`, `sonnet`, `opus`, and `fable` aliases to `api.anthropic.com`. Explicit GPT, Kimi, Grok, OpenCode Go and Cursor IDs retain their provider. A preceding GPT or Kimi turn does not change where the Claude aliases go. Restart the proxy after changing `aliasProvider`.

## Authentication and model switching

Sign in using Claude Code. The proxy forwards the credentials Claude Code includes in each request; it does not read the keychain, store Anthropic credentials, or refresh them. Codex continues to use its own `claude-code-proxy codex auth login`.

Launch Claude Code using its existing subscription login:

```sh
env -u ANTHROPIC_AUTH_TOKEN -u ANTHROPIC_API_KEY \
ANTHROPIC_BASE_URL=http://127.0.0.1:18765 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --model claude-opus-5
```

Also remove dummy credentials from Claude Code settings if configured there. `ANTHROPIC_AUTH_TOKEN=unused` replaces the subscription login and causes the Anthropic route to fail. Deliberately configured Anthropic API credentials are forwarded too.

Use `/model gpt-6-astra` and `/model claude-opus-5` to switch between the providers. Availability still depends on the selected provider account. The model catalog advertises known aliases, but explicit new `claude-*` IDs can be forwarded without a proxy release.

## Thinking and conversation history

Native Anthropic thinking and `redacted_thinking` blocks remain unchanged, including signatures. Requests with no local model rewrite or foreign thinking conversion retain their original bytes, including unknown fields, cache controls and beta headers.

When switching from GPT to Claude, unsigned thinking and thinking with proxy-owned `ccp:` signatures become `<previous_reasoning>` text blocks containing only the visible summary. When switching from Claude to GPT, visible thinking summaries similarly become text; opaque Claude signatures and redacted content are not sent as Codex reasoning. Valid native Codex reasoning continues to replay in its encrypted form on Codex turns. Responses are streamed unchanged, so the conversation keeps the original provider blocks for a later switch back.

This carries visible summaries across providers, not encrypted internal reasoning. Switch after a completed turn. Changing providers during an unfinished tool-use turn can still fail the receiving provider's history validation.

## HTTP behavior and monitor

`POST /v1/messages` and `POST /v1/messages/count_tokens` preserve query parameters and forward Anthropic authentication and beta headers. Local aliases and the `[1m]` suffix are resolved before forwarding. Redirects are not followed. Upstream status codes, request IDs, retry headers, response bodies and SSE events are preserved; connection-specific headers are removed.

Anthropic requests appear in the existing monitor, including usage and streamed errors. The provider does not expose Anthropic through the proxy's OpenAI-compatible endpoints. With `aliasProvider` set to `codex` (the default) or `kimi`, routing and session affinity keep their existing behavior.
3 changes: 2 additions & 1 deletion docs/src/content/docs/providers/choosing-a-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ One `serve` process supports every provider. Choose based on the account you hav

| Provider | Account | Upstream protocol | Model selection | Notable capabilities |
| --- | --- | --- | --- | --- |
| [Anthropic](/providers/anthropic/) (opt-in) | Claude Code subscription login or Anthropic API credentials | Anthropic Messages passthrough | `claude-*` and built-in Claude aliases | Native tools, thinking and streaming; credentials forwarded from Claude Code |
| [Codex](/providers/codex/) | ChatGPT Plus or Pro | OpenAI Responses over WebSocket or HTTP SSE | Named Codex catalog, `-fast` variants | Function tools, image input, hosted web search, reasoning summaries, optional native Responses route |
| [Kimi](/providers/kimi/) | kimi.com with Kimi Code access | OpenAI-style chat completions | `kimi-for-coding` and aliases | Function tools, reasoning, image and video input |
| [Grok](/providers/grok/) | grok.com | Responses API | `grok-composer-2.5-fast`, `grok-4.5`, `grok-4.6` | Function tools, reasoning, web search, X search, citations |
Expand All @@ -23,7 +24,7 @@ One `serve` process supports every provider. Choose based on the account you hav

## Shared behavior

All providers route by model ID, use proxy-owned credentials, refresh tokens when supported, stream responses, translate Claude Code tool definitions, and report failures through the same Anthropic-shaped API.
All providers route by model ID, stream responses and report failures through the same Anthropic-shaped API. Translating providers use proxy-owned credentials and refresh tokens when supported. Anthropic forwards Claude Code's credentials and native request fields instead.

<div class="security-callout">
<strong>Account policy matters.</strong> Provider subscriptions, model access, regional availability, rate limits, and rules for unofficial clients can change. Review the terms for your account before using a provider through the proxy.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ All keys are optional. An unreadable file, malformed JSON, or incompatible field
| `CCP_BIND_ADDRESS` | `bindAddress` | `127.0.0.1` | Listener IP address. |
| `PORT` | `port` | `18765` | Listener port. |
| `CCP_CONFIG_DIR` | none | Platform config directory | Replaces the configuration and file-backed auth root. |
| `CCP_ALIAS_PROVIDER` | `aliasProvider` | `codex` | Routes recognized Anthropic-style aliases through `codex` or `kimi`. |
| `CCP_ALIAS_PROVIDER` | `aliasProvider` | `codex` | Routes recognized Anthropic-style aliases through `codex` or `kimi`; `anthropic` enables [native Anthropic passthrough](/providers/anthropic/). |
| `CCP_AUTO_REVIEW_MODEL` | `autoReviewModel` | `gpt-5.6-luna` for Codex | Routes Claude Code's non-streaming, tool-free Bash security-review classifier through a registered model. |
| `CCP_LOG_STDERR` | `log.stderr` | `false` | Mirrors logs to stderr when present in the environment, regardless of its value. |
| `CCP_LOG_VERBOSE` | `log.verbose` | `false` | Preserves full string fields in structured logs when present, regardless of its value. |
Expand Down
4 changes: 3 additions & 1 deletion docs/src/content/docs/using/models-and-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,6 @@ ANTHROPIC_SMALL_FAST_MODEL=gpt-5.6-luna[1m] \

## Alias routing

`CCP_ALIAS_PROVIDER=kimi` or `"aliasProvider": "kimi"` routes recognized Anthropic-style aliases to Kimi. Accepted values are `codex` and `kimi`. Explicit provider IDs always use their provider.
`CCP_ALIAS_PROVIDER=kimi` or `"aliasProvider": "kimi"` routes recognized Anthropic-style aliases to Kimi. Accepted values are `codex`, `kimi`, and `anthropic`. Explicit provider IDs always use their provider.

With `anthropic`, Claude aliases and explicit `claude-*` IDs go to [Anthropic](/providers/anthropic/) using the credentials Claude Code sends. They remain on Anthropic even after a GPT or Kimi turn establishes session affinity. Existing `codex` and `kimi` alias behavior is unchanged.
2 changes: 2 additions & 0 deletions docs/src/content/docs/using/switching-models-and-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Claude Code binds its base URL and client auth when the process starts. A **back
| Toggle between proxy and direct Anthropic | Use a launch wrapper controlled by a flag |
| Stay on the proxy and change provider or model | Use `/model`, `--model`, or a new `ANTHROPIC_MODEL` |

To use Claude and GPT in one proxy-backed session, enable [Anthropic passthrough](/providers/anthropic/) with `CCP_ALIAS_PROVIDER=anthropic`. Claude Code keeps its subscription login and `/model` selects the provider per request. Switching the base URL itself still requires a new Claude Code process.

## One-shot aliases

```sh
Expand Down
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ use crate::paths;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AliasProvider {
Anthropic,
Codex,
Kimi,
}

impl AliasProvider {
pub fn as_str(&self) -> &str {
match self {
AliasProvider::Anthropic => "anthropic",
AliasProvider::Codex => "codex",
AliasProvider::Kimi => "kimi",
}
Expand Down Expand Up @@ -122,6 +124,7 @@ struct FileLog {

fn parse_alias(raw: &str) -> Option<AliasProvider> {
match raw {
"anthropic" => Some(AliasProvider::Anthropic),
"codex" => Some(AliasProvider::Codex),
"kimi" => Some(AliasProvider::Kimi),
_ => None,
Expand Down
9 changes: 7 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ fn run_provider_cli(name: &str, command: ProviderGroup) -> Result<()> {

fn print_models(registry: &Registry, full: bool) {
let grouped = registry.grouped_models();
for provider in ["codex", "kimi", "grok", "opencode", "cursor"] {
for provider in ["anthropic", "codex", "kimi", "grok", "opencode", "cursor"] {
let Some(models) = grouped.get(provider) else {
continue;
};
Expand Down Expand Up @@ -277,7 +277,12 @@ fn print_server_banner(bind_address: &str, port: u16, registry: &Registry) {
println!();
println!("Configure Claude Code (pick a model from above):");
println!(" export ANTHROPIC_BASE_URL=\"http://localhost:{port}\"");
println!(" export ANTHROPIC_AUTH_TOKEN=\"anything\"");
if registry.provider("anthropic").is_some() {
println!(" # Keep Claude Code's subscription login for Claude models:");
println!(" unset ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY");
} else {
println!(" export ANTHROPIC_AUTH_TOKEN=\"anything\"");
}
println!(" export ANTHROPIC_MODEL=\"gpt-5.6-sol\"");
println!(" export ANTHROPIC_SMALL_FAST_MODEL=\"gpt-5.6-luna\"");
println!(" export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1");
Expand Down
52 changes: 50 additions & 2 deletions src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ use crate::request_identity::ConversationIdentity;
use crate::traffic::TrafficCapture;
use anyhow::Result;
use async_trait::async_trait;
use axum::{body::Body, http::StatusCode, response::Response};
use axum::{
body::Body,
http::{Request, StatusCode},
response::Response,
};
use bytes::Bytes;
use clap::Subcommand;
use std::sync::Arc;
use std::sync::{Arc, Mutex};

#[derive(Debug, Clone, Subcommand)]
pub enum AuthCommand {
Expand All @@ -26,6 +30,16 @@ pub trait Provider: Send + Sync {
fn name(&self) -> &'static str;
fn supported_models(&self) -> Vec<String>;
fn cli(&self) -> &'static dyn CliHandlers;

async fn handle_request(&self, request: ProviderRequest, ctx: RequestContext) -> Response {
match request.endpoint {
RequestEndpoint::Messages(identity) => {
self.handle_messages_with_conversation_identity(request.body, ctx, identity)
.await
}
RequestEndpoint::CountTokens => self.handle_count_tokens(request.body, ctx).await,
}
}
async fn handle_messages(&self, body: MessagesRequest, ctx: RequestContext) -> Response;

async fn handle_messages_with_conversation_identity(
Expand Down Expand Up @@ -56,6 +70,40 @@ pub trait Provider: Send + Sync {
}
}

/// The parsed routing input and the original HTTP representation travel together.
/// Translating providers use `body`; passthrough providers retain fields unknown
/// to the proxy by forwarding `original`.
pub struct ProviderRequest {
pub body: MessagesRequest,
pub original: Request<Bytes>,
pub endpoint: RequestEndpoint,
}

pub enum RequestEndpoint {
Messages(Option<ConversationIdentity>),
CountTokens,
}

/// Records a protocol failure discovered after response headers were sent.
#[derive(Clone, Default)]
pub struct ResponseOutcome {
failure: Arc<Mutex<Option<String>>>,
}

impl ResponseOutcome {
pub fn failure(&self) -> Option<String> {
self.failure.lock().ok().and_then(|failure| failure.clone())
}

pub(crate) fn fail(&self, message: String) {
if let Ok(mut failure) = self.failure.lock()
&& failure.is_none()
{
*failure = Some(message);
}
}
}

pub enum GenerationBody {
BufferedSse(Bytes),
LiveSse(Body),
Expand Down
74 changes: 74 additions & 0 deletions src/providers/anthropic/headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use http::{HeaderMap, HeaderName, header};

pub(super) fn forwarded_headers(headers: &HeaderMap) -> HeaderMap {
let connection_headers: Vec<HeaderName> = headers
.get_all(header::CONNECTION)
.iter()
.filter_map(|value| value.to_str().ok())
.flat_map(|value| value.split(','))
.filter_map(|name| HeaderName::from_bytes(name.trim().as_bytes()).ok())
.collect();
let mut forwarded = HeaderMap::new();
for (name, value) in headers {
if !connection_headers.contains(name)
&& !matches!(
name.as_str(),
"host"
| "connection"
| "keep-alive"
| "proxy-authenticate"
| "proxy-authorization"
| "te"
| "trailer"
| "transfer-encoding"
| "upgrade"
| "content-length"
)
{
forwarded.append(name.clone(), value.clone());
}
}
forwarded
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn strips_connection_headers_but_preserves_credentials_and_encoding() {
let mut headers = HeaderMap::new();
for (name, value) in [
("connection", "keep-alive, X-Private"),
("x-private", "hop"),
("host", "localhost"),
("content-length", "12"),
("authorization", "Bearer token"),
("x-api-key", "key"),
("anthropic-beta", "beta"),
("content-encoding", "gzip"),
] {
headers.insert(HeaderName::from_static(name), value.parse().unwrap());
}
headers.append(header::CONNECTION, "X-Other".parse().unwrap());
headers.insert("x-other", "hop".parse().unwrap());
let forwarded = forwarded_headers(&headers);
for name in [
"connection",
"x-private",
"x-other",
"host",
"content-length",
] {
assert!(!forwarded.contains_key(name));
}
for name in [
"authorization",
"x-api-key",
"anthropic-beta",
"content-encoding",
] {
assert_eq!(forwarded[name], headers[name]);
}
}
}
Loading