// xAI Grok Imagine — text-to-image + single/multi image editing // Docs: // https://docs.x.ai/developers/model-capabilities/images/generation // https://docs.x.ai/developers/model-capabilities/images/editing // https://docs.x.ai/developers/model-capabilities/images/multi-image-editing import { sizeToAspectRatio } from "./_base.js"; import { PROVIDER_MEDIA } from "../../providers/index.js"; const IMG_CFG = PROVIDER_MEDIA["xai"]?.imageConfig || {}; const GENERATIONS_URL = IMG_CFG.baseUrl || "https://api.x.ai/v1/images/generations"; const EDITS_URL = IMG_CFG.editsUrl || "https://api.x.ai/v1/images/edits"; const ASPECT_RATIOS = new Set([ "auto", "1:1", "16:9", "9:16", "4:3", "3:2", "2:3", "9:19.5", "20:9", ]); function hasEditInput(body) { if (!body || typeof body !== "object") return false; if (body.image) return true; return Array.isArray(body.images) && body.images.some(Boolean); } /** Normalize client image input → xAI image ref object */ function toXaiImageRef(input) { if (!input) return null; if (typeof input === "object") { // Already xAI-shaped or partial if (input.file_id) { return { type: input.type || "image_url", file_id: input.file_id, ...(input.url ? { url: input.url } : {}), }; } if (input.url) { return { type: input.type || "image_url", url: input.url }; } return null; } if (typeof input !== "string") return null; const trimmed = input.trim(); if (!trimmed) return null; // Public URL or data URI if (/^https?:\/\//i.test(trimmed) || /^data:image\//i.test(trimmed)) { return { type: "image_url", url: trimmed }; } // Raw base64 → data URI return { type: "image_url", url: `data:image/png;base64,${trimmed}` }; } function collectImageRefs(body) { const refs = []; if (Array.isArray(body.images)) { for (const item of body.images) { const ref = toXaiImageRef(item); if (ref) refs.push(ref); } } if (body.image) { const ref = toXaiImageRef(body.image); if (ref) refs.push(ref); } // xAI multi-edit supports up to 3 source images return refs.slice(0, 3); } function resolveAspectRatio(body) { if (typeof body.aspect_ratio === "string" && body.aspect_ratio.trim()) { const ratio = body.aspect_ratio.trim(); if (ASPECT_RATIOS.has(ratio)) return ratio; // Pass through unknown ratio strings (upstream will validate) return ratio; } // OpenAI-style size → aspect ratio (skip auto) if (body.size && body.size !== "auto") { return sizeToAspectRatio(body.size); } return undefined; } function resolveResolution(body) { if (typeof body.resolution !== "string") return undefined; const value = body.resolution.trim().toLowerCase(); if (!value || value === "auto") return undefined; return value; // "1k" | "2k" } export default { buildUrl: (_model, _credentials, body) => (hasEditInput(body) ? EDITS_URL : GENERATIONS_URL), buildHeaders: (creds) => { const headers = { "Content-Type": "application/json", ...(IMG_CFG.headers || {}) }; const key = creds?.apiKey || creds?.accessToken; if (key) headers["Authorization"] = `Bearer ${key}`; return headers; }, buildBody: (model, body) => { const req = { model, prompt: body.prompt, }; if (body.n != null) req.n = body.n; if (body.response_format) req.response_format = body.response_format; const aspectRatio = resolveAspectRatio(body); if (aspectRatio) req.aspect_ratio = aspectRatio; const resolution = resolveResolution(body); if (resolution) req.resolution = resolution; const refs = collectImageRefs(body); if (refs.length === 1) { req.image = refs[0]; } else if (refs.length > 1) { req.images = refs; } return req; }, // xAI already returns OpenAI-compatible { created, data: [{ url | b64_json }] } normalize: (responseBody) => responseBody, };