├── .gitignore ├── README.md ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anthropic-proxy 2 | 3 | A proxy server that transforms Anthropic API requests to OpenAI format and sends it to openrouter.ai. This enables you to use Anthropic's API format while connecting to OpenAI-compatible endpoints. 4 | 5 | ## Usage 6 | 7 | With this command, you can start the proxy server with your OpenRouter API key on port 3000: 8 | 9 | ```bash 10 | OPENROUTER_API_KEY=your-api-key npx anthropic-proxy 11 | ``` 12 | 13 | Environment variables: 14 | 15 | - `OPENROUTER_API_KEY`: Your OpenRouter API key (required when using OpenRouter) 16 | - `ANTHROPIC_PROXY_BASE_URL`: Custom base URL for the transformed OpenAI-format message (default: `openrouter.ai`) 17 | - `PORT`: The port the proxy server should listen on (default: 3000) 18 | - `REASONING_MODEL`: The reasoning model to use (default: `google/gemini-2.0-pro-exp-02-05:free`) 19 | - `COMPLETION_MODEL`: The completion model to use (default: `google/gemini-2.0-pro-exp-02-05:free`) 20 | - `DEBUG`: Set to `1` to enable debug logging 21 | 22 | Note: When `ANTHROPIC_PROXY_BASE_URL` is set to a custom URL, the `OPENROUTER_API_KEY` is not required. 23 | 24 | ## Claude Code 25 | 26 | To use the proxy server as a backend for Claude Code, you have to set the `ANTHROPIC_BASE_URL` to the URL of the proxy server: 27 | 28 | ```bash 29 | ANTHROPIC_BASE_URL=http://0.0.0.0:3000 claude 30 | ``` 31 | 32 | ## License 33 | Licensed under MIT license. Copyright (c) 2025 Max Nowack 34 | 35 | ## Contributions 36 | Contributions are welcome. Please open issues and/or file Pull Requests. 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import Fastify from 'fastify' 3 | import { TextDecoder } from 'util' 4 | 5 | const baseUrl = process.env.ANTHROPIC_PROXY_BASE_URL || 'https://openrouter.ai/api' 6 | const requiresApiKey = !process.env.ANTHROPIC_PROXY_BASE_URL 7 | const key = requiresApiKey ? process.env.OPENROUTER_API_KEY : null 8 | const model = 'google/gemini-2.0-pro-exp-02-05:free' 9 | const models = { 10 | reasoning: process.env.REASONING_MODEL || model, 11 | completion: process.env.COMPLETION_MODEL || model, 12 | } 13 | 14 | const fastify = Fastify({ 15 | logger: true 16 | }) 17 | function debug(...args) { 18 | if (!process.env.DEBUG) return 19 | console.log(...args) 20 | } 21 | 22 | // Helper function to send SSE events and flush immediately. 23 | const sendSSE = (reply, event, data) => { 24 | const sseMessage = `event: ${event}\n` + 25 | `data: ${JSON.stringify(data)}\n\n` 26 | reply.raw.write(sseMessage) 27 | // Flush if the flush method is available. 28 | if (typeof reply.raw.flush === 'function') { 29 | reply.raw.flush() 30 | } 31 | } 32 | 33 | function mapStopReason(finishReason) { 34 | switch (finishReason) { 35 | case 'tool_calls': return 'tool_use' 36 | case 'stop': return 'end_turn' 37 | case 'length': return 'max_tokens' 38 | default: return 'end_turn' 39 | } 40 | } 41 | 42 | fastify.post('/v1/messages', async (request, reply) => { 43 | try { 44 | const payload = request.body 45 | 46 | // Helper to normalize a message's content. 47 | // If content is a string, return it directly. 48 | // If it's an array (of objects with text property), join them. 49 | const normalizeContent = (content) => { 50 | if (typeof content === 'string') return content 51 | if (Array.isArray(content)) { 52 | return content.map(item => item.text).join(' ') 53 | } 54 | return null 55 | } 56 | 57 | // Build messages array for the OpenAI payload. 58 | // Start with system messages if provided. 59 | const messages = [] 60 | if (payload.system && Array.isArray(payload.system)) { 61 | payload.system.forEach(sysMsg => { 62 | const normalized = normalizeContent(sysMsg.text || sysMsg.content) 63 | if (normalized) { 64 | messages.push({ 65 | role: 'system', 66 | content: normalized 67 | }) 68 | } 69 | }) 70 | } 71 | // Then add user (or other) messages. 72 | if (payload.messages && Array.isArray(payload.messages)) { 73 | payload.messages.forEach(msg => { 74 | const toolCalls = (Array.isArray(msg.content) ? msg.content : []).filter(item => item.type === 'tool_use').map(toolCall => ({ 75 | function: { 76 | type: 'function', 77 | id: toolCall.id, 78 | function: { 79 | name: toolCall.name, 80 | parameters: toolCall.input, 81 | }, 82 | } 83 | })) 84 | const newMsg = { role: msg.role } 85 | const normalized = normalizeContent(msg.content) 86 | if (normalized) newMsg.content = normalized 87 | if (toolCalls.length > 0) newMsg.tool_calls = toolCalls 88 | if (newMsg.content || newMsg.tool_calls) messages.push(newMsg) 89 | 90 | if (Array.isArray(msg.content)) { 91 | const toolResults = msg.content.filter(item => item.type === 'tool_result') 92 | toolResults.forEach(toolResult => { 93 | messages.push({ 94 | role: 'tool', 95 | content: toolResult.text || toolResult.content, 96 | tool_call_id: toolResult.tool_use_id, 97 | }) 98 | }) 99 | } 100 | }) 101 | } 102 | 103 | // Prepare the OpenAI payload. 104 | // Helper function to recursively traverse JSON schema and remove format: 'uri' 105 | const removeUriFormat = (schema) => { 106 | if (!schema || typeof schema !== 'object') return schema; 107 | 108 | // If this is a string type with uri format, remove the format 109 | if (schema.type === 'string' && schema.format === 'uri') { 110 | const { format, ...rest } = schema; 111 | return rest; 112 | } 113 | 114 | // Handle array of schemas (like in anyOf, allOf, oneOf) 115 | if (Array.isArray(schema)) { 116 | return schema.map(item => removeUriFormat(item)); 117 | } 118 | 119 | // Recursively process all properties 120 | const result = {}; 121 | for (const key in schema) { 122 | if (key === 'properties' && typeof schema[key] === 'object') { 123 | result[key] = {}; 124 | for (const propKey in schema[key]) { 125 | result[key][propKey] = removeUriFormat(schema[key][propKey]); 126 | } 127 | } else if (key === 'items' && typeof schema[key] === 'object') { 128 | result[key] = removeUriFormat(schema[key]); 129 | } else if (key === 'additionalProperties' && typeof schema[key] === 'object') { 130 | result[key] = removeUriFormat(schema[key]); 131 | } else if (['anyOf', 'allOf', 'oneOf'].includes(key) && Array.isArray(schema[key])) { 132 | result[key] = schema[key].map(item => removeUriFormat(item)); 133 | } else { 134 | result[key] = removeUriFormat(schema[key]); 135 | } 136 | } 137 | return result; 138 | }; 139 | 140 | const tools = (payload.tools || []).filter(tool => !['BatchTool'].includes(tool.name)).map(tool => ({ 141 | type: 'function', 142 | function: { 143 | name: tool.name, 144 | description: tool.description, 145 | parameters: removeUriFormat(tool.input_schema), 146 | }, 147 | })) 148 | const openaiPayload = { 149 | model: payload.thinking ? models.reasoning : models.completion, 150 | messages, 151 | max_tokens: payload.max_tokens, 152 | temperature: payload.temperature !== undefined ? payload.temperature : 1, 153 | stream: payload.stream === true, 154 | } 155 | if (tools.length > 0) openaiPayload.tools = tools 156 | debug('OpenAI payload:', openaiPayload) 157 | 158 | const headers = { 159 | 'Content-Type': 'application/json' 160 | } 161 | 162 | if (requiresApiKey) { 163 | headers['Authorization'] = `Bearer ${key}` 164 | } 165 | 166 | const openaiResponse = await fetch(`${baseUrl}/v1/chat/completions`, { 167 | method: 'POST', 168 | headers, 169 | body: JSON.stringify(openaiPayload) 170 | }); 171 | 172 | if (!openaiResponse.ok) { 173 | const errorDetails = await openaiResponse.text() 174 | reply.code(openaiResponse.status) 175 | return { error: errorDetails } 176 | } 177 | 178 | // If stream is not enabled, process the complete response. 179 | if (!openaiPayload.stream) { 180 | const data = await openaiResponse.json() 181 | debug('OpenAI response:', data) 182 | if (data.error) { 183 | throw new Error(data.error.message) 184 | } 185 | 186 | 187 | const choice = data.choices[0] 188 | const openaiMessage = choice.message 189 | 190 | // Map finish_reason to anthropic stop_reason. 191 | const stopReason = mapStopReason(choice.finish_reason) 192 | const toolCalls = openaiMessage.tool_calls || [] 193 | 194 | // Create a message id; if available, replace prefix, otherwise generate one. 195 | const messageId = data.id 196 | ? data.id.replace('chatcmpl', 'msg') 197 | : 'msg_' + Math.random().toString(36).substr(2, 24) 198 | 199 | const anthropicResponse = { 200 | content: [ 201 | { 202 | text: openaiMessage.content, 203 | type: 'text' 204 | }, 205 | ...toolCalls.map(toolCall => ({ 206 | type: 'tool_use', 207 | id: toolCall.id, 208 | name: toolCall.function.name, 209 | input: JSON.parse(toolCall.function.arguments), 210 | })), 211 | ], 212 | id: messageId, 213 | model: openaiPayload.model, 214 | role: openaiMessage.role, 215 | stop_reason: stopReason, 216 | stop_sequence: null, 217 | type: 'message', 218 | usage: { 219 | input_tokens: data.usage 220 | ? data.usage.prompt_tokens 221 | : messages.reduce((acc, msg) => acc + msg.content.split(' ').length, 0), 222 | output_tokens: data.usage 223 | ? data.usage.completion_tokens 224 | : openaiMessage.content.split(' ').length, 225 | } 226 | } 227 | 228 | return anthropicResponse 229 | } 230 | 231 | 232 | let isSucceeded = false 233 | function sendSuccessMessage() { 234 | if (isSucceeded) return 235 | isSucceeded = true 236 | 237 | // Streaming response using Server-Sent Events. 238 | reply.raw.writeHead(200, { 239 | 'Content-Type': 'text/event-stream', 240 | 'Cache-Control': 'no-cache', 241 | Connection: 'keep-alive' 242 | }) 243 | 244 | // Create a unique message id. 245 | const messageId = 'msg_' + Math.random().toString(36).substr(2, 24) 246 | 247 | // Send initial SSE event for message start. 248 | sendSSE(reply, 'message_start', { 249 | type: 'message_start', 250 | message: { 251 | id: messageId, 252 | type: 'message', 253 | role: 'assistant', 254 | model: openaiPayload.model, 255 | content: [], 256 | stop_reason: null, 257 | stop_sequence: null, 258 | usage: { input_tokens: 0, output_tokens: 0 }, 259 | } 260 | }) 261 | 262 | // Send initial ping. 263 | sendSSE(reply, 'ping', { type: 'ping' }) 264 | } 265 | 266 | // Prepare for reading streamed data. 267 | let accumulatedContent = '' 268 | let accumulatedReasoning = '' 269 | let usage = null 270 | let textBlockStarted = false 271 | let encounteredToolCall = false 272 | const toolCallAccumulators = {} // key: tool call index, value: accumulated arguments string 273 | const decoder = new TextDecoder('utf-8') 274 | const reader = openaiResponse.body.getReader() 275 | let done = false 276 | 277 | while (!done) { 278 | const { value, done: doneReading } = await reader.read() 279 | done = doneReading 280 | if (value) { 281 | const chunk = decoder.decode(value) 282 | debug('OpenAI response chunk:', chunk) 283 | // OpenAI streaming responses are typically sent as lines prefixed with "data: " 284 | const lines = chunk.split('\n') 285 | 286 | 287 | for (const line of lines) { 288 | const trimmed = line.trim() 289 | if (trimmed === '' || !trimmed.startsWith('data:')) continue 290 | const dataStr = trimmed.replace(/^data:\s*/, '') 291 | if (dataStr === '[DONE]') { 292 | // Finalize the stream with stop events. 293 | if (encounteredToolCall) { 294 | for (const idx in toolCallAccumulators) { 295 | sendSSE(reply, 'content_block_stop', { 296 | type: 'content_block_stop', 297 | index: parseInt(idx, 10) 298 | }) 299 | } 300 | } else if (textBlockStarted) { 301 | sendSSE(reply, 'content_block_stop', { 302 | type: 'content_block_stop', 303 | index: 0 304 | }) 305 | } 306 | sendSSE(reply, 'message_delta', { 307 | type: 'message_delta', 308 | delta: { 309 | stop_reason: encounteredToolCall ? 'tool_use' : 'end_turn', 310 | stop_sequence: null 311 | }, 312 | usage: usage 313 | ? { output_tokens: usage.completion_tokens } 314 | : { output_tokens: accumulatedContent.split(' ').length + accumulatedReasoning.split(' ').length } 315 | }) 316 | sendSSE(reply, 'message_stop', { 317 | type: 'message_stop' 318 | }) 319 | reply.raw.end() 320 | return 321 | } 322 | 323 | const parsed = JSON.parse(dataStr) 324 | if (parsed.error) { 325 | throw new Error(parsed.error.message) 326 | } 327 | sendSuccessMessage() 328 | // Capture usage if available. 329 | if (parsed.usage) { 330 | usage = parsed.usage 331 | } 332 | const delta = parsed.choices[0].delta 333 | if (delta && delta.tool_calls) { 334 | for (const toolCall of delta.tool_calls) { 335 | encounteredToolCall = true 336 | const idx = toolCall.index 337 | if (toolCallAccumulators[idx] === undefined) { 338 | toolCallAccumulators[idx] = "" 339 | sendSSE(reply, 'content_block_start', { 340 | type: 'content_block_start', 341 | index: idx, 342 | content_block: { 343 | type: 'tool_use', 344 | id: toolCall.id, 345 | name: toolCall.function.name, 346 | input: {} 347 | } 348 | }) 349 | } 350 | const newArgs = toolCall.function.arguments || "" 351 | const oldArgs = toolCallAccumulators[idx] 352 | if (newArgs.length > oldArgs.length) { 353 | const deltaText = newArgs.substring(oldArgs.length) 354 | sendSSE(reply, 'content_block_delta', { 355 | type: 'content_block_delta', 356 | index: idx, 357 | delta: { 358 | type: 'input_json_delta', 359 | partial_json: deltaText 360 | } 361 | }) 362 | toolCallAccumulators[idx] = newArgs 363 | } 364 | } 365 | } else if (delta && delta.content) { 366 | if (!textBlockStarted) { 367 | textBlockStarted = true 368 | sendSSE(reply, 'content_block_start', { 369 | type: 'content_block_start', 370 | index: 0, 371 | content_block: { 372 | type: 'text', 373 | text: '' 374 | } 375 | }) 376 | } 377 | accumulatedContent += delta.content 378 | sendSSE(reply, 'content_block_delta', { 379 | type: 'content_block_delta', 380 | index: 0, 381 | delta: { 382 | type: 'text_delta', 383 | text: delta.content 384 | } 385 | }) 386 | } else if (delta && delta.reasoning) { 387 | if (!textBlockStarted) { 388 | textBlockStarted = true 389 | sendSSE(reply, 'content_block_start', { 390 | type: 'content_block_start', 391 | index: 0, 392 | content_block: { 393 | type: 'text', 394 | text: '' 395 | } 396 | }) 397 | } 398 | accumulatedReasoning += delta.reasoning 399 | sendSSE(reply, 'content_block_delta', { 400 | type: 'content_block_delta', 401 | index: 0, 402 | delta: { 403 | type: 'thinking_delta', 404 | thinking: delta.reasoning 405 | } 406 | }) 407 | } 408 | } 409 | } 410 | } 411 | 412 | reply.raw.end() 413 | } catch (err) { 414 | console.error(err) 415 | reply.code(500) 416 | return { error: err.message } 417 | } 418 | }) 419 | 420 | const start = async () => { 421 | try { 422 | await fastify.listen({ port: process.env.PORT || 3000 }) 423 | } catch (err) { 424 | process.exit(1) 425 | } 426 | } 427 | 428 | start() 429 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anthropic-proxy", 3 | "version": "1.3.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "anthropic-proxy", 9 | "version": "1.3.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "fastify": "^5.2.1" 13 | } 14 | }, 15 | "node_modules/@fastify/ajv-compiler": { 16 | "version": "4.0.2", 17 | "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz", 18 | "integrity": "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==", 19 | "funding": [ 20 | { 21 | "type": "github", 22 | "url": "https://github.com/sponsors/fastify" 23 | }, 24 | { 25 | "type": "opencollective", 26 | "url": "https://opencollective.com/fastify" 27 | } 28 | ], 29 | "license": "MIT", 30 | "dependencies": { 31 | "ajv": "^8.12.0", 32 | "ajv-formats": "^3.0.1", 33 | "fast-uri": "^3.0.0" 34 | } 35 | }, 36 | "node_modules/@fastify/error": { 37 | "version": "4.1.0", 38 | "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.1.0.tgz", 39 | "integrity": "sha512-KeFcciOr1eo/YvIXHP65S94jfEEqn1RxTRBT1aJaHxY5FK0/GDXYozsQMMWlZoHgi8i0s+YtrLsgj/JkUUjSkQ==", 40 | "funding": [ 41 | { 42 | "type": "github", 43 | "url": "https://github.com/sponsors/fastify" 44 | }, 45 | { 46 | "type": "opencollective", 47 | "url": "https://opencollective.com/fastify" 48 | } 49 | ], 50 | "license": "MIT" 51 | }, 52 | "node_modules/@fastify/fast-json-stringify-compiler": { 53 | "version": "5.0.2", 54 | "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.2.tgz", 55 | "integrity": "sha512-YdR7gqlLg1xZAQa+SX4sMNzQHY5pC54fu9oC5aYSUqBhyn6fkLkrdtKlpVdCNPlwuUuXA1PjFTEmvMF6ZVXVGw==", 56 | "funding": [ 57 | { 58 | "type": "github", 59 | "url": "https://github.com/sponsors/fastify" 60 | }, 61 | { 62 | "type": "opencollective", 63 | "url": "https://opencollective.com/fastify" 64 | } 65 | ], 66 | "license": "MIT", 67 | "dependencies": { 68 | "fast-json-stringify": "^6.0.0" 69 | } 70 | }, 71 | "node_modules/@fastify/forwarded": { 72 | "version": "3.0.0", 73 | "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz", 74 | "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", 75 | "license": "MIT" 76 | }, 77 | "node_modules/@fastify/merge-json-schemas": { 78 | "version": "0.2.1", 79 | "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", 80 | "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", 81 | "funding": [ 82 | { 83 | "type": "github", 84 | "url": "https://github.com/sponsors/fastify" 85 | }, 86 | { 87 | "type": "opencollective", 88 | "url": "https://opencollective.com/fastify" 89 | } 90 | ], 91 | "license": "MIT", 92 | "dependencies": { 93 | "dequal": "^2.0.3" 94 | } 95 | }, 96 | "node_modules/@fastify/proxy-addr": { 97 | "version": "5.0.0", 98 | "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", 99 | "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", 100 | "license": "MIT", 101 | "dependencies": { 102 | "@fastify/forwarded": "^3.0.0", 103 | "ipaddr.js": "^2.1.0" 104 | } 105 | }, 106 | "node_modules/abstract-logging": { 107 | "version": "2.0.1", 108 | "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", 109 | "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", 110 | "license": "MIT" 111 | }, 112 | "node_modules/ajv": { 113 | "version": "8.17.1", 114 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", 115 | "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", 116 | "license": "MIT", 117 | "dependencies": { 118 | "fast-deep-equal": "^3.1.3", 119 | "fast-uri": "^3.0.1", 120 | "json-schema-traverse": "^1.0.0", 121 | "require-from-string": "^2.0.2" 122 | }, 123 | "funding": { 124 | "type": "github", 125 | "url": "https://github.com/sponsors/epoberezkin" 126 | } 127 | }, 128 | "node_modules/ajv-formats": { 129 | "version": "3.0.1", 130 | "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", 131 | "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", 132 | "license": "MIT", 133 | "dependencies": { 134 | "ajv": "^8.0.0" 135 | }, 136 | "peerDependencies": { 137 | "ajv": "^8.0.0" 138 | }, 139 | "peerDependenciesMeta": { 140 | "ajv": { 141 | "optional": true 142 | } 143 | } 144 | }, 145 | "node_modules/atomic-sleep": { 146 | "version": "1.0.0", 147 | "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 148 | "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", 149 | "license": "MIT", 150 | "engines": { 151 | "node": ">=8.0.0" 152 | } 153 | }, 154 | "node_modules/avvio": { 155 | "version": "9.1.0", 156 | "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", 157 | "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", 158 | "license": "MIT", 159 | "dependencies": { 160 | "@fastify/error": "^4.0.0", 161 | "fastq": "^1.17.1" 162 | } 163 | }, 164 | "node_modules/cookie": { 165 | "version": "1.0.2", 166 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", 167 | "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", 168 | "license": "MIT", 169 | "engines": { 170 | "node": ">=18" 171 | } 172 | }, 173 | "node_modules/dequal": { 174 | "version": "2.0.3", 175 | "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", 176 | "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", 177 | "license": "MIT", 178 | "engines": { 179 | "node": ">=6" 180 | } 181 | }, 182 | "node_modules/fast-decode-uri-component": { 183 | "version": "1.0.1", 184 | "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", 185 | "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", 186 | "license": "MIT" 187 | }, 188 | "node_modules/fast-deep-equal": { 189 | "version": "3.1.3", 190 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 191 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 192 | "license": "MIT" 193 | }, 194 | "node_modules/fast-json-stringify": { 195 | "version": "6.0.1", 196 | "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz", 197 | "integrity": "sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==", 198 | "funding": [ 199 | { 200 | "type": "github", 201 | "url": "https://github.com/sponsors/fastify" 202 | }, 203 | { 204 | "type": "opencollective", 205 | "url": "https://opencollective.com/fastify" 206 | } 207 | ], 208 | "license": "MIT", 209 | "dependencies": { 210 | "@fastify/merge-json-schemas": "^0.2.0", 211 | "ajv": "^8.12.0", 212 | "ajv-formats": "^3.0.1", 213 | "fast-uri": "^3.0.0", 214 | "json-schema-ref-resolver": "^2.0.0", 215 | "rfdc": "^1.2.0" 216 | } 217 | }, 218 | "node_modules/fast-querystring": { 219 | "version": "1.1.2", 220 | "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", 221 | "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", 222 | "license": "MIT", 223 | "dependencies": { 224 | "fast-decode-uri-component": "^1.0.1" 225 | } 226 | }, 227 | "node_modules/fast-redact": { 228 | "version": "3.5.0", 229 | "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", 230 | "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", 231 | "license": "MIT", 232 | "engines": { 233 | "node": ">=6" 234 | } 235 | }, 236 | "node_modules/fast-uri": { 237 | "version": "3.0.6", 238 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", 239 | "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", 240 | "funding": [ 241 | { 242 | "type": "github", 243 | "url": "https://github.com/sponsors/fastify" 244 | }, 245 | { 246 | "type": "opencollective", 247 | "url": "https://opencollective.com/fastify" 248 | } 249 | ], 250 | "license": "BSD-3-Clause" 251 | }, 252 | "node_modules/fastify": { 253 | "version": "5.2.1", 254 | "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.2.1.tgz", 255 | "integrity": "sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA==", 256 | "funding": [ 257 | { 258 | "type": "github", 259 | "url": "https://github.com/sponsors/fastify" 260 | }, 261 | { 262 | "type": "opencollective", 263 | "url": "https://opencollective.com/fastify" 264 | } 265 | ], 266 | "license": "MIT", 267 | "dependencies": { 268 | "@fastify/ajv-compiler": "^4.0.0", 269 | "@fastify/error": "^4.0.0", 270 | "@fastify/fast-json-stringify-compiler": "^5.0.0", 271 | "@fastify/proxy-addr": "^5.0.0", 272 | "abstract-logging": "^2.0.1", 273 | "avvio": "^9.0.0", 274 | "fast-json-stringify": "^6.0.0", 275 | "find-my-way": "^9.0.0", 276 | "light-my-request": "^6.0.0", 277 | "pino": "^9.0.0", 278 | "process-warning": "^4.0.0", 279 | "rfdc": "^1.3.1", 280 | "secure-json-parse": "^3.0.1", 281 | "semver": "^7.6.0", 282 | "toad-cache": "^3.7.0" 283 | } 284 | }, 285 | "node_modules/fastq": { 286 | "version": "1.19.1", 287 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", 288 | "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", 289 | "license": "ISC", 290 | "dependencies": { 291 | "reusify": "^1.0.4" 292 | } 293 | }, 294 | "node_modules/find-my-way": { 295 | "version": "9.2.0", 296 | "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.2.0.tgz", 297 | "integrity": "sha512-d3uCir8Hmg7W1Ywp8nKf2lJJYU9Nwinvo+1D39Dn09nz65UKXIxUh7j7K8zeWhxqe1WrkS7FJyON/Q/3lPoc6w==", 298 | "license": "MIT", 299 | "dependencies": { 300 | "fast-deep-equal": "^3.1.3", 301 | "fast-querystring": "^1.0.0", 302 | "safe-regex2": "^4.0.0" 303 | }, 304 | "engines": { 305 | "node": ">=14" 306 | } 307 | }, 308 | "node_modules/ipaddr.js": { 309 | "version": "2.2.0", 310 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", 311 | "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", 312 | "license": "MIT", 313 | "engines": { 314 | "node": ">= 10" 315 | } 316 | }, 317 | "node_modules/json-schema-ref-resolver": { 318 | "version": "2.0.1", 319 | "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz", 320 | "integrity": "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==", 321 | "funding": [ 322 | { 323 | "type": "github", 324 | "url": "https://github.com/sponsors/fastify" 325 | }, 326 | { 327 | "type": "opencollective", 328 | "url": "https://opencollective.com/fastify" 329 | } 330 | ], 331 | "license": "MIT", 332 | "dependencies": { 333 | "dequal": "^2.0.3" 334 | } 335 | }, 336 | "node_modules/json-schema-traverse": { 337 | "version": "1.0.0", 338 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 339 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", 340 | "license": "MIT" 341 | }, 342 | "node_modules/light-my-request": { 343 | "version": "6.6.0", 344 | "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", 345 | "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", 346 | "funding": [ 347 | { 348 | "type": "github", 349 | "url": "https://github.com/sponsors/fastify" 350 | }, 351 | { 352 | "type": "opencollective", 353 | "url": "https://opencollective.com/fastify" 354 | } 355 | ], 356 | "license": "BSD-3-Clause", 357 | "dependencies": { 358 | "cookie": "^1.0.1", 359 | "process-warning": "^4.0.0", 360 | "set-cookie-parser": "^2.6.0" 361 | } 362 | }, 363 | "node_modules/on-exit-leak-free": { 364 | "version": "2.1.2", 365 | "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", 366 | "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", 367 | "license": "MIT", 368 | "engines": { 369 | "node": ">=14.0.0" 370 | } 371 | }, 372 | "node_modules/pino": { 373 | "version": "9.6.0", 374 | "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz", 375 | "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", 376 | "license": "MIT", 377 | "dependencies": { 378 | "atomic-sleep": "^1.0.0", 379 | "fast-redact": "^3.1.1", 380 | "on-exit-leak-free": "^2.1.0", 381 | "pino-abstract-transport": "^2.0.0", 382 | "pino-std-serializers": "^7.0.0", 383 | "process-warning": "^4.0.0", 384 | "quick-format-unescaped": "^4.0.3", 385 | "real-require": "^0.2.0", 386 | "safe-stable-stringify": "^2.3.1", 387 | "sonic-boom": "^4.0.1", 388 | "thread-stream": "^3.0.0" 389 | }, 390 | "bin": { 391 | "pino": "bin.js" 392 | } 393 | }, 394 | "node_modules/pino-abstract-transport": { 395 | "version": "2.0.0", 396 | "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", 397 | "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", 398 | "license": "MIT", 399 | "dependencies": { 400 | "split2": "^4.0.0" 401 | } 402 | }, 403 | "node_modules/pino-std-serializers": { 404 | "version": "7.0.0", 405 | "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", 406 | "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", 407 | "license": "MIT" 408 | }, 409 | "node_modules/process-warning": { 410 | "version": "4.0.1", 411 | "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", 412 | "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", 413 | "funding": [ 414 | { 415 | "type": "github", 416 | "url": "https://github.com/sponsors/fastify" 417 | }, 418 | { 419 | "type": "opencollective", 420 | "url": "https://opencollective.com/fastify" 421 | } 422 | ], 423 | "license": "MIT" 424 | }, 425 | "node_modules/quick-format-unescaped": { 426 | "version": "4.0.4", 427 | "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 428 | "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", 429 | "license": "MIT" 430 | }, 431 | "node_modules/real-require": { 432 | "version": "0.2.0", 433 | "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", 434 | "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", 435 | "license": "MIT", 436 | "engines": { 437 | "node": ">= 12.13.0" 438 | } 439 | }, 440 | "node_modules/require-from-string": { 441 | "version": "2.0.2", 442 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 443 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 444 | "license": "MIT", 445 | "engines": { 446 | "node": ">=0.10.0" 447 | } 448 | }, 449 | "node_modules/ret": { 450 | "version": "0.5.0", 451 | "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", 452 | "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", 453 | "license": "MIT", 454 | "engines": { 455 | "node": ">=10" 456 | } 457 | }, 458 | "node_modules/reusify": { 459 | "version": "1.1.0", 460 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", 461 | "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", 462 | "license": "MIT", 463 | "engines": { 464 | "iojs": ">=1.0.0", 465 | "node": ">=0.10.0" 466 | } 467 | }, 468 | "node_modules/rfdc": { 469 | "version": "1.4.1", 470 | "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", 471 | "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", 472 | "license": "MIT" 473 | }, 474 | "node_modules/safe-regex2": { 475 | "version": "4.0.1", 476 | "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.1.tgz", 477 | "integrity": "sha512-goqsB+bSlOmVX+CiFX2PFc1OV88j5jvBqIM+DgqrucHnUguAUNtiNOs+aTadq2NqsLQ+TQ3UEVG3gtSFcdlkCg==", 478 | "funding": [ 479 | { 480 | "type": "github", 481 | "url": "https://github.com/sponsors/fastify" 482 | }, 483 | { 484 | "type": "opencollective", 485 | "url": "https://opencollective.com/fastify" 486 | } 487 | ], 488 | "license": "MIT", 489 | "dependencies": { 490 | "ret": "~0.5.0" 491 | } 492 | }, 493 | "node_modules/safe-stable-stringify": { 494 | "version": "2.5.0", 495 | "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", 496 | "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", 497 | "license": "MIT", 498 | "engines": { 499 | "node": ">=10" 500 | } 501 | }, 502 | "node_modules/secure-json-parse": { 503 | "version": "3.0.2", 504 | "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-3.0.2.tgz", 505 | "integrity": "sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==", 506 | "funding": [ 507 | { 508 | "type": "github", 509 | "url": "https://github.com/sponsors/fastify" 510 | }, 511 | { 512 | "type": "opencollective", 513 | "url": "https://opencollective.com/fastify" 514 | } 515 | ], 516 | "license": "BSD-3-Clause" 517 | }, 518 | "node_modules/semver": { 519 | "version": "7.7.1", 520 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 521 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 522 | "license": "ISC", 523 | "bin": { 524 | "semver": "bin/semver.js" 525 | }, 526 | "engines": { 527 | "node": ">=10" 528 | } 529 | }, 530 | "node_modules/set-cookie-parser": { 531 | "version": "2.7.1", 532 | "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", 533 | "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", 534 | "license": "MIT" 535 | }, 536 | "node_modules/sonic-boom": { 537 | "version": "4.2.0", 538 | "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", 539 | "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", 540 | "license": "MIT", 541 | "dependencies": { 542 | "atomic-sleep": "^1.0.0" 543 | } 544 | }, 545 | "node_modules/split2": { 546 | "version": "4.2.0", 547 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", 548 | "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", 549 | "license": "ISC", 550 | "engines": { 551 | "node": ">= 10.x" 552 | } 553 | }, 554 | "node_modules/thread-stream": { 555 | "version": "3.1.0", 556 | "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", 557 | "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", 558 | "license": "MIT", 559 | "dependencies": { 560 | "real-require": "^0.2.0" 561 | } 562 | }, 563 | "node_modules/toad-cache": { 564 | "version": "3.7.0", 565 | "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", 566 | "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", 567 | "license": "MIT", 568 | "engines": { 569 | "node": ">=12" 570 | } 571 | } 572 | } 573 | } 574 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anthropic-proxy", 3 | "description": "A proxy server that transforms Anthropic API requests to OpenAI format and sends it to openrouter.ai.", 4 | "version": "1.3.0", 5 | "type": "module", 6 | "bin": { 7 | "anthropic-proxy": "index.js" 8 | }, 9 | "scripts": { 10 | "start": "node index.js" 11 | }, 12 | "keywords": [ 13 | "anthropic", 14 | "openai", 15 | "api", 16 | "proxy", 17 | "claude", 18 | "streaming", 19 | "sse" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/maxnowack/anthropic-proxy" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/maxnowack/anthropic-proxy/issues" 27 | }, 28 | "author": "Max Nowack ", 29 | "license": "MIT", 30 | "dependencies": { 31 | "fastify": "^5.2.1" 32 | } 33 | } 34 | --------------------------------------------------------------------------------