import type { ChatCompletionRequest, ChatCompletionResponse, ProviderConfig } from './types.js'; /** * Send an OpenAI-compatible chat completion request to a provider. * Returns the parsed response or throws on HTTP/network errors. */ export async function sendChatCompletion( provider: ProviderConfig, modelId: string, request: ChatCompletionRequest, timeoutMs: number = 30_000 ): Promise<{ response: ChatCompletionResponse; latencyMs: number; status: number }> { const apiKey = process.env[provider.apiKeyEnv]; if (!apiKey) { throw new Error(`Missing API key: env var ${provider.apiKeyEnv} is not set`); } const url = `${provider.baseUrl}/chat/completions`; const headers: Record = { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, ...provider.extraHeaders, }; const body = JSON.stringify({ model: modelId, messages: request.messages, ...(request.temperature !== undefined && { temperature: request.temperature }), ...(request.max_tokens !== undefined && { max_tokens: request.max_tokens }), ...(request.top_p !== undefined && { top_p: request.top_p }), stream: false, }); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); const start = Date.now(); try { const res = await fetch(url, { method: 'POST', headers, body, signal: controller.signal, }); const latencyMs = Date.now() - start; if (res.status === 429) { return { response: null as unknown as ChatCompletionResponse, latencyMs, status: 429, }; } if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`${provider.name} returned ${res.status}: ${text.slice(0, 200)}`); } const data = (await res.json()) as ChatCompletionResponse; return { response: data, latencyMs, status: res.status }; } finally { clearTimeout(timer); } }