Download src/services/api/customOpenAIClient.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 9.09 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/services/api/customOpenAIClient.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/services/api/customOpenAIClient.ts
-
curl -L -o customOpenAIClient.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/services/api/customOpenAIClient.ts
9.09 kB
| import { getGlobalConfig, type ConnectedProviderInfo } from '../../utils/config.js' | |
| import { | |
| convertAnthropicMessagesToOpenAI, | |
| convertAnthropicToolsToOpenAI, | |
| convertOpenAIStreamToAnthropic, | |
| type AnthropicMessage, | |
| } from './copilotClient.js' | |
| const PREFIX = 'custom-openai:' | |
| export function isCustomOpenAIModel(model: string | undefined): boolean { | |
| return !!model?.startsWith(PREFIX) | |
| } | |
| export function getCustomOpenAIModelId(model: string): string { | |
| const rest = model.slice(PREFIX.length).trim() | |
| if (rest) { | |
| return rest | |
| } | |
| const p = getGlobalConfig().connectedProviders?.['custom-openai'] | |
| return p?.defaultModel || 'gpt-4o-mini' | |
| } | |
| export function getCustomOpenAIProvider(): ConnectedProviderInfo | undefined { | |
| return getGlobalConfig().connectedProviders?.['custom-openai'] | |
| } | |
| export function isCustomOpenAIConnected(): boolean { | |
| const c = getGlobalConfig() | |
| return c.activeProvider === 'custom-openai' && !!c.connectedProviders?.['custom-openai']?.baseUrl | |
| } | |
| function normalizeBaseUrl(url: string): string { | |
| return url.replace(/\/$/, '') | |
| } | |
| /** Supports `https://host` or `https://host/v1` style bases. */ | |
| function chatCompletionsUrl(base: string): string { | |
| const b = normalizeBaseUrl(base) | |
| if (b.endsWith('/v1')) { | |
| return `${b}/chat/completions` | |
| } | |
| return `${b}/v1/chat/completions` | |
| } | |
| /** | |
| * OpenAI-compatible `/v1/chat/completions` bridge (same protocol as GitHub Copilot path). | |
| */ | |
| export function createCustomOpenAIFetchOverride( | |
| model: string, | |
| ): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> { | |
| const provider = getCustomOpenAIProvider() | |
| if (!provider?.baseUrl) { | |
| throw new Error('Custom OpenAI-compatible API is not configured') | |
| } | |
| const openaiModelId = getCustomOpenAIModelId(model) | |
| const endpoint = chatCompletionsUrl(provider.baseUrl) | |
| return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => { | |
| const url = input instanceof URL ? input.href : typeof input === 'string' ? input : input.url | |
| if (!url.includes('/messages') && !url.includes('/v1/')) { | |
| return fetch(input, init) | |
| } | |
| if (url.includes('/count_tokens') || url.includes('/models')) { | |
| return new Response(JSON.stringify({ input_tokens: 0 }), { | |
| status: 200, | |
| headers: { 'Content-Type': 'application/json' }, | |
| }) | |
| } | |
| let anthropicBody: Record<string, unknown> = {} | |
| if (init?.body) { | |
| try { | |
| anthropicBody = JSON.parse( | |
| typeof init.body === 'string' ? init.body : new TextDecoder().decode(init.body as ArrayBuffer), | |
| ) | |
| } catch { | |
| return fetch(input, init) | |
| } | |
| } | |
| const systemBlocks = anthropicBody.system as | |
| | Array<{ type: string; text: string }> | |
| | string | |
| | undefined | |
| let systemPrompt = '' | |
| if (typeof systemBlocks === 'string') { | |
| systemPrompt = systemBlocks | |
| } else if (Array.isArray(systemBlocks)) { | |
| systemPrompt = systemBlocks | |
| .filter(b => b.type === 'text') | |
| .map(b => b.text) | |
| .join('\n\n') | |
| } | |
| const anthropicMessages = (anthropicBody.messages || []) as AnthropicMessage[] | |
| const openaiMessages = convertAnthropicMessagesToOpenAI(anthropicMessages, systemPrompt) | |
| const anthropicTools = (anthropicBody.tools || []) as Array<{ | |
| name: string | |
| description?: string | |
| input_schema?: Record<string, unknown> | |
| }> | |
| const openaiTools = anthropicTools.length > 0 ? convertAnthropicToolsToOpenAI(anthropicTools) : undefined | |
| const isStreaming = anthropicBody.stream === true | |
| const requestBody: Record<string, unknown> = { | |
| model: openaiModelId, | |
| messages: openaiMessages, | |
| stream: isStreaming, | |
| } | |
| if (anthropicBody.max_tokens) { | |
| requestBody.max_tokens = anthropicBody.max_tokens | |
| } | |
| if (openaiTools && openaiTools.length > 0) { | |
| requestBody.tools = openaiTools | |
| requestBody.tool_choice = 'auto' | |
| } | |
| const headers: Record<string, string> = { | |
| 'Content-Type': 'application/json', | |
| 'User-Agent': 'claude-code/2.1.88', | |
| } | |
| if (provider.apiKey) { | |
| headers.Authorization = `Bearer ${provider.apiKey}` | |
| } | |
| const openaiResponse = await fetch(endpoint, { | |
| method: 'POST', | |
| headers, | |
| body: JSON.stringify(requestBody), | |
| signal: init?.signal, | |
| }) | |
| if (!openaiResponse.ok) { | |
| return openaiResponse | |
| } | |
| if (!isStreaming) { | |
| const data = (await openaiResponse.json()) as { | |
| id: string | |
| choices: Array<{ | |
| message: { | |
| role: string | |
| content: string | null | |
| tool_calls?: Array<{ | |
| id: string | |
| function: { name: string; arguments: string } | |
| }> | |
| } | |
| finish_reason: string | |
| }> | |
| usage?: { prompt_tokens: number; completion_tokens: number } | |
| } | |
| const choice = data.choices[0] | |
| const anthropicContent: Array<{ | |
| type: string | |
| text?: string | |
| id?: string | |
| name?: string | |
| input?: unknown | |
| }> = [] | |
| if (choice?.message?.content) { | |
| anthropicContent.push({ type: 'text', text: choice.message.content }) | |
| } | |
| if (choice?.message?.tool_calls) { | |
| for (const tc of choice.message.tool_calls) { | |
| anthropicContent.push({ | |
| type: 'tool_use', | |
| id: tc.id, | |
| name: tc.function.name, | |
| input: JSON.parse(tc.function.arguments || '{}'), | |
| }) | |
| } | |
| } | |
| const anthropicResponse = { | |
| id: data.id || `msg_custom_openai_${Date.now()}`, | |
| type: 'message', | |
| role: 'assistant', | |
| content: anthropicContent, | |
| model: openaiModelId, | |
| stop_reason: choice?.finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn', | |
| usage: { | |
| input_tokens: data.usage?.prompt_tokens || 0, | |
| output_tokens: data.usage?.completion_tokens || 0, | |
| }, | |
| } | |
| return new Response(JSON.stringify(anthropicResponse), { | |
| status: 200, | |
| headers: { 'Content-Type': 'application/json' }, | |
| }) | |
| } | |
| if (!openaiResponse.body) { | |
| return openaiResponse | |
| } | |
| const transformStream = convertOpenAIStreamToAnthropic(openaiResponse.body, openaiModelId) | |
| return new Response(transformStream, { | |
| status: 200, | |
| headers: { | |
| 'Content-Type': 'text/event-stream', | |
| 'Cache-Control': 'no-cache', | |
| Connection: 'keep-alive', | |
| }, | |
| }) | |
| } | |
| } | |
| function modelsListUrlFromOpenAIBase(base: string): string { | |
| const b = normalizeBaseUrl(base) | |
| if (b.endsWith('/v1')) { | |
| return `${b}/models` | |
| } | |
| return `${b}/v1/models` | |
| } | |
| function parseOpenAIStyleModelList(json: unknown): string[] { | |
| if (!json || typeof json !== 'object') { | |
| return [] | |
| } | |
| const o = json as Record<string, unknown> | |
| const data = o.data | |
| if (Array.isArray(data)) { | |
| const ids: string[] = [] | |
| for (const item of data) { | |
| if (item && typeof item === 'object' && 'id' in item && typeof (item as { id: unknown }).id === 'string') { | |
| ids.push((item as { id: string }).id) | |
| } | |
| } | |
| return [...new Set(ids.filter(Boolean))] | |
| } | |
| const models = o.models | |
| if (Array.isArray(models)) { | |
| const ids: string[] = [] | |
| for (const item of models) { | |
| if (typeof item === 'string') { | |
| ids.push(item) | |
| } else if (item && typeof item === 'object' && 'id' in item && typeof (item as { id: unknown }).id === 'string') { | |
| ids.push((item as { id: string }).id) | |
| } | |
| } | |
| return [...new Set(ids.filter(Boolean))] | |
| } | |
| return [] | |
| } | |
| /** | |
| * GET /v1/models (OpenAI-compatible). | |
| */ | |
| export async function fetchOpenAICompatibleModelIds( | |
| baseUrl: string, | |
| apiKey?: string, | |
| ): Promise<string[]> { | |
| const url = modelsListUrlFromOpenAIBase(baseUrl) | |
| const headers: Record<string, string> = {} | |
| if (apiKey) { | |
| headers.Authorization = `Bearer ${apiKey}` | |
| } | |
| const res = await fetch(url, { headers, signal: AbortSignal.timeout(20_000) }) | |
| if (!res.ok) { | |
| const body = await res.text().catch(() => '') | |
| throw new Error(`OpenAI-compatible /v1/models failed (${res.status})${body ? `: ${body.slice(0, 200)}` : ''}`) | |
| } | |
| const json: unknown = await res.json() | |
| const ids = parseOpenAIStyleModelList(json) | |
| return ids | |
| } | |
| /** | |
| * GET /v1/models (Anthropic API). | |
| */ | |
| export async function fetchAnthropicCompatibleModelIds( | |
| baseUrl: string, | |
| apiKey?: string, | |
| ): Promise<string[]> { | |
| let root = baseUrl.replace(/\/$/, '') | |
| if (root.endsWith('/v1')) { | |
| root = root.slice(0, -3) | |
| } | |
| const url = `${root}/v1/models` | |
| const headers: Record<string, string> = { | |
| 'anthropic-version': '2023-06-01', | |
| } | |
| if (apiKey) { | |
| headers['x-api-key'] = apiKey | |
| } | |
| const res = await fetch(url, { headers, signal: AbortSignal.timeout(20_000) }) | |
| if (!res.ok) { | |
| const body = await res.text().catch(() => '') | |
| throw new Error(`Anthropic /v1/models failed (${res.status})${body ? `: ${body.slice(0, 200)}` : ''}`) | |
| } | |
| const json: unknown = await res.json() | |
| const ids = parseOpenAIStyleModelList(json) | |
| return ids | |
| } |