Skip to content
Open
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# claude-code-proxy

Claude Code, powered by **OpenAI Codex**, **Kimi**, **Grok**, or **Cursor
Agent**.
Claude Code, powered by **OpenAI Codex**, **Kimi**, **Grok**, **OpenCode Go**,
or **Cursor Agent**.

Docs: <https://claude-code-proxy.raine.dev>

Expand Down Expand Up @@ -73,6 +73,7 @@ for the complete first session.
| Codex | ChatGPT Plus or Pro | Registered `gpt-*` models and `-fast` variants |
| Kimi | kimi.com with Kimi Code access | `kimi-for-coding` and aliases |
| Grok | grok.com | Registered Grok models |
| OpenCode Go | OpenCode Go workspace | Bare IDs and `opencode-go/<model-id>` forms |
| Cursor Agent | Cursor account | Cursor aliases and `cursor:<model-id>` prefixes |

Run `claude-code-proxy models` for the current catalog or
Expand Down
3 changes: 2 additions & 1 deletion docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineConfig({
integrations: [
starlight({
title: 'claude-code-proxy',
description: 'Run Claude Code with Codex, Kimi, Grok, or Cursor Agent.',
description: 'Run Claude Code with Codex, Kimi, Grok, OpenCode Go, or Cursor Agent.',
plugins: [starlightLlmsTxt()],
favicon: '/favicon.svg',
head: [
Expand Down Expand Up @@ -47,6 +47,7 @@ export default defineConfig({
{ label: 'Codex', slug: 'providers/codex' },
{ label: 'Kimi', slug: 'providers/kimi' },
{ label: 'Grok', slug: 'providers/grok' },
{ label: 'OpenCode Go', slug: 'providers/opencode-go' },
{ label: 'Cursor Agent', slug: 'providers/cursor-agent' },
],
},
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Getting started
description: Install claude-code-proxy, authenticate with Codex, start the server, and open one working Claude Code session.
---

This path gets one Codex-backed Claude Code session working. See [Choosing a provider](/providers/choosing-a-provider/) for Kimi, Grok, and Cursor Agent.
This path gets one Codex-backed Claude Code session working. See [Choosing a provider](/providers/choosing-a-provider/) for Kimi, Grok, OpenCode Go, and Cursor Agent.

## 1. Install

Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ claude-code-proxy exposes an Anthropic-compatible HTTP surface to Claude Code an
<div class="route-arrow" aria-hidden="true">→</div>
<div class="route-node"><strong>Proxy pipeline</strong><span>route model<br/>refresh auth<br/>translate events</span></div>
<div class="route-arrow" aria-hidden="true">→</div>
<div class="route-node provider-stack"><code>Codex Responses</code><code>Kimi Chat Completions</code><code>Grok Responses</code><code>Cursor Connect</code></div>
<div class="route-node provider-stack"><code>Codex Responses</code><code>Kimi Chat Completions</code><code>Grok Responses</code><code>OpenCode Chat / Messages</code><code>Cursor Connect</code></div>
</div>

## Request lifecycle
Expand All @@ -25,11 +25,11 @@ claude-code-proxy exposes an Anthropic-compatible HTTP surface to Claude Code an

## Authentication boundary

Each provider login belongs to claude-code-proxy. The proxy does not read native Codex, Grok, or Cursor Agent credentials. Credentials live in the platform credential store described in [Files and storage](/reference/files-and-storage/). Incoming `ANTHROPIC_AUTH_TOKEN` values are accepted for client compatibility and are not used as upstream credentials.
Each provider login belongs to claude-code-proxy. The proxy does not read native Codex, Grok, or Cursor Agent credentials. Credentials live in the platform credential store described in [Files and storage](/reference/files-and-storage/). OpenCode Go instead uses the configured workspace API key. Incoming `ANTHROPIC_AUTH_TOKEN` values are accepted for client compatibility and are not used as upstream credentials.

## Routing boundary

Routing happens per request, not per server process. Codex IDs, Kimi IDs, Grok IDs, Cursor prefixes, and configured Anthropic-style aliases can share one listener. Unknown model IDs return HTTP 400 with the supported catalog.
Routing happens per request, not per server process. Codex IDs, Kimi IDs, Grok IDs, OpenCode Go IDs, Cursor prefixes, and configured Anthropic-style aliases can share one listener. Unknown model IDs return HTTP 400 with the supported catalog.

## Session state

Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/index.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: What is claude-code-proxy?
description: Run Claude Code with Codex, Kimi, Grok, or Cursor Agent through one local Anthropic-compatible proxy.
description: Run Claude Code with Codex, Kimi, Grok, OpenCode Go, or Cursor Agent through one local Anthropic-compatible proxy.
---

<div class="hero-copy">
claude-code-proxy lets you use Claude Code with Codex, Kimi, Grok, or Cursor Agent. Start one local app, choose a model, and keep working in the Claude Code interface you already know.
claude-code-proxy lets you use Claude Code with Codex, Kimi, Grok, OpenCode Go, or Cursor Agent. Start one local app, choose a model, and keep working in the Claude Code interface you already know.
</div>

<div class="route-rail" aria-label="Claude Code connects through the proxy to a supported provider">
Expand All @@ -21,7 +21,7 @@ claude-code-proxy lets you use Claude Code with Codex, Kimi, Grok, or Cursor Age
<div class="route-connector" aria-hidden="true"><span>→</span></div>
<div class="route-node route-providers">
<span class="route-kicker">Supported providers</span>
<div class="provider-stack"><span>Codex</span><span>Kimi</span><span>Grok</span><span>Cursor Agent</span></div>
<div class="provider-stack"><span>Codex</span><span>Kimi</span><span>Grok</span><span>OpenCode Go</span><span>Cursor Agent</span></div>
</div>
</div>

Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/providers/choosing-a-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ One `serve` process supports every provider. Choose based on the account you hav
| [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` | Function tools, reasoning, web search, X search, citations |
| [OpenCode Go](/providers/opencode-go/) | OpenCode Go workspace | OpenAI chat completions or Anthropic messages | Bare IDs and `opencode-go/<model-id>` forms | Function tools, reasoning, native streaming |
| [Cursor Agent](/providers/cursor-agent/) | Cursor account | HTTP/2 Connect stream | Cursor modes and `cursor:<model-id>` prefixes | Dynamic model catalog, effort variants, images, plan and ask modes, session continuation |

## Practical guidance

- Start with **Codex** when you have a ChatGPT subscription and want the most developed Claude Code translation path.
- Choose **Kimi** for the Kimi Code model and multimodal coding input.
- Choose **Grok** for Grok models and hosted web or X search.
- Choose **OpenCode Go** when you have a Go workspace key and want its model catalog in Claude Code.
- Choose **Cursor Agent** when you want Cursor's model catalog and agent modes. It depends on an installed Cursor Agent bundle for protobuf schemas.

## Shared behavior
Expand Down
53 changes: 53 additions & 0 deletions docs/src/content/docs/providers/opencode-go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: OpenCode Go
description: Configure an OpenCode Go workspace key, model routing, streaming, tools, and provider overrides.
---

OpenCode Go uses the API at `https://opencode.ai/zen/go/v1`. Its catalog is
split between OpenAI-compatible chat completions and Anthropic-compatible
messages; the proxy selects the wire protocol for each registered model.

## Account and authentication

Create a workspace key in the OpenCode console and provide it to the proxy:

```sh
export OPENCODE_API_KEY=sk-...
claude-code-proxy serve
```

`CCP_OPENCODE_API_KEY` takes precedence over `OPENCODE_API_KEY`. The
`opencode.apiKey` configuration key is also supported. The proxy does not
implement an OpenCode login flow.

## Models

Run `claude-code-proxy models` for the current registered catalog. Both bare
model IDs and provider-qualified forms are accepted:

```sh
ANTHROPIC_MODEL=opencode-go/glm-5.2 \
ANTHROPIC_SMALL_FAST_MODEL=opencode-go/glm-5.2 \
claude --model opencode-go/glm-5.2
```

The bare `kimi-k2.6` ID remains owned by the Kimi provider. Use
`opencode-go/kimi-k2.6` to select the OpenCode Go version.

## Tools and streaming

Claude function definitions, tool choices, tool calls, and tool results are
translated for chat-completions models. Tool-call argument fragments are
streamed incrementally and reassembled into Anthropic `tool_use` blocks, which
keeps Claude Code tool and subagent workflows functional.

Models served through the Anthropic-compatible endpoint retain their native
messages stream. Token counting remains local and does not consume Go usage.

## Configuration

- `CCP_OPENCODE_API_KEY`, `OPENCODE_API_KEY`, or `opencode.apiKey` supplies the key.
- `CCP_OPENCODE_BASE_URL` or `opencode.baseUrl` changes the API base URL.

Model availability and limits come from the OpenCode Go workspace. Unsupported
or inaccessible models return the upstream error to Claude Code.
12 changes: 12 additions & 0 deletions docs/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ These settings configure the proxy process. Claude Code client settings such as
"baseUrl": "https://cli-chat-proxy.grok.com/v1",
"clientVersion": "0.2.93"
},
"opencode": {
"apiKey": "sk-...",
"baseUrl": "https://opencode.ai/zen/go/v1"
},
"cursor": {
"baseUrl": "https://api2.cursor.sh",
"clientVersion": "0.48.5",
Expand Down Expand Up @@ -126,6 +130,14 @@ Proxy URLs may use `http`, `https`, `socks4`, `socks4a`, `socks5`, or `socks5h`.
| `CCP_GROK_CLIENT_VERSION` | `grok.clientVersion` | `0.2.93` | Changes the Grok client version header. |
| `CCP_GROK_TOOL_IMAGE` | none | `omit` | Selects `omit`, `reattach`, `inline`, or `reject` image handling. |

## OpenCode Go

| Environment | Config key | Default | Purpose |
| --- | --- | --- | --- |
| `CCP_OPENCODE_API_KEY` | `opencode.apiKey` | unset | OpenCode Go workspace API key; takes precedence over `OPENCODE_API_KEY` and config. |
| `OPENCODE_API_KEY` | `opencode.apiKey` | unset | Standard OpenCode Go key variable used when the CCP-specific variable is unset. |
| `CCP_OPENCODE_BASE_URL` | `opencode.baseUrl` | `https://opencode.ai/zen/go/v1` | Changes the OpenCode Go API base URL. |

## Cursor Agent

| Environment | Config key | Default | Purpose |
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/using/models-and-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The model ID in each request selects its provider. One proxy listener can serve
| Registered `gpt-*` IDs and their `-fast` forms | Codex |
| `kimi-for-coding`, `kimi-k2.6`, `k2.6` | Kimi |
| `grok-composer-2.5-fast`, `grok-4.5` | Grok |
| Registered OpenCode Go IDs and `opencode-go/<model-id>` | OpenCode Go |
| `cursor`, Cursor legacy aliases, `cursor:<id>`, `cursor-plan:<id>`, `cursor-ask:<id>` | Cursor Agent |
| Anthropic-style aliases such as `haiku`, `sonnet`, `opus`, `fable`, and registered `claude-*` aliases | The `aliasProvider`, Codex by default |

Expand Down
135 changes: 135 additions & 0 deletions src/anthropic/sse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,96 @@ pub struct SseParseStats {
pub event_count: usize,
}

pub const MAX_SSE_FRAME_BYTES: usize = 1024 * 1024;

#[derive(Default)]
pub struct SseDecoder {
frame: Vec<u8>,
line_start: usize,
skip_lf: bool,
}

impl SseDecoder {
pub fn push(&mut self, input: &[u8]) -> anyhow::Result<Vec<SseEvent>> {
let mut events = Vec::new();
for &byte in input {
if self.skip_lf {
self.skip_lf = false;
if byte == b'\n' {
continue;
}
}
match byte {
b'\n' => self.end_line(&mut events)?,
b'\r' => {
self.end_line(&mut events)?;
self.skip_lf = true;
}
_ => self.push_byte(byte)?,
}
}
Ok(events)
}

pub fn finish(&mut self) -> anyhow::Result<()> {
if self.frame.is_empty() {
Ok(())
} else {
anyhow::bail!("SSE stream ended with an incomplete frame")
}
}

fn push_byte(&mut self, byte: u8) -> anyhow::Result<()> {
if self.frame.len() >= MAX_SSE_FRAME_BYTES {
anyhow::bail!("SSE frame exceeds the size limit");
}
self.frame.push(byte);
Ok(())
}

fn end_line(&mut self, events: &mut Vec<SseEvent>) -> anyhow::Result<()> {
if self.frame.len() == self.line_start {
if !self.frame.is_empty()
&& let Some(event) = parse_incremental_frame(&self.frame)?
{
events.push(event);
}
self.frame.clear();
self.line_start = 0;
return Ok(());
}
self.push_byte(b'\n')?;
self.line_start = self.frame.len();
Ok(())
}
}

fn parse_incremental_frame(frame: &[u8]) -> anyhow::Result<Option<SseEvent>> {
let frame = std::str::from_utf8(frame)
.map_err(|_| anyhow::anyhow!("SSE frame contains invalid UTF-8"))?;
let mut event = None;
let mut data = Vec::new();
for line in frame.lines() {
if line.starts_with(':') {
continue;
}
let (field, value) = line.split_once(':').unwrap_or((line, ""));
let value = value.strip_prefix(' ').unwrap_or(value);
match field {
"event" => event = Some(value.to_owned()),
"data" => data.push(value),
_ => {}
}
}
if data.is_empty() {
return Ok(None);
}
Ok(Some(SseEvent {
event,
data: data.join("\n"),
}))
}

pub fn parse_sse_events(input: &[u8]) -> Vec<SseEvent> {
parse_sse_events_with_stats(input).0
}
Expand Down Expand Up @@ -128,3 +218,48 @@ fn parse_block(block: &str) -> Vec<SseEvent> {

events
}

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

#[test]
fn incremental_decoder_accepts_chunk_boundaries_and_crlf() {
let input = b": note\r\nevent: update\r\ndata: first\r\ndata: second\r\n\r\n";
let expected = vec![SseEvent {
event: Some("update".to_string()),
data: "first\nsecond".to_string(),
}];
for split in 0..=input.len() {
let mut decoder = SseDecoder::default();
let mut events = decoder.push(&input[..split]).unwrap();
events.extend(decoder.push(&input[split..]).unwrap());
decoder.finish().unwrap();
assert_eq!(events, expected, "split at byte {split}");
}
}

#[test]
fn incremental_decoder_rejects_invalid_incomplete_and_oversized_frames() {
assert!(SseDecoder::default().push(b"data: \xff\n\n").is_err());

let mut incomplete = SseDecoder::default();
incomplete.push(b"data: partial").unwrap();
assert!(incomplete.finish().is_err());

let mut oversized = SseDecoder::default();
let exact = vec![b'x'; MAX_SSE_FRAME_BYTES - b"data: \n".len()];
let mut frame = b"data: ".to_vec();
frame.extend(exact);
frame.extend(b"\n\n");
oversized.push(&frame).unwrap();
oversized.finish().unwrap();

let mut too_large = SseDecoder::default();
assert!(
too_large
.push(&vec![b'x'; MAX_SSE_FRAME_BYTES + 1])
.is_err()
);
}
}
Loading