Created
November 21, 2025 10:36
-
-
Save rodrigorodriguez/5a104410708ba20485ddf37f0dd5198e to your computer and use it in GitHub Desktop.
Azure Claude
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const http = require("http"); | |
| const https = require("https"); | |
| const endpoint = "https://xxxxxxxx-eastus2.openai.azure.com/anthropic"; | |
| const deploymentName = "claude-sonnet-4-5"; | |
| const apiKey = process.env.AZURE_API_KEY; | |
| // Retry configuration | |
| const RETRY_CONFIG = { | |
| maxRetries: 5, | |
| initialDelay: 1000, // 1 second | |
| maxDelay: 30000, // 30 seconds | |
| backoffMultiplier: 2, | |
| timeoutMs: 60000, // 60 seconds per attempt | |
| }; | |
| // HTTP agent with connection pooling and keep-alive | |
| const httpsAgent = new https.Agent({ | |
| keepAlive: true, | |
| keepAliveMsecs: 30000, | |
| maxSockets: 50, | |
| maxFreeSockets: 10, | |
| timeout: RETRY_CONFIG.timeoutMs, | |
| }); | |
| /** | |
| * Sleep utility for retry delays | |
| */ | |
| function sleep(ms) { | |
| return new Promise((resolve) => setTimeout(resolve, ms)); | |
| } | |
| /** | |
| * Calculate exponential backoff delay with jitter | |
| */ | |
| function getRetryDelay(attemptNumber) { | |
| const delay = Math.min( | |
| RETRY_CONFIG.initialDelay * | |
| Math.pow(RETRY_CONFIG.backoffMultiplier, attemptNumber), | |
| RETRY_CONFIG.maxDelay, | |
| ); | |
| // Add jitter (Β±25% randomness) | |
| const jitter = delay * 0.25 * (Math.random() * 2 - 1); | |
| return Math.floor(delay + jitter); | |
| } | |
| /** | |
| * Check if error is retryable | |
| */ | |
| function isRetryableError(error, statusCode) { | |
| // Network errors that should be retried | |
| const retryableErrors = [ | |
| "ECONNRESET", | |
| "ETIMEDOUT", | |
| "ENOTFOUND", | |
| "ECONNREFUSED", | |
| "EPIPE", | |
| "EHOSTUNREACH", | |
| "EAI_AGAIN", | |
| ]; | |
| if (error.code && retryableErrors.includes(error.code)) { | |
| return true; | |
| } | |
| // HTTP status codes that should be retried | |
| const retryableStatuses = [408, 429, 500, 502, 503, 504]; | |
| if (statusCode && retryableStatuses.includes(statusCode)) { | |
| return true; | |
| } | |
| // Timeout errors | |
| if (error.message && error.message.toLowerCase().includes("timeout")) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| const server = http.createServer(async (req, res) => { | |
| // Set CORS headers | |
| res.setHeader("Access-Control-Allow-Origin", "*"); | |
| res.setHeader( | |
| "Access-Control-Allow-Methods", | |
| "GET, POST, OPTIONS, PUT, DELETE", | |
| ); | |
| res.setHeader( | |
| "Access-Control-Allow-Headers", | |
| "Content-Type, Authorization, x-requested-with", | |
| ); | |
| // Handle preflight requests | |
| if (req.method === "OPTIONS") { | |
| res.writeHead(200); | |
| res.end(); | |
| return; | |
| } | |
| console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); | |
| // Only handle POST requests to /v1/messages | |
| if (req.method === "POST" && req.url === "/v1/messages") { | |
| let body = ""; | |
| let requestHandled = false; | |
| try { | |
| // Collect the request body | |
| req.on("data", (chunk) => { | |
| body += chunk.toString(); | |
| }); | |
| req.on("end", async () => { | |
| if (requestHandled) return; | |
| requestHandled = true; | |
| try { | |
| const claudeRequest = JSON.parse(body); | |
| console.log( | |
| `${new Date().toISOString()} - Received request with ${claudeRequest.messages?.length || 0} messages`, | |
| ); | |
| // Check if streaming is requested | |
| const stream = claudeRequest.stream === true; | |
| if (stream) { | |
| // Handle streaming response | |
| await handleStreamingResponse(claudeRequest, res); | |
| } else { | |
| // Handle non-streaming response | |
| const claudeResponse = | |
| await forwardToClaudeWithRetry(claudeRequest); | |
| if (!res.headersSent) { | |
| res.writeHead(200, { | |
| "Content-Type": "application/json", | |
| "Access-Control-Allow-Origin": "*", | |
| }); | |
| res.end(JSON.stringify(claudeResponse)); | |
| } | |
| } | |
| } catch (error) { | |
| console.error( | |
| `${new Date().toISOString()} - Error processing request:`, | |
| error.message, | |
| ); | |
| if (!res.headersSent) { | |
| res.writeHead(error.statusCode || 400, { | |
| "Content-Type": "application/json", | |
| "Access-Control-Allow-Origin": "*", | |
| }); | |
| res.end( | |
| JSON.stringify({ | |
| error: { | |
| message: error.message, | |
| type: error.type || "invalid_request_error", | |
| }, | |
| }), | |
| ); | |
| } | |
| } | |
| }); | |
| req.on("error", (error) => { | |
| console.error( | |
| `${new Date().toISOString()} - Request error:`, | |
| error.message, | |
| ); | |
| if (!requestHandled && !res.headersSent) { | |
| requestHandled = true; | |
| res.writeHead(400, { | |
| "Content-Type": "application/json", | |
| "Access-Control-Allow-Origin": "*", | |
| }); | |
| res.end( | |
| JSON.stringify({ | |
| error: { | |
| message: "Bad request", | |
| type: "invalid_request_error", | |
| }, | |
| }), | |
| ); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error( | |
| `${new Date().toISOString()} - Server error:`, | |
| error.message, | |
| ); | |
| if (!res.headersSent) { | |
| res.writeHead(500, { | |
| "Content-Type": "application/json", | |
| "Access-Control-Allow-Origin": "*", | |
| }); | |
| res.end( | |
| JSON.stringify({ | |
| error: { | |
| message: "Internal server error", | |
| type: "server_error", | |
| }, | |
| }), | |
| ); | |
| } | |
| } | |
| } else { | |
| res.writeHead(404, { | |
| "Content-Type": "application/json", | |
| "Access-Control-Allow-Origin": "*", | |
| }); | |
| res.end( | |
| JSON.stringify({ | |
| error: { | |
| message: "Endpoint not found. Use POST /v1/messages", | |
| type: "not_found", | |
| }, | |
| }), | |
| ); | |
| } | |
| }); | |
| /** | |
| * Wrapper function that adds retry logic to forwardToClaude | |
| */ | |
| async function forwardToClaudeWithRetry(claudeRequest) { | |
| let lastError; | |
| for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) { | |
| try { | |
| console.log( | |
| `${new Date().toISOString()} - Attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries + 1}`, | |
| ); | |
| const response = await forwardToClaude(claudeRequest); | |
| if (attempt > 0) { | |
| console.log( | |
| `${new Date().toISOString()} - β Success after ${attempt + 1} attempts`, | |
| ); | |
| } | |
| return response; | |
| } catch (error) { | |
| lastError = error; | |
| const statusCode = error.statusCode; | |
| console.error( | |
| `${new Date().toISOString()} - Attempt ${attempt + 1} failed:`, | |
| error.message, | |
| statusCode ? `(Status: ${statusCode})` : "", | |
| ); | |
| // Check if we should retry | |
| if ( | |
| attempt < RETRY_CONFIG.maxRetries && | |
| isRetryableError(error, statusCode) | |
| ) { | |
| const delay = getRetryDelay(attempt); | |
| console.log(`${new Date().toISOString()} - Retrying in ${delay}ms...`); | |
| await sleep(delay); | |
| } else if (attempt >= RETRY_CONFIG.maxRetries) { | |
| console.error( | |
| `${new Date().toISOString()} - β Max retries (${RETRY_CONFIG.maxRetries}) exceeded`, | |
| ); | |
| break; | |
| } else { | |
| console.error( | |
| `${new Date().toISOString()} - β Non-retryable error, not retrying`, | |
| ); | |
| break; | |
| } | |
| } | |
| } | |
| // If we get here, all retries failed | |
| throw lastError; | |
| } | |
| /** | |
| * Forward request to Claude API (single attempt) | |
| */ | |
| async function forwardToClaude(claudeRequest) { | |
| return new Promise((resolve, reject) => { | |
| let resolved = false; | |
| let timeoutHandle; | |
| const cleanup = () => { | |
| if (timeoutHandle) { | |
| clearTimeout(timeoutHandle); | |
| timeoutHandle = null; | |
| } | |
| }; | |
| const safeResolve = (value) => { | |
| if (!resolved) { | |
| resolved = true; | |
| cleanup(); | |
| resolve(value); | |
| } | |
| }; | |
| const safeReject = (error) => { | |
| if (!resolved) { | |
| resolved = true; | |
| cleanup(); | |
| reject(error); | |
| } | |
| }; | |
| try { | |
| // Prepare the request data | |
| const requestData = { | |
| ...claudeRequest, | |
| model: deploymentName, | |
| }; | |
| // Remove stream flag for non-streaming requests | |
| if (!requestData.stream) { | |
| delete requestData.stream; | |
| } | |
| const postData = JSON.stringify(requestData); | |
| // Parse the endpoint URL | |
| const url = new URL(endpoint + "/v1/messages"); | |
| const options = { | |
| hostname: url.hostname, | |
| port: url.port || 443, | |
| path: url.pathname, | |
| method: "POST", | |
| agent: httpsAgent, | |
| headers: { | |
| "Content-Type": "application/json", | |
| "x-api-key": apiKey, | |
| "anthropic-version": "2023-06-01", | |
| "Content-Length": Buffer.byteLength(postData), | |
| Connection: "keep-alive", | |
| }, | |
| }; | |
| const req = https.request(options, (res) => { | |
| let responseBody = ""; | |
| res.on("data", (chunk) => { | |
| responseBody += chunk; | |
| }); | |
| res.on("end", () => { | |
| if (res.statusCode >= 200 && res.statusCode < 300) { | |
| try { | |
| const parsedResponse = JSON.parse(responseBody); | |
| safeResolve(parsedResponse); | |
| } catch (parseError) { | |
| const error = new Error( | |
| `Failed to parse Claude response: ${parseError.message}`, | |
| ); | |
| error.statusCode = 502; | |
| safeReject(error); | |
| } | |
| } else { | |
| let errorMessage = `Claude API returned status ${res.statusCode}`; | |
| try { | |
| const errorResponse = JSON.parse(responseBody); | |
| errorMessage = errorResponse.error?.message || errorMessage; | |
| } catch (e) { | |
| errorMessage = responseBody || errorMessage; | |
| } | |
| const error = new Error(errorMessage); | |
| error.statusCode = res.statusCode; | |
| safeReject(error); | |
| } | |
| }); | |
| res.on("error", (error) => { | |
| safeReject(error); | |
| }); | |
| }); | |
| req.on("error", (error) => { | |
| safeReject(error); | |
| }); | |
| req.on("timeout", () => { | |
| req.destroy(); | |
| const error = new Error("Request timeout"); | |
| error.code = "ETIMEDOUT"; | |
| safeReject(error); | |
| }); | |
| // Set request timeout | |
| req.setTimeout(RETRY_CONFIG.timeoutMs); | |
| // Additional safety timeout | |
| timeoutHandle = setTimeout(() => { | |
| if (!resolved) { | |
| req.destroy(); | |
| const error = new Error("Request timeout (safety)"); | |
| error.code = "ETIMEDOUT"; | |
| safeReject(error); | |
| } | |
| }, RETRY_CONFIG.timeoutMs + 5000); | |
| // Write the request body | |
| req.write(postData); | |
| req.end(); | |
| } catch (error) { | |
| safeReject(error); | |
| } | |
| }); | |
| } | |
| /** | |
| * Handle streaming response with retry logic | |
| */ | |
| async function handleStreamingResponse(claudeRequest, clientRes) { | |
| let lastError; | |
| for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) { | |
| try { | |
| console.log( | |
| `${new Date().toISOString()} - Streaming attempt ${attempt + 1}/${RETRY_CONFIG.maxRetries + 1}`, | |
| ); | |
| await handleStreamingSingleAttempt(claudeRequest, clientRes); | |
| if (attempt > 0) { | |
| console.log( | |
| `${new Date().toISOString()} - β Streaming success after ${attempt + 1} attempts`, | |
| ); | |
| } | |
| return; | |
| } catch (error) { | |
| lastError = error; | |
| const statusCode = error.statusCode; | |
| console.error( | |
| `${new Date().toISOString()} - Streaming attempt ${attempt + 1} failed:`, | |
| error.message, | |
| statusCode ? `(Status: ${statusCode})` : "", | |
| ); | |
| // If headers were already sent, we can't retry | |
| if (clientRes.headersSent) { | |
| console.error( | |
| `${new Date().toISOString()} - Cannot retry: headers already sent`, | |
| ); | |
| throw error; | |
| } | |
| // Check if we should retry | |
| if ( | |
| attempt < RETRY_CONFIG.maxRetries && | |
| isRetryableError(error, statusCode) | |
| ) { | |
| const delay = getRetryDelay(attempt); | |
| console.log( | |
| `${new Date().toISOString()} - Retrying streaming in ${delay}ms...`, | |
| ); | |
| await sleep(delay); | |
| } else if (attempt >= RETRY_CONFIG.maxRetries) { | |
| console.error( | |
| `${new Date().toISOString()} - β Max streaming retries (${RETRY_CONFIG.maxRetries}) exceeded`, | |
| ); | |
| break; | |
| } else { | |
| console.error( | |
| `${new Date().toISOString()} - β Non-retryable streaming error`, | |
| ); | |
| break; | |
| } | |
| } | |
| } | |
| // If we get here, all retries failed | |
| if (!clientRes.headersSent) { | |
| clientRes.writeHead(lastError.statusCode || 500, { | |
| "Content-Type": "application/json", | |
| "Access-Control-Allow-Origin": "*", | |
| }); | |
| clientRes.end( | |
| JSON.stringify({ | |
| error: { | |
| message: lastError.message, | |
| type: "streaming_error", | |
| }, | |
| }), | |
| ); | |
| } | |
| throw lastError; | |
| } | |
| /** | |
| * Single attempt at streaming response | |
| */ | |
| async function handleStreamingSingleAttempt(claudeRequest, clientRes) { | |
| return new Promise((resolve, reject) => { | |
| let headersSent = false; | |
| let resolved = false; | |
| let timeoutHandle; | |
| const cleanup = () => { | |
| if (timeoutHandle) { | |
| clearTimeout(timeoutHandle); | |
| timeoutHandle = null; | |
| } | |
| }; | |
| const safeResolve = () => { | |
| if (!resolved) { | |
| resolved = true; | |
| cleanup(); | |
| resolve(); | |
| } | |
| }; | |
| const safeReject = (error) => { | |
| if (!resolved) { | |
| resolved = true; | |
| cleanup(); | |
| reject(error); | |
| } | |
| }; | |
| try { | |
| // Prepare the request data | |
| const requestData = { | |
| ...claudeRequest, | |
| model: deploymentName, | |
| stream: true, | |
| }; | |
| const postData = JSON.stringify(requestData); | |
| const url = new URL(endpoint + "/v1/messages"); | |
| const options = { | |
| hostname: url.hostname, | |
| port: url.port || 443, | |
| path: url.pathname, | |
| method: "POST", | |
| agent: httpsAgent, | |
| headers: { | |
| "Content-Type": "application/json", | |
| "x-api-key": apiKey, | |
| "anthropic-version": "2023-06-01", | |
| "Content-Length": Buffer.byteLength(postData), | |
| Connection: "keep-alive", | |
| }, | |
| }; | |
| const req = https.request(options, (claudeRes) => { | |
| if (claudeRes.statusCode >= 200 && claudeRes.statusCode < 300) { | |
| // Set up streaming response headers | |
| if (!headersSent && !clientRes.headersSent) { | |
| headersSent = true; | |
| clientRes.writeHead(200, { | |
| "Content-Type": "text/event-stream", | |
| "Transfer-Encoding": "chunked", | |
| "Access-Control-Allow-Origin": "*", | |
| "Cache-Control": "no-cache", | |
| Connection: "keep-alive", | |
| }); | |
| } | |
| // Pipe the streaming response | |
| claudeRes.pipe(clientRes, { end: true }); | |
| claudeRes.on("end", () => { | |
| console.log(`${new Date().toISOString()} - Streaming completed`); | |
| safeResolve(); | |
| }); | |
| claudeRes.on("error", (error) => { | |
| console.error( | |
| `${new Date().toISOString()} - Stream error:`, | |
| error.message, | |
| ); | |
| safeReject(error); | |
| }); | |
| } else { | |
| let errorBody = ""; | |
| claudeRes.on("data", (chunk) => { | |
| errorBody += chunk; | |
| }); | |
| claudeRes.on("end", () => { | |
| let errorMessage = `Claude API returned status ${claudeRes.statusCode}`; | |
| try { | |
| const errorResponse = JSON.parse(errorBody); | |
| errorMessage = errorResponse.error?.message || errorMessage; | |
| } catch (e) { | |
| errorMessage = errorBody || errorMessage; | |
| } | |
| const error = new Error(errorMessage); | |
| error.statusCode = claudeRes.statusCode; | |
| safeReject(error); | |
| }); | |
| } | |
| }); | |
| req.on("error", (error) => { | |
| safeReject(error); | |
| }); | |
| req.on("timeout", () => { | |
| req.destroy(); | |
| const error = new Error("Streaming request timeout"); | |
| error.code = "ETIMEDOUT"; | |
| safeReject(error); | |
| }); | |
| // Set a longer timeout for streaming | |
| req.setTimeout(120000); | |
| // Safety timeout | |
| timeoutHandle = setTimeout(() => { | |
| if (!resolved) { | |
| req.destroy(); | |
| const error = new Error("Streaming timeout (safety)"); | |
| error.code = "ETIMEDOUT"; | |
| safeReject(error); | |
| } | |
| }, 125000); | |
| // Write the request body | |
| req.write(postData); | |
| req.end(); | |
| } catch (error) { | |
| safeReject(error); | |
| } | |
| }); | |
| } | |
| /** | |
| * Test Claude connection with retry | |
| */ | |
| async function testClaudeConnection() { | |
| try { | |
| console.log(`${new Date().toISOString()} - Testing Claude connection...`); | |
| const testResponse = await forwardToClaudeWithRetry({ | |
| messages: [ | |
| { | |
| role: "user", | |
| content: "Hello, respond with just 'OK' if you can hear me.", | |
| }, | |
| ], | |
| max_tokens: 10, | |
| temperature: 0, | |
| }); | |
| console.log( | |
| `${new Date().toISOString()} - β Claude connection test successful:`, | |
| testResponse.content[0]?.text, | |
| ); | |
| return true; | |
| } catch (error) { | |
| console.error( | |
| `${new Date().toISOString()} - β Claude connection test failed:`, | |
| error.message, | |
| ); | |
| return false; | |
| } | |
| } | |
| const PORT = process.env.PORT || 3000; | |
| server.listen(PORT, async () => { | |
| console.log(`\n${"=".repeat(60)}`); | |
| console.log(`π Claude API Proxy Server`); | |
| console.log(`${"=".repeat(60)}`); | |
| console.log(`π Server: http://localhost:${PORT}`); | |
| console.log(`π‘ Upstream: ${endpoint}`); | |
| console.log(`π€ Model: ${deploymentName}`); | |
| console.log(`π Endpoint: POST http://localhost:${PORT}/v1/messages`); | |
| console.log(`\nβοΈ Configuration:`); | |
| console.log(` β’ Max Retries: ${RETRY_CONFIG.maxRetries}`); | |
| console.log(` β’ Initial Delay: ${RETRY_CONFIG.initialDelay}ms`); | |
| console.log(` β’ Max Delay: ${RETRY_CONFIG.maxDelay}ms`); | |
| console.log(` β’ Timeout: ${RETRY_CONFIG.timeoutMs}ms`); | |
| console.log(` β’ Streaming: ENABLED`); | |
| console.log(` β’ Connection Pooling: ENABLED`); | |
| console.log(`${"=".repeat(60)}\n`); | |
| // Test the connection on startup | |
| const testPassed = await testClaudeConnection(); | |
| console.log(`\n${testPassed ? "β " : "β οΈ"} Server ready to accept requests!`); | |
| console.log(`${"=".repeat(60)}\n`); | |
| }); | |
| // Graceful shutdown | |
| process.on("SIGINT", () => { | |
| console.log(`\n${new Date().toISOString()} - π Shutting down server...`); | |
| // Destroy the agent to close all connections | |
| httpsAgent.destroy(); | |
| server.close(() => { | |
| console.log(`${new Date().toISOString()} - β Server closed`); | |
| process.exit(0); | |
| }); | |
| // Force exit after 10 seconds | |
| setTimeout(() => { | |
| console.log(`${new Date().toISOString()} - β οΈ Forcing exit`); | |
| process.exit(1); | |
| }, 10000); | |
| }); | |
| process.on("unhandledRejection", (err) => { | |
| console.error( | |
| `${new Date().toISOString()} - Unhandled promise rejection:`, | |
| err.message, | |
| ); | |
| }); | |
| process.on("uncaughtException", (err) => { | |
| console.error( | |
| `${new Date().toISOString()} - Uncaught exception:`, | |
| err.message, | |
| ); | |
| // Don't exit immediately, let other handlers run | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment