- Project overview, system architecture, code standards - API reference with 15+ examples - Quick start guide with troubleshooting - Updated README with feature highlights and compatibility matrix
8.7 KiB
Code Standards & Architecture
Codebase Structure
src/
├── index.js # Hono app entry point, middleware setup
├── auth-middleware.js # Authentication logic, timing-safe comparison
├── openai-client.js # Cached OpenAI client, model mapping
├── transform-request.js # Anthropic → OpenAI request transformation
├── transform-response.js # OpenAI → Anthropic response streaming
└── routes/
└── messages.js # POST /v1/messages handler
Module Responsibilities
index.js
- Creates Hono application instance
- Registers middleware (logging, CORS)
- Mounts auth middleware for
/v1/*routes - Registers message routes
- Handles 404 and error cases
auth-middleware.js
-
timingSafeEqual(): Constant-time string comparison using byte-level XOR
- Works cross-platform: Node.js 18+, Cloudflare Workers, Deno, Bun
- No dependency on native crypto module (cross-platform safe)
- Takes two strings, returns boolean
-
authMiddleware(): Hono middleware factory
- Extracts token from
x-api-keyheader orAuthorization: Bearer - Compares against
GATEWAY_TOKENenv var using timing-safe comparison - Returns 401 if missing or invalid
- Returns 500 if GATEWAY_TOKEN not configured
- Extracts token from
openai-client.js
- Creates and caches OpenAI client instance
- Handles model name mapping via
MODEL_MAPenv var - Format:
claude-sonnet-4:gpt-4o,claude-3-opus:gpt-4-turbo - Falls back to model name from request if no mapping found
transform-request.js
Converts Anthropic Messages API request format → OpenAI Chat Completions format.
Main export: buildOpenAIRequest(anthropicRequest, model)
- Input: Anthropic request object + mapped model name
- Output: OpenAI request payload (plain object, not yet stringified)
Key transformations:
max_tokens,temperature,top_p: Pass through unchangedstream: If true, setsstream: trueandstream_options: { include_usage: true }stop_sequences: Maps to OpenAIstoparray parametertools: Converts Anthropic tool definitions to OpenAItoolsarray withtype: 'function'tool_choice: Maps Anthropic tool_choice enum to OpenAI tool_choice formatsystem: Handles both string and array of text blocksmessages: Transforms message array with special handling for content types
Message transformation details:
-
User messages: Handles text, images, and tool_result blocks
- Images: base64 and URL sources supported, converted to
image_urlformat - tool_result blocks: Split into separate tool messages (OpenAI format)
- Text content: Preserved in order
- Images: base64 and URL sources supported, converted to
-
Assistant messages: Handles text and tool_use blocks
- tool_use blocks: Converted to OpenAI tool_calls format
- Text content: Merged into
contentfield - Result: Single message with optional
tool_callsarray
Implementation notes:
- System message: Joins array blocks with
\n\nseparator - tool_result content: Supports string or array of text blocks; prepends
[ERROR]ifis_error: true - Filters out unsupported blocks (thinking, cache_control, etc.)
transform-response.js
Converts OpenAI Chat Completions responses → Anthropic Messages API format.
Exports:
-
transformResponse(openaiResponse, anthropicRequest): Non-streaming response conversion
- Input: OpenAI response object, original Anthropic request
- Output: Anthropic message response object with
id,type,role,content,stop_reason,usage
-
streamAnthropicResponse(c, openaiStream, anthropicRequest): Streaming response handler
- Input: Hono context, async iterable of OpenAI chunks, original Anthropic request
- Outputs: Server-sent events in Anthropic SSE format
- Emits:
message_start→ content blocks →message_delta→message_stop
Response building:
- Content blocks: Anthropic format uses array of content objects with
typefieldtext: Standard text contenttool_use: Tool calls withid,name,input(parsed JSON object)
Stop reason mapping:
finish_reason: 'stop'→'end_turn'(or'stop_sequence'if stop_sequences were used)finish_reason: 'length'→'max_tokens'finish_reason: 'tool_calls'→'tool_use'finish_reason: 'content_filter'→'end_turn'
Streaming behavior:
- Sends
message_startevent with empty content array - For text delta: Sends
content_block_start, thencontent_block_deltaevents - For tool_calls delta: Sends
content_block_start, thencontent_block_deltawithinput_json_delta - Tracks text and tool blocks separately to avoid mixing in output
- Closes blocks before transitioning between text and tool content
- Captures usage from final chunk (requires
stream_options.include_usage) - Sends
message_deltawith stop_reason and output tokens - Sends
message_stopto mark stream end
Implementation notes:
- Tool call buffering: Accumulates arguments across multiple chunks before outputting deltas
- Block indexing: Separate indices for text blocks (0-n) and tool blocks (offset by text count)
- Tool result content extraction: Handles string or text-block-array formats
routes/messages.js
HTTP handler for POST /v1/messages.
Request flow:
- Extract
modelfrom body - Map model name via
openai-client - Build OpenAI request via
transform-request - If streaming: Use
streamAnthropicResponse(), setContent-Type: text/event-stream - If non-streaming: Transform response via
transformResponse()
Error handling:
- Catches OpenAI API errors, returns formatted Anthropic error response
- Catches transform errors, returns 400 Bad Request
Naming Conventions
Functions
- camelCase:
buildOpenAIRequest,timingSafeEqual,transformMessages - Descriptive verbs: build, transform, map, extract, handle
- Prefixes for private functions: None (all functions are internal to modules)
Variables
- camelCase:
messageId,toolCallBuffers,inputTokens - Constants: UPPERCASE with underscores for env vars only (
GATEWAY_TOKEN,OPENAI_API_KEY,MODEL_MAP) - Booleans: Prefix with
is,had,should:isError,hadStopSequences,textBlockStarted
Files
- kebab-case with descriptive names:
auth-middleware.js,transform-request.js,transform-response.js - Purpose clear from name: No abbreviations
Error Handling
Authentication Failures
- 401 Unauthorized: Invalid or missing token
- 500 Internal Server Error: GATEWAY_TOKEN not configured
API Errors
- Forward OpenAI errors to client in Anthropic error format
- Log error details for debugging
- Return 500 for unexpected errors
Transform Errors
- Catch JSON parsing errors (tool arguments)
- Provide fallback values (empty objects, empty strings)
- Log parsing failures with context
Security Practices
- Timing-Safe Authentication:
timingSafeEqual()prevents timing attacks - Header Validation: Checks both
x-api-keyandAuthorizationheaders - Token Comparison: Constant-time comparison regardless of token length
- No Logging of Sensitive Data: Auth tokens not logged
Testing Strategy
Unit Tests (Recommended)
- Test transformations with sample Anthropic/OpenAI payloads
- Test edge cases: empty messages, tool calls without text, images only
- Test error scenarios: malformed JSON, missing required fields
- Test utility functions:
timingSafeEqual,mapStopReason
Integration Tests (Recommended)
- Mock OpenAI API responses
- Test full request/response cycle with streaming and non-streaming
- Test model mapping
Manual Testing
- Deploy to Vercel/Cloudflare and test with Claude Code
- Verify streaming works correctly
- Test tool use workflows (request → tool_use → tool_result → response)
Performance Considerations
- Client Caching: OpenAI client created once and reused
- Streaming Efficiency: Response streamed directly from OpenAI to client (no buffering)
- String Operations: Minimal string concatenation, uses joins for system message
- JSON Parsing: Lazy parsed only when needed (tool arguments)
Compatibility Notes
- Runtime: Works on Node.js 18+, Cloudflare Workers, Deno, Bun (via Hono)
- APIs: Uses standard JavaScript TextEncoder (not Node.js crypto for auth)
- Framework: Hono provides multi-platform support, no custom server implementation
Code Quality Standards
- No External Dependencies: Only Hono for framework (included in package.json)
- Readable Over Clever: Prefer explicit logic over compact code
- Comments for Non-Obvious Logic: Transformation rules, SSE event sequencing
- Self-Documenting Names: Function names describe purpose, no abbreviations
- Modular Structure: Single responsibility per file