/** * OpenAI-compatible provider — proxies requests to any OpenAI-compatible backend * (vLLM, Ollama, LiteLLM, etc.) using native fetch(). */ export function createProvider(config) { const { id, baseUrl, apiKey, models } = config; // Normalize baseUrl: strip trailing slash, ensure /v1 is present let normalizedUrl = baseUrl.replace(/\/+$/, ''); if (!normalizedUrl.endsWith('/v1')) { normalizedUrl += '/v1'; } const completionsUrl = `${normalizedUrl}/chat/completions`; function buildHeaders() { const headers = { 'Content-Type': 'application/json' }; if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`; return headers; } function buildBody(messages, modelId, options, stream) { const providerModelId = models[modelId] || modelId; const body = { model: providerModelId, messages, stream }; if (options.temperature !== undefined) body.temperature = options.temperature; if (options.max_tokens !== undefined) body.max_tokens = options.max_tokens; if (options.top_p !== undefined) body.top_p = options.top_p; if (options.stop !== undefined) body.stop = options.stop; return body; } async function request(messages, modelId, options, stream) { const body = buildBody(messages, modelId, options, stream); const headers = buildHeaders(); let response; try { response = await fetch(completionsUrl, { method: 'POST', headers, body: JSON.stringify(body) }); } catch (error) { throw new Error(`OpenAI-compatible provider request failed: ${error.message}`); } if (!response.ok) { let errorBody = ''; try { errorBody = await response.text(); } catch { /* ignore */ } throw new Error( `OpenAI-compatible provider returned ${response.status}${errorBody ? `: ${errorBody}` : ''}` ); } return response; } async function chat(messages, modelId, options = {}) { const response = await request(messages, modelId, options, false); return response.json(); } async function chatStream(messages, modelId, options = {}) { const response = await request(messages, modelId, options, true); return response.body; } return { id, chat, chatStream }; }