- Add dedicated xAI image adapter with generate + edit (multi-image) via /v1/images/generations and /v1/images/edits, plus aspect_ratio/resolution UI - Support importing existing API keys and exposing connection api-key routes - Add global/per-provider connect timeout overrides from settings - Keep unrelated provider UX improvements on this branch; no Grok quota tracking
138 lines
3.9 KiB
JavaScript
138 lines
3.9 KiB
JavaScript
// 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,
|
|
};
|