@just-every/ensemble
Version:
LLM provider abstraction layer with unified streaming interface
93 lines • 3.8 kB
JavaScript
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';
function mapImageSize(size) {
if (!size)
return undefined;
const s = String(size);
if (s === 'square')
return 'square_hd';
if (s === '1024x1024')
return 'square_hd';
if (s === 'landscape' || s === '1792x1024' || s === '1536x1024')
return 'landscape_16_9';
if (s === 'portrait' || s === '1024x1792' || s === '1024x1536')
return 'portrait_16_9';
return undefined;
}
export class FALProvider extends BaseModelProvider {
constructor() {
super('fal');
}
async *createResponseStream() {
throw new Error('FAL provider does not support text streaming');
}
endpointFor(model) {
const m = model.toLowerCase();
if (m.startsWith('recraft'))
return { path: 'fal-ai/recraft/v3/text-to-image', bodyMode: 'top' };
if (m.includes('runway') || m.includes('gen4'))
return { path: 'runwayml/gen4-image', bodyMode: 'input' };
if (m.includes('schnell'))
return { path: 'fal-ai/flux/schnell', bodyMode: 'top' };
if (m.includes('dev'))
return { path: 'fal-ai/flux/dev', bodyMode: 'top' };
if (m.includes('kontext') || m.includes('pro'))
return { path: 'fal-ai/flux-pro/kontext', bodyMode: 'top' };
return { path: 'fal-ai/flux/schnell', bodyMode: 'top' };
}
async createImage(prompt, model, agent, opts = {}) {
const falKey = process.env.FAL_KEY;
const requestId = log_llm_request(agent.agent_id || 'default', 'fal', model, { prompt, opts }, new Date());
try {
if (!falKey)
throw new Error('FAL_KEY is not set');
const { path, bodyMode } = this.endpointFor(model);
const size = mapImageSize(opts.size);
const bodyInput = bodyMode === 'top' ? { prompt } : { input: { prompt } };
if (size) {
if (bodyMode === 'top')
bodyInput.image_size = size;
else
bodyInput.input.image_size = size;
}
if (opts?.response_format === 'b64_json') {
if (bodyMode === 'top')
bodyInput.sync_mode = true;
else
bodyInput.input.sync_mode = true;
}
const res = await fetch(`https://fal.run/${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Key ${falKey}`,
},
body: JSON.stringify(bodyInput),
});
if (!res.ok)
throw new Error(`FAL request failed: ${res.status} ${await res.text()}`);
const data = await res.json();
const images = [];
const arr = data?.images || data?.output?.images || [];
for (const im of arr)
if (im?.url)
images.push(im.url);
if (!images.length && data?.url)
images.push(data.url);
if (!images.length)
throw new Error('FAL: no image url in response');
costTracker.addUsage({ model, image_count: images.length, request_id: opts?.request_id, metadata: { source: 'fal' } });
return images;
}
catch (err) {
log_llm_error(requestId, err);
throw err;
}
finally {
log_llm_response(requestId, { ok: true });
}
}
}
export const falProvider = new FALProvider();
//# sourceMappingURL=fal.js.map