// OpenAI-compatible provider — analysis only. // // Same wire format as OpenAI's chat.completions endpoint, but pointed // at a user-supplied baseURL: DeepSeek, Together, Groq, Fireworks, your // own self-hosted vLLM, etc. The user provides baseURL + apiKey + model // name; we don't ship a hardcoded model list (each backend's catalog // differs), and we don't have pricing (varies wildly per backend). // // Structurally this is a thin re-export of the OpenAI SDK with the // pricing table forced to zero — costs are reported as $0.0000 since we // can't know the backend's rates without per-deploy configuration. import OpenAI from "openai"; import { retryAPI } from "../util.js"; import { zeroCost } from "./cost.js"; // Default model lists are empty — the picker UI surfaces a free-text // model field for OpenAI-compatible. listAnalysisModels() returns the // caller-provided defaults if any were passed at construction time. const ANALYSIS_MAX_TOKENS = 16000; export function createOpenAICompatibleProvider({ apiKey, baseURL, defaultModels = [], timeoutMs = 900_000, } = {}) { if (!baseURL) { throw new Error( "createOpenAICompatibleProvider: baseURL is required (e.g. https://api.deepseek.com/v1)" ); } // Some self-hosted backends accept any non-empty key. Default to a // sentinel so the SDK's auth header stays well-formed. const client = new OpenAI({ apiKey: apiKey || "no-auth", baseURL, timeout: timeoutMs, }); return { name: "openai-compatible", capabilities: { transcribe: false, analyze: true, listModels: defaultModels.length > 0, }, listAnalysisModels() { return [...defaultModels]; }, listTranscriptionModels() { return []; }, async transcribeAudio() { throw new Error( "openai-compatible providers are wired for analysis only. Use Gemini or OpenAI Whisper for transcription." ); }, async analyzeText({ prompt, model, onProgress = () => {}, retries = 2, signal, }) { const result = await retryAPI( () => client.chat.completions.create( { model, max_tokens: ANALYSIS_MAX_TOKENS, messages: [{ role: "user", content: prompt }], }, signal ? { signal } : undefined ), { retries, delayMs: 5000, label: "openai-compatible analysis", log: (msg) => onProgress(msg), } ); const choice = result.choices?.[0]; const text = choice?.message?.content || ""; const usage = { inputTokens: result.usage?.prompt_tokens || 0, outputTokens: result.usage?.completion_tokens || 0, thinkingTokens: 0, }; // Per-backend pricing varies — report zero. UI can warn that cost // tracking is not available for this provider. const cost = zeroCost(usage); return { text, usage, cost, finishReason: choice?.finish_reason || null, raw: result, }; }, }; }