learning_ai_common_plat/packages/llm-router/src/client.ts

69 lines
2.0 KiB
TypeScript

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 = provider.apiKeyEnv ? process.env[provider.apiKeyEnv] : null;
if (provider.apiKeyEnv && !apiKey) {
throw new Error(`Missing API key: env var ${provider.apiKeyEnv} is not set`);
}
const url = `${provider.baseUrl}/chat/completions`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...provider.extraHeaders,
};
if (apiKey) {
headers.Authorization = `Bearer ${apiKey}`;
}
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);
}
}