# 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: ```yaml 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 ` 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` -> `model` - `temperature` -> `temperature` - `max_tokens` -> `max_tokens` (default 32000 if omitted) - `stream` -> `stream` - `top_p` -> `top_p` - `stop` -> `stop_sequences` ### Messages - OpenAI `messages[]` with `role` and `content` mapped to Anthropic `messages[]` - `system` message (role="system") extracted to top-level `system` field - 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_choice` mapped: `auto`/`none`/`required` + specific tool selection ## Response Conversion (Anthropic -> OpenAI) ### Non-streaming - `content[0].text` -> `choices[0].message.content` - `content[].type == "tool_use"` -> `choices[0].message.tool_calls[]` - `usage.input_tokens` -> `usage.prompt_tokens` - `usage.output_tokens` -> `usage.completion_tokens` - `stop_reason` mapped: `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`, `content` deltas, `tool_calls` deltas, `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