Implement OpenAI-to-Anthropic proxy with streaming support

- Add request/response converters (OpenAI <-> Anthropic formats)
- Implement SSE streaming conversion (Anthropic events -> OpenAI SSE)
- Add /v1/models endpoint with Claude model list
- Add /v1/chat/completions endpoint with streaming and non-streaming support
- Fix context key type matching bug (sessionIDKey)
- Configurable upstream URL via config.yaml
- Mimic claude-code CLI headers for upstream requests
This commit is contained in:
Franz Kafka 2026-04-15 06:29:03 +00:00
parent 87a74edbf5
commit 8450d96e2e
10 changed files with 1270 additions and 0 deletions

68
docs/superpowers/PLAN.md Normal file
View file

@ -0,0 +1,68 @@
# proxx Implementation Plan
## Goal
A working Go binary that proxies OpenAI-format requests to an Anthropic endpoint, with full streaming and non-streaming support.
## Approach
Straightforward per the spec. Go 1.21+ with only `gopkg.in/yaml.v3` as external dependency. Standard library `net/http` for everything HTTP-related.
## Steps
### Phase 1: Project Scaffolding
- [ ] Initialize `go mod init github.com/penal-colony/proxx`
- [ ] Create directory structure: `main.go`, `handler.go`, `converter.go`, `types.go`, `streaming.go`, `config.yaml`
- [ ] Add `gopkg.in/yaml.v3` dependency
### Phase 2: Types (`types.go`)
- [ ] OpenAI request/response structs (`ChatCompletionRequest`, `ChatCompletionResponse`, `Message`, `Choice`, `Usage`)
- [ ] Anthropic request/response structs (`AnthropicRequest`, `AnthropicResponse`, `ContentBlock`, `Message`, `Usage`)
- [ ] Tool/function conversion types
- [ ] Streaming event structs (Anthropic SSE + OpenAI SSE)
### Phase 3: Converter (`converter.go`)
- [ ] `convertRequest()` — map OpenAI req to Anthropic req (model, messages, system, tools, temperature, top_p, max_tokens, stop_sequences)
- [ ] `extractSystemMessage()` — pull role=system out of messages into top-level system field
- [ ] `convertTools()` — map OpenAI function tools to Anthropic tool format
- [ ] `convertResponse()` — map Anthropic non-streaming response to OpenAI format
- [ ] `mapStopReason()` — end_turn→stop, tool_use→tool_calls, max_tokens→length
### Phase 4: Streaming (`streaming.go`)
- [ ] `StreamConverter` struct holding accumulated state
- [ ] `HandleAnthropicStream()` — parse SSE events, convert to OpenAI SSE format
- [ ] Handle: `message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`
- [ ] Map: role, content deltas, tool_use deltas, finish_reason
- [ ] Send `data: [DONE]` on stream end
### Phase 5: HTTP Handlers (`handler.go`)
- [ ] `handleModels()` — static list of Claude models in OpenAI format
- [ ] `handleChatCompletions()` — routing: detect streaming vs non-streaming, call upstream, return converted response
- [ ] Extract `Authorization: Bearer` → forward as `x-api-key`
- [ ] Set Claude-Code mimicry headers on upstream requests
- [ ] Error handling: 400 for bad requests, 502 for upstream failures
### Phase 6: Entry Point (`main.go`)
- [ ] Load `config.yaml` via `gopkg.in/yaml.v3`
- [ ] Register handlers on `/v1/chat/completions` and `/v1/models`
- [ ] Start HTTP server on configured port
- [ ] Generate random `X-Claude-Code-Session-Id` UUID at startup
### Phase 7: Config
- [ ] Create `config.yaml` with defaults (port 8080, upstream_url)
### Phase 8: Testing
- [ ] Unit tests for `converter.go` (pure logic, no HTTP)
- [ ] Unit tests for `streaming.go` (test SSE event conversion)
- [ ] Integration test with mock upstream
## Risks
- **SSE parsing**: Anthropic uses SSE format, need to handle `data:` lines correctly. Risk: low, well-documented format.
- **Tool calling conversion**: Complex nested structure mapping. Risk: medium — need to verify edge cases.
- **Streaming state machine**: Accumulating partial tool_use blocks across multiple events. Risk: medium — test with actual stream.
## Definition of Done
- [ ] `go build` produces a binary with no errors
- [ ] Unit tests pass for converter and streaming logic
- [ ] Binary starts, loads config, listens on port
- [ ] `/v1/models` returns Claude model list in OpenAI format
- [ ] Non-streaming `/v1/chat/completions` round-trips correctly through Anthropic upstream
- [ ] Streaming `/v1/chat/completions` produces valid OpenAI SSE output