mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 04:32:31 +03:00
refactor(sse): decompose xAI Grok Imagine handler to fix complexity ratchets
The file-size red was masking two ratchet regressions (the gate aborts on the first failure): complexity 2058 > 2056 and cognitive 891 > 890. Both came from the PR's own handleXaiVideoGeneration — a single 107-line function with complexity 37 / cognitive 24, tripping `complexity`, `max-lines-per-function` (2 complexity-ratchet violations) and `sonarjs/cognitive-complexity` (1 cognitive violation). Decompose it into four cohesive units instead of rebaselining: - resolveXaiVideoOptions() — timeouts/credential/endpoints/prompt - buildXaiVideoPayload() — OmniRoute body -> xAI create payload - createXaiVideoJob() — create-job POST -> request_id | error - pollXaiVideoJob() — poll loop -> terminal outcome - buildXaiVideoResponse() — outcome -> OpenAI-like response Both ratchets now sit exactly at baseline (complexity 2056, cognitive 890) and file-size stays under cap. pollXaiVideoJob reads Date.now() only in the loop condition, so the caller keeps its timeout budget semantics. No behavior change; the 7 existing tests pass unmodified.
This commit is contained in:
@@ -28,6 +28,155 @@ interface XaiVideoLog {
|
||||
error: (scope: string, message: string) => void;
|
||||
}
|
||||
|
||||
/** Map the OmniRoute video body onto xAI's create-job payload. */
|
||||
function buildXaiVideoPayload(model: string, prompt: string, body: XaiVideoBody) {
|
||||
const payload: Record<string, unknown> = { model, prompt };
|
||||
if (typeof body.image === "string") payload.image = body.image;
|
||||
if (body.duration != null) payload.duration = Number(body.duration);
|
||||
if (typeof body.aspect_ratio === "string") payload.aspect_ratio = body.aspect_ratio;
|
||||
if (typeof body.resolution === "string") payload.resolution = body.resolution;
|
||||
return payload;
|
||||
}
|
||||
|
||||
/** POST the create-job request; resolves to the request_id or a ready error message. */
|
||||
async function createXaiVideoJob({
|
||||
baseUrl,
|
||||
token,
|
||||
payload,
|
||||
log,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
payload: Record<string, unknown>;
|
||||
log?: XaiVideoLog | null;
|
||||
}): Promise<{ requestId?: string; error?: string }> {
|
||||
const createRes = await fetch(`${baseUrl}/generations`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const createData = await createRes.json().catch(() => ({}));
|
||||
const requestId = createData?.request_id;
|
||||
if (requestId) return { requestId: String(requestId) };
|
||||
|
||||
const errorMessage =
|
||||
createData?.error?.message ||
|
||||
createData?.message ||
|
||||
"xAI video generation did not return request_id";
|
||||
if (log) {
|
||||
log.error("VIDEO", `xAI createJob failed: ${JSON.stringify(createData)}`);
|
||||
}
|
||||
return { error: String(errorMessage) };
|
||||
}
|
||||
|
||||
type XaiPollOutcome =
|
||||
| { terminal: "done"; videoUrl?: string }
|
||||
| { terminal: "failed"; error?: unknown }
|
||||
| { terminal: "timeout"; lastStatus: string };
|
||||
|
||||
/**
|
||||
* Poll statusUrl/{request_id} until a terminal status or the deadline.
|
||||
* Date.now() is read only in the loop condition, so the caller keeps full
|
||||
* control over the timeout budget it computed from its own startTime.
|
||||
*/
|
||||
async function pollXaiVideoJob({
|
||||
statusUrl,
|
||||
requestId,
|
||||
token,
|
||||
deadline,
|
||||
pollIntervalMs,
|
||||
}: {
|
||||
statusUrl: string;
|
||||
requestId: string;
|
||||
token: string;
|
||||
deadline: number;
|
||||
pollIntervalMs: number;
|
||||
}): Promise<XaiPollOutcome> {
|
||||
let lastStatus = "pending";
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
const pollRes = await fetch(`${statusUrl}/${requestId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const pollData = await pollRes.json().catch(() => ({}));
|
||||
lastStatus = pollData?.status || "pending";
|
||||
|
||||
if (lastStatus === "done") return { terminal: "done", videoUrl: pollData?.video?.url };
|
||||
if (lastStatus === "failed") return { terminal: "failed", error: pollData?.error };
|
||||
// pending / processing → keep polling
|
||||
}
|
||||
return { terminal: "timeout", lastStatus };
|
||||
}
|
||||
|
||||
/** Resolve the request knobs (timeouts, credential, endpoints, prompt) from the call. */
|
||||
function resolveXaiVideoOptions(
|
||||
body: XaiVideoBody,
|
||||
providerConfig: { baseUrl: string; statusUrl?: string },
|
||||
credentials?: { apiKey?: string; accessToken?: string } | null
|
||||
) {
|
||||
const baseUrl = providerConfig.baseUrl.replace(/\/$/, "");
|
||||
return {
|
||||
timeoutMs: Number(body.timeout_ms) > 0 ? Number(body.timeout_ms) : 300000,
|
||||
pollIntervalMs: Number(body.poll_interval_ms) > 0 ? Number(body.poll_interval_ms) : 2500,
|
||||
token: credentials?.apiKey || credentials?.accessToken,
|
||||
baseUrl,
|
||||
statusUrl: (providerConfig.statusUrl || baseUrl).replace(/\/$/, ""),
|
||||
prompt: typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
/** Map a terminal poll outcome onto the OpenAI-like video response (or an error). */
|
||||
function buildXaiVideoResponse({
|
||||
outcome,
|
||||
requestId,
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
}: {
|
||||
outcome: XaiPollOutcome;
|
||||
requestId: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
startTime: number;
|
||||
}) {
|
||||
if (outcome.terminal === "failed") {
|
||||
return { success: false, status: 502, error: String(outcome.error || "xAI video job failed") };
|
||||
}
|
||||
|
||||
if (outcome.terminal === "timeout") {
|
||||
return {
|
||||
success: false,
|
||||
status: 504,
|
||||
error: `xAI video job ${requestId} timed out (status: ${outcome.lastStatus})`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!outcome.videoUrl) {
|
||||
return { success: false, status: 502, error: "xAI video job done but no video.url" };
|
||||
}
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { videos_count: 1 },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ url: outcome.videoUrl, format: "mp4" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleXaiVideoGeneration({
|
||||
model,
|
||||
provider,
|
||||
@@ -44,100 +193,46 @@ export async function handleXaiVideoGeneration({
|
||||
log?: XaiVideoLog | null;
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const timeoutMs = Number(body.timeout_ms) > 0 ? Number(body.timeout_ms) : 300000;
|
||||
const pollIntervalMs = Number(body.poll_interval_ms) > 0 ? Number(body.poll_interval_ms) : 2500;
|
||||
const token = credentials?.apiKey || credentials?.accessToken;
|
||||
const baseUrl = providerConfig.baseUrl.replace(/\/$/, "");
|
||||
const statusUrl = (providerConfig.statusUrl || baseUrl).replace(/\/$/, "");
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
const { timeoutMs, pollIntervalMs, token, baseUrl, statusUrl, prompt } = resolveXaiVideoOptions(
|
||||
body,
|
||||
providerConfig,
|
||||
credentials
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return { success: false, status: 401, error: "xAI API key is required" };
|
||||
}
|
||||
|
||||
const payload: Record<string, unknown> = { model, prompt };
|
||||
if (typeof body.image === "string") payload.image = body.image;
|
||||
if (body.duration != null) payload.duration = Number(body.duration);
|
||||
if (typeof body.aspect_ratio === "string") payload.aspect_ratio = body.aspect_ratio;
|
||||
if (typeof body.resolution === "string") payload.resolution = body.resolution;
|
||||
|
||||
if (log) {
|
||||
log.info("VIDEO", `${provider}/${model} (xai-video) | prompt: "${prompt.slice(0, 60)}..."`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: create async job
|
||||
const createRes = await fetch(`${baseUrl}/generations`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
const created = await createXaiVideoJob({
|
||||
baseUrl,
|
||||
token,
|
||||
payload: buildXaiVideoPayload(model, prompt, body),
|
||||
log,
|
||||
});
|
||||
const createData = await createRes.json().catch(() => ({}));
|
||||
const requestId = createData?.request_id;
|
||||
if (!requestId) {
|
||||
const errorMessage =
|
||||
createData?.error?.message ||
|
||||
createData?.message ||
|
||||
"xAI video generation did not return request_id";
|
||||
if (log) {
|
||||
log.error("VIDEO", `xAI createJob failed: ${JSON.stringify(createData)}`);
|
||||
}
|
||||
return { success: false, status: 502, error: String(errorMessage) };
|
||||
if (!created.requestId) {
|
||||
return { success: false, status: 502, error: created.error };
|
||||
}
|
||||
|
||||
// Step 2: poll statusUrl/{request_id} until terminal
|
||||
const deadline = startTime + timeoutMs;
|
||||
let lastStatus = "pending";
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
const pollRes = await fetch(`${statusUrl}/${requestId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const pollData = await pollRes.json().catch(() => ({}));
|
||||
lastStatus = pollData?.status || "pending";
|
||||
const outcome = await pollXaiVideoJob({
|
||||
statusUrl,
|
||||
requestId: created.requestId,
|
||||
token,
|
||||
deadline: startTime + timeoutMs,
|
||||
pollIntervalMs,
|
||||
});
|
||||
|
||||
if (lastStatus === "done") {
|
||||
const videoUrl = pollData?.video?.url;
|
||||
if (!videoUrl) {
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
error: "xAI video job done but no video.url",
|
||||
};
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { videos_count: 1 },
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ url: videoUrl, format: "mp4" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (lastStatus === "failed") {
|
||||
const errorMessage = pollData?.error || "xAI video job failed";
|
||||
return { success: false, status: 502, error: String(errorMessage) };
|
||||
}
|
||||
// pending / processing → keep polling
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: 504,
|
||||
error: `xAI video job ${requestId} timed out (status: ${lastStatus})`,
|
||||
};
|
||||
return buildXaiVideoResponse({
|
||||
outcome,
|
||||
requestId: created.requestId,
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
Reference in New Issue
Block a user