3.9 KiB
OpenAI-to-Anthropic Proxy Design (proxx)
Purpose
A Go proxy that exposes OpenAI-compatible API endpoints, converts requests to Anthropic format, and forwards them to a configurable upstream Anthropic endpoint. The proxy mimics claude-code (the CLI tool) so upstream sees requests as originating from claude-code.
Architecture
Client (OpenAI format) --> proxx (Go :8080) --> Upstream Anthropic endpoint
e.g. https://api.z.ai/api/anthropic
Single binary using Go standard library net/http. Only external dependency: gopkg.in/yaml.v3 for config parsing.
Configuration
Config loaded from config.yaml in the working directory:
port: 8080
upstream_url: "https://api.z.ai/api/anthropic"
Endpoints
POST /v1/chat/completions
Accepts OpenAI chat completion format. Converts to Anthropic POST /v1/messages. Supports both streaming and non-streaming modes.
GET /v1/models
Returns a static list of Claude models in OpenAI /v1/models response format. No upstream call needed.
Authentication
The API key is extracted from the incoming request's Authorization: Bearer <key> header and forwarded to the upstream as the x-api-key header.
Claude-Code Mimicry
The proxy sets these headers on all upstream requests:
| Header | Value |
|---|---|
User-Agent |
claude-cli/1.0.18 (pro, cli) |
x-api-key |
From incoming Bearer token |
x-app |
cli |
anthropic-version |
2023-06-01 |
anthropic-beta |
interleaved-thinking-2025-05-14,prompt-caching-scope-2026-01-05,context-management-2025-06-27 |
X-Claude-Code-Session-Id |
Random UUID generated at startup |
content-type |
application/json |
Request Conversion (OpenAI -> Anthropic)
Fields mapped directly
model->modeltemperature->temperaturemax_tokens->max_tokens(default 32000 if omitted)stream->streamtop_p->top_pstop->stop_sequences
Messages
- OpenAI
messages[]withroleandcontentmapped to Anthropicmessages[] systemmessage (role="system") extracted to top-levelsystemfield- String content mapped directly; array content (text+image parts) mapped to Anthropic content block format
Tools
- OpenAI function tools converted to Anthropic tool format (
name,description,input_schema) tool_choicemapped:auto/none/required+ specific tool selection
Response Conversion (Anthropic -> OpenAI)
Non-streaming
content[0].text->choices[0].message.contentcontent[].type == "tool_use"->choices[0].message.tool_calls[]usage.input_tokens->usage.prompt_tokensusage.output_tokens->usage.completion_tokensstop_reasonmapped:end_turn->stop,tool_use->tool_calls,max_tokens->length
Streaming
- Anthropic SSE events (
message_start,content_block_start,content_block_delta,content_block_stop,message_delta,message_stop) converted to OpenAI SSE format (role,contentdeltas,tool_callsdeltas,finish_reason) - Final
data: [DONE]sent after stream ends
Error Responses
- Upstream errors converted to OpenAI error format:
{"error": {"message": "...", "type": "...", "code": "..."}} - Connection failures: HTTP 502
- Malformed requests: HTTP 400
Project Structure
proxx/
├── main.go # Entry point, config loading, HTTP server setup
├── handler.go # HTTP handlers for /v1/chat/completions and /v1/models
├── converter.go # OpenAI <-> Anthropic format conversion logic
├── types.go # All request/response struct types
├── streaming.go # SSE streaming conversion (Anthropic -> OpenAI)
├── config.yaml # Default configuration file
├── go.mod
└── go.sum
Testing
- Unit tests for the converter functions (pure logic, no HTTP)
- Integration test with a mock upstream server to verify end-to-end flow
- Streaming test to verify SSE event conversion