Download src/server/proxy/handler.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 8.02 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/server/proxy/handler.ts
- Command line
-
hf download hf://chenbhao/codev/src/server/proxy/handler.ts
-
curl -L -o handler.ts https://huggingface.co/chenbhao/codev/resolve/main/src/server/proxy/handler.ts
8.02 kB
| /** | |
| * Proxy Handler — protocol-translating reverse proxy for OpenAI-compatible APIs. | |
| * | |
| * Receives Anthropic Messages API requests from the CLI, transforms them to | |
| * OpenAI Chat Completions or Responses API format, forwards to the upstream | |
| * provider, and transforms the response back to Anthropic format. | |
| * | |
| * Derived from cc-switch (https://github.com/farion1231/cc-switch) | |
| * Original work by Jason Young, MIT License | |
| */ | |
| import { signClaudeCodeCCHInTransformedString } from '../../utils/claudeCodeCch.js' | |
| import { ProviderService } from '../services/providerService.js' | |
| import { ensureClaudeCodeAttribution } from './claudeCodeAttribution.js' | |
| import { anthropicToOpenaiChat } from './transform/anthropicToOpenaiChat.js' | |
| import { anthropicToOpenaiResponses } from './transform/anthropicToOpenaiResponses.js' | |
| import { openaiChatToAnthropic } from './transform/openaiChatToAnthropic.js' | |
| import { openaiResponsesToAnthropic } from './transform/openaiResponsesToAnthropic.js' | |
| import { openaiChatStreamToAnthropic } from './streaming/openaiChatStreamToAnthropic.js' | |
| import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStreamToAnthropic.js' | |
| import type { AnthropicRequest } from './transform/types.js' | |
| import { getProxyFetchOptions } from '../../utils/proxy.js' | |
| import { getManualNetworkProxyUrl, loadNetworkSettings } from '../services/networkSettings.js' | |
| const providerService = new ProviderService() | |
| export async function handleProxyRequest(req: Request, url: URL): Promise<Response> { | |
| const providerMatch = url.pathname.match(/^\/proxy\/providers\/([^/]+)\/v1\/messages$/) | |
| const providerId = providerMatch ? decodeURIComponent(providerMatch[1]!) : undefined | |
| const isActiveProxyPath = url.pathname === '/proxy/v1/messages' | |
| // Only handle POST /proxy/v1/messages or POST /proxy/providers/:providerId/v1/messages | |
| if (req.method !== 'POST' || (!isActiveProxyPath && !providerMatch)) { | |
| return Response.json( | |
| { | |
| error: 'Not Found', | |
| message: 'Proxy only handles POST /proxy/v1/messages and POST /proxy/providers/:providerId/v1/messages', | |
| }, | |
| { status: 404 }, | |
| ) | |
| } | |
| // Read active/default provider config or an explicitly-scoped provider config. | |
| const config = await providerService.getProviderForProxy(providerId) | |
| if (!config) { | |
| return Response.json( | |
| { | |
| type: 'error', | |
| error: { | |
| type: 'invalid_request_error', | |
| message: providerId | |
| ? `Provider "${providerId}" is not configured for proxy` | |
| : 'No active provider configured for proxy', | |
| }, | |
| }, | |
| { status: 400 }, | |
| ) | |
| } | |
| if (config.apiFormat === 'anthropic') { | |
| return Response.json( | |
| { | |
| type: 'error', | |
| error: { | |
| type: 'invalid_request_error', | |
| message: providerId | |
| ? `Provider "${providerId}" uses anthropic format — proxy not needed` | |
| : 'Active provider uses anthropic format — proxy not needed', | |
| }, | |
| }, | |
| { status: 400 }, | |
| ) | |
| } | |
| // Parse request body | |
| let body: AnthropicRequest | |
| try { | |
| body = (await req.json()) as AnthropicRequest | |
| } catch { | |
| return Response.json( | |
| { type: 'error', error: { type: 'invalid_request_error', message: 'Invalid JSON in request body' } }, | |
| { status: 400 }, | |
| ) | |
| } | |
| body = ensureClaudeCodeAttribution(body) | |
| const isStream = body.stream === true | |
| const baseUrl = config.baseUrl.replace(/\/+$/, '') | |
| const networkSettings = await loadNetworkSettings() | |
| const proxyUrl = getManualNetworkProxyUrl(networkSettings) | |
| try { | |
| if (config.apiFormat === 'openai_chat') { | |
| return await handleOpenaiChat(body, baseUrl, config.apiKey, isStream, networkSettings.aiRequestTimeoutMs, proxyUrl) | |
| } else { | |
| return await handleOpenaiResponses(body, baseUrl, config.apiKey, isStream, networkSettings.aiRequestTimeoutMs, proxyUrl) | |
| } | |
| } catch (err) { | |
| console.error('[Proxy] Upstream request failed:', err) | |
| return Response.json( | |
| { | |
| type: 'error', | |
| error: { | |
| type: 'api_error', | |
| message: err instanceof Error ? err.message : String(err), | |
| }, | |
| }, | |
| { status: 502 }, | |
| ) | |
| } | |
| } | |
| async function handleOpenaiChat( | |
| body: AnthropicRequest, | |
| baseUrl: string, | |
| apiKey: string, | |
| isStream: boolean, | |
| aiRequestTimeoutMs: number, | |
| proxyUrl: string | undefined, | |
| ): Promise<Response> { | |
| const deepSeekCompatible = shouldUseDeepSeekReasoningCompat(baseUrl) | |
| const transformed = anthropicToOpenaiChat(body, { | |
| roundTripReasoningContent: deepSeekCompatible, | |
| passThinkingToggle: deepSeekCompatible, | |
| }) | |
| const url = `${baseUrl}/v1/chat/completions` | |
| const proxyOptions = getProxyFetchOptions({ proxyUrl }) | |
| const upstream = await fetch(url, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: `Bearer ${apiKey}`, | |
| }, | |
| body: signClaudeCodeCCHInTransformedString(JSON.stringify(transformed)), | |
| signal: AbortSignal.timeout(aiRequestTimeoutMs), | |
| ...proxyOptions, | |
| }) | |
| if (!upstream.ok) { | |
| const errText = await upstream.text().catch(() => '') | |
| return Response.json( | |
| { | |
| type: 'error', | |
| error: { | |
| type: 'api_error', | |
| message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`, | |
| }, | |
| }, | |
| { status: upstream.status }, | |
| ) | |
| } | |
| if (isStream) { | |
| if (!upstream.body) { | |
| return Response.json( | |
| { type: 'error', error: { type: 'api_error', message: 'Upstream returned no body for stream' } }, | |
| { status: 502 }, | |
| ) | |
| } | |
| const anthropicStream = openaiChatStreamToAnthropic(upstream.body, body.model) | |
| return new Response(anthropicStream, { | |
| status: 200, | |
| headers: { | |
| 'Content-Type': 'text/event-stream', | |
| 'Cache-Control': 'no-cache', | |
| Connection: 'keep-alive', | |
| }, | |
| }) | |
| } | |
| // Non-streaming | |
| const responseBody = await upstream.json() | |
| const anthropicResponse = openaiChatToAnthropic(responseBody, body.model) | |
| return Response.json(anthropicResponse) | |
| } | |
| function shouldUseDeepSeekReasoningCompat(baseUrl: string): boolean { | |
| return ( | |
| /(^|[./-])deepseek([./-]|$)/i.test(baseUrl) || | |
| /(^|[./-])opencode\.ai([:/]|$)/i.test(baseUrl) | |
| ) | |
| } | |
| async function handleOpenaiResponses( | |
| body: AnthropicRequest, | |
| baseUrl: string, | |
| apiKey: string, | |
| isStream: boolean, | |
| aiRequestTimeoutMs: number, | |
| proxyUrl: string | undefined, | |
| ): Promise<Response> { | |
| const transformed = anthropicToOpenaiResponses(body) | |
| const url = `${baseUrl}/v1/responses` | |
| const proxyOptions = getProxyFetchOptions({ proxyUrl }) | |
| const upstream = await fetch(url, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: `Bearer ${apiKey}`, | |
| }, | |
| body: signClaudeCodeCCHInTransformedString(JSON.stringify(transformed)), | |
| signal: AbortSignal.timeout(aiRequestTimeoutMs), | |
| ...proxyOptions, | |
| }) | |
| if (!upstream.ok) { | |
| const errText = await upstream.text().catch(() => '') | |
| return Response.json( | |
| { | |
| type: 'error', | |
| error: { | |
| type: 'api_error', | |
| message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`, | |
| }, | |
| }, | |
| { status: upstream.status }, | |
| ) | |
| } | |
| if (isStream) { | |
| if (!upstream.body) { | |
| return Response.json( | |
| { type: 'error', error: { type: 'api_error', message: 'Upstream returned no body for stream' } }, | |
| { status: 502 }, | |
| ) | |
| } | |
| const anthropicStream = openaiResponsesStreamToAnthropic(upstream.body, body.model) | |
| return new Response(anthropicStream, { | |
| status: 200, | |
| headers: { | |
| 'Content-Type': 'text/event-stream', | |
| 'Cache-Control': 'no-cache', | |
| Connection: 'keep-alive', | |
| }, | |
| }) | |
| } | |
| // Non-streaming | |
| const responseBody = await upstream.json() | |
| const anthropicResponse = openaiResponsesToAnthropic(responseBody, body.model) | |
| return Response.json(anthropicResponse) | |
| } | |