/** * Grok (xAI) Video Generation Adapter * * Async task pattern: submit → poll → return video URL. * * REST endpoints: * - Submit: POST /v1/videos/generations * - Poll: GET /v1/videos/{request_id} * * Supported models: * - grok-imagine-video ($0.05/sec) * * Authentication: Bearer token via Authorization header * * API docs: https://docs.x.ai/developers/rest-api-reference/inference/videos */ import type { VideoGenerationConfig, VideoGenerationOptions, VideoGenerationResult, } from '../types'; import { probeAuth } from '../probe-auth'; import { runPolledTask } from '../polled-task'; const DEFAULT_MODEL = 'grok-imagine-video'; const DEFAULT_BASE_URL = 'https://api.x.ai/v1'; const POLL_INTERVAL_MS = 10_000; // 10 seconds const MAX_POLL_ATTEMPTS = 60; // 10 minutes max /** Dimension defaults per aspect ratio */ function getDimensions(aspectRatio?: string): { width: number; height: number; } { switch (aspectRatio) { case '9:16': return { width: 720, height: 1280 }; case '1:1': return { width: 1080, height: 1080 }; case '4:3': return { width: 1024, height: 768 }; default: return { width: 1280, height: 720 }; // 16:9 } } /** Common headers for all Grok Video API calls */ function apiHeaders(apiKey: string): Record { return { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }; } // --------------------------------------------------------------------------- // REST types // --------------------------------------------------------------------------- interface GrokVideoSubmitResponse { request_id: string; } interface GrokVideoPollResponse { status: string; // "pending" | "done" | "failed" progress?: number; // 0-100 video?: { url: string; duration: number; respect_moderation?: boolean; }; model?: string; } // --------------------------------------------------------------------------- // Connectivity test // --------------------------------------------------------------------------- /** * Lightweight connectivity test — validates API key by making a minimal * request that triggers auth check. 401/403 means key invalid. */ export async function testGrokVideoConnectivity( config: VideoGenerationConfig, ): Promise<{ success: boolean; message: string }> { const baseUrl = config.baseUrl || DEFAULT_BASE_URL; return probeAuth({ providerName: 'Grok Video', request: () => fetch(`${baseUrl}/videos/generations`, { method: 'POST', redirect: 'manual', headers: apiHeaders(config.apiKey), body: JSON.stringify({ model: config.model || DEFAULT_MODEL, prompt: '', }), }), }); } // --------------------------------------------------------------------------- // Submit // --------------------------------------------------------------------------- async function submitVideoGeneration( baseUrl: string, apiKey: string, model: string, options: VideoGenerationOptions, ): Promise { const body: Record = { model, prompt: options.prompt, }; if (options.duration) body.duration = options.duration; const response = await fetch(`${baseUrl}/videos/generations`, { method: 'POST', headers: apiHeaders(apiKey), body: JSON.stringify(body), }); if (!response.ok) { const text = await response.text(); throw new Error(`Grok video submit failed (${response.status}): ${text}`); } const data = (await response.json()) as GrokVideoSubmitResponse; if (!data.request_id) { throw new Error('Grok video returned empty request_id'); } return data.request_id; } // --------------------------------------------------------------------------- // Poll // --------------------------------------------------------------------------- async function pollVideoStatus( baseUrl: string, apiKey: string, requestId: string, ): Promise { const response = await fetch(`${baseUrl}/videos/${requestId}`, { method: 'GET', headers: apiHeaders(apiKey), }); if (!response.ok) { const text = await response.text(); throw new Error(`Grok video poll failed (${response.status}): ${text}`); } return response.json() as Promise; } // --------------------------------------------------------------------------- // Public entry point // --------------------------------------------------------------------------- export async function generateWithGrokVideo( config: VideoGenerationConfig, options: VideoGenerationOptions, ): Promise { const model = config.model || DEFAULT_MODEL; const baseUrl = config.baseUrl || DEFAULT_BASE_URL; return runPolledTask({ submit: async () => ({ status: 'submitted', taskId: await submitVideoGeneration(baseUrl, config.apiKey, model, options), }), poll: async (requestId) => { const result = await pollVideoStatus(baseUrl, config.apiKey, requestId); if (result.status === 'done') { if (!result.video?.url) { throw new Error('Grok video task completed but no video URL returned'); } const { width, height } = getDimensions(options.aspectRatio); return { status: 'done', result: { url: result.video.url, duration: result.video.duration || options.duration || 6, width, height, }, }; } if (result.status === 'failed') { return { status: 'failed', message: `Grok video generation failed: ${JSON.stringify(result)}`, }; } return { status: 'pending' }; }, intervalMs: POLL_INTERVAL_MS, maxAttempts: MAX_POLL_ATTEMPTS, label: 'Grok video generation', formatTimeout: ({ taskId, elapsedMs }) => `Grok video generation timed out after ${elapsedMs / 1000}s (request: ${taskId})`, }); }