import { configError, jobFailed } from './errors.js';
import * as http from './http.js';
/** POST /jobs — Enqueue a job (202 with the queued job). */
export const enqueue = (client, spec) => http.post(client, '/jobs', { body: spec });
/** GET /jobs/{id} — Job status: queued/processing/succeeded/failed. */
export const get = (client, id) => http.get(client, '/jobs/{id}', { params: { id } });
/** GET /jobs/{id}/result — Job result. 409 while still running, 410 once expired (24h). */
export const getResult = (client, id) => http.get(client, '/jobs/{id}/result', { params: { id } });
// ---------------------------------------------------------------------------
// Payload builders (one per job type). Options merge first, so `type` and
// the primary input can never be overridden.
/** Check up to 100 domains in one job. */
export const domainAvailabilityBulk = (domains) => ({
type: 'domain-availability-bulk',
domains: [...domains],
});
/** Embed INPUT text (`model` defaults server-side). */
export const embedding = (input, options = {}) => ({
...options,
type: 'embedding',
input,
});
/** Chat completion over MESSAGES. */
export const llmChat = (messages, options = {}) => ({ ...options, type: 'llm-chat', messages: [...messages] });
/** Raw completion of PROMPT. */
export const llmRawCompletion = (prompt, options = {}) => ({ ...options, type: 'llm-raw-completion', prompt });
/** NLP analysis of INPUT text. */
export const nlpAnalyze = (input, options = {}) => ({
...options,
type: 'nlp-analyze',
input,
});
/** Language detection of INPUT text. */
export const nlpDetectLanguage = (input) => ({
type: 'nlp-detect-language',
input,
});
const MIME_TYPES = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.gif': 'image/gif',
};
const guessMime = (path) => {
const lower = path.toLowerCase();
return Object.entries(MIME_TYPES).find(([ext]) => lower.endsWith(ext))?.[1];
};
const toBase64 = (bytes) => {
let binary = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
}
return btoa(binary);
};
/** OpenAI-style text content part. */
export const textPart = (text) => ({ type: 'text', text });
/**
* OpenAI-style image_url content part from SOURCE (a file path or bytes),
* embedded as a base64 data URL. The MIME type is sniffed from a path's
* extension (png/jpg/jpeg/webp/gif) or passed explicitly.
*/
export const imagePart = async (source, mimeType) => {
const mime = mimeType ?? (typeof source === 'string' ? guessMime(source) : undefined);
if (!mime)
throw configError('Cannot guess image MIME type; pass it explicitly', { source });
const bytes = typeof source === 'string' ? new Uint8Array(await (await import('node:fs/promises')).readFile(source)) : source;
return { type: 'image_url', image_url: { url: `data:${mime};base64,${toBase64(bytes)}` } };
};
/** A user message whose content is TEXT plus any IMAGE-PARTS (from `imagePart`). */
export const userMessage = (text, ...imageParts) => ({
role: 'user',
content: [textPart(text), ...imageParts],
});
const TERMINAL = new Set(['succeeded', 'failed']);
/**
* Enqueue SPEC, poll until the job reaches a terminal status, then fetch
* and return the result. Throws a jobFailed error with the failed job, or a
* timeout error when the deadline passes first.
*/
export const run = async (client, spec, options = {}) => {
const { job } = await enqueue(client, spec);
const id = job.id;
const { job: final } = await http.pollUntil(() => get(client, id), (r) => TERMINAL.has(r.job.status), {
intervalMs: options.intervalMs,
timeoutMs: options.timeoutMs ?? 120000,
message: `Timed out waiting for job ${id}`,
sleep: client.sleep,
});
if (final.status === 'succeeded')
return getResult(client, id);
throw jobFailed(`Job ${id} failed`, final);
};