UNPKG

@just-every/ensemble

Version:

LLM provider abstraction layer with unified streaming interface

141 lines 6.42 kB
import { BaseModelProvider } from './base_provider.js'; import { costTracker } from '../utils/cost_tracker.js'; import { log_llm_error, log_llm_request, log_llm_response } from '../utils/llm_logger.js'; import { fetchWithTimeout } from '../utils/fetch_with_timeout.js'; const RUNWAY_BASE = process.env.RUNWAY_API_BASE || 'https://api.dev.runwayml.com'; const RUNWAY_VERSION = process.env.RUNWAY_API_VERSION || '2024-11-06'; function mapRatio(size) { const s = String(size || 'square'); if (s === 'landscape' || s === '1792x1024' || s === '1536x1024' || s === '1920x1080' || s === '1280x720') return '1920:1080'; if (s === 'portrait' || s === '1024x1792' || s === '1080x1920' || s === '720x1280') return '1080:1920'; return '1024:1024'; } const shouldRetry = (error) => { const message = String(error?.message || '').toLowerCase(); return error?.name === 'AbortError' || message.includes('timed out'); }; const withRetries = async (fn, attempts = 3) => { let lastError; for (let attempt = 1; attempt <= attempts; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (!shouldRetry(error) || attempt === attempts) throw error; await new Promise(r => setTimeout(r, attempt * 500)); } } throw lastError; }; export class RunwayProvider extends BaseModelProvider { constructor() { super('runway'); } async *createResponseStream() { throw new Error('Runway provider does not support text streaming'); } async createImage(prompt, model, agent, opts = {}) { const apiKey = process.env.RUNWAY_API_KEY; const requestId = log_llm_request(agent.agent_id || 'default', 'runway', model, { prompt, opts }, new Date()); let success = false; try { if (!apiKey) throw new Error('RUNWAY_API_KEY is not set'); const body = { promptText: prompt, ratio: mapRatio(opts.size), model: (model && model.toLowerCase().includes('turbo')) ? 'gen4_image_turbo' : 'gen4_image', }; if (opts?.source_images) { const srcs = Array.isArray(opts.source_images) ? opts.source_images : [opts.source_images]; body.referenceImages = srcs.slice(0, 3).map((v, i) => { const uri = typeof v === 'string' ? v : v?.data || v; return { uri, tag: `ref${i + 1}` }; }); } const createRes = await withRetries(() => fetchWithTimeout(`${RUNWAY_BASE}/v1/text_to_image`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, 'X-Runway-Version': RUNWAY_VERSION, }, body: JSON.stringify(body), }, 20000), 3); if (!createRes.ok) throw new Error(`Runway create failed: ${createRes.status} ${await createRes.text()}`); const created = await createRes.json(); const id = created?.id || created?.task?.id; if (!id) throw new Error('Runway: missing task id'); const pollUrl = `${RUNWAY_BASE}/v1/tasks/${encodeURIComponent(id)}`; const started = Date.now(); const timeoutMs = 180000; let urls = []; while (true) { const response = await withRetries(() => fetchWithTimeout(pollUrl, { headers: { Authorization: `Bearer ${apiKey}`, 'X-Runway-Version': RUNWAY_VERSION, }, }, 15000), 2); if (!response.ok) throw new Error(`Runway poll failed: ${response.status} ${await response.text()}`); const info = await response.json(); const status = (info?.status || info?.task?.status || '').toLowerCase(); const extracted = []; const tryPush = (value) => { if (!value) return; if (typeof value === 'string') extracted.push(value); else if (Array.isArray(value)) value.forEach(tryPush); else if (typeof value === 'object') { if (typeof value.url === 'string') extracted.push(value.url); if (typeof value.uri === 'string') extracted.push(value.uri); if (Array.isArray(value.images)) value.images.forEach(tryPush); if (Array.isArray(value.assets)) value.assets.forEach(tryPush); if (value.image) tryPush(value.image); } }; tryPush(info?.output); tryPush(info?.assets); tryPush(info?.task?.output); if (status === 'succeeded' || status === 'completed' || (extracted.length > 0 && (status === 'success' || status === ''))) { urls = extracted; break; } if (status === 'failed' || status === 'canceled' || status === 'error') { throw new Error(`Runway task failed (status=${status || 'unknown'})`); } if (Date.now() - started > timeoutMs) throw new Error('Runway generation timed out'); await new Promise(r => setTimeout(r, 1200)); } if (!urls.length) throw new Error('Runway: no image url in response'); costTracker.addUsage({ model, image_count: urls.length, request_id: opts?.request_id, metadata: { source: 'runway' } }); success = true; return urls; } catch (err) { log_llm_error(requestId, err); throw err; } finally { log_llm_response(requestId, { ok: success }); } } } export const runwayProvider = new RunwayProvider(); //# sourceMappingURL=runway.js.map