Repository Public
1import { ProviderError } from "./types";2import { log, redact } from "../lib/log";34const BASE = "https://api.x.ai/v1";5/** Hosts the server is allowed to download generated media from (SSRF allow-list). */6const MEDIA_HOSTS = [/^vidgen\.x\.ai$/, /^imgen\.x\.ai$/, /^files-cdn\.x\.ai$/, /(^|\.)x\.ai$/];78interface Breaker { failures: number; openUntil: number }9const breakers = new Map<string, Breaker>();10const BREAKER_THRESHOLD = 5, BREAKER_OPEN_MS = 60_000;1112function breakerFor(key: string) {13 let b = breakers.get(key);14 if (!b) breakers.set(key, (b = { failures: 0, openUntil: 0 }));15 return b;16}17export function breakerState() {18 return [...breakers.entries()].map(([key, b]) => ({ key, failures: b.failures, open: b.openUntil > Date.now() }));19}20const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));2122export interface XaiFetchOpts { method?: "GET" | "POST"; body?: unknown; timeoutMs?: number; retries?: number; breaker?: string; apiKey: string }2324/** JSON call to the xAI API with timeout, bounded exponential backoff and a per-endpoint circuit breaker. */25export async function xaiJson<T = unknown>(path: string, o: XaiFetchOpts): Promise<{ status: number; data: T }> {26 const key = o.breaker ?? path.split("/")[1] ?? "xai";27 const b = breakerFor(key);28 if (b.openUntil > Date.now()) throw new ProviderError(`Circuit open for xAI ${key}: retry after ${new Date(b.openUntil).toISOString()}`, { retryable: true, code: "circuit_open" });29 const retries = o.retries ?? 2;30 let last: ProviderError | null = null;31 for (let attempt = 0; attempt <= retries; attempt++) {32 if (attempt > 0) await sleep(Math.min(20_000, 1000 * 2 ** attempt) + Math.random() * 500);33 try {34 const res = await fetch(`${BASE}${path}`, {35 method: o.method ?? (o.body === undefined ? "GET" : "POST"),36 headers: { Authorization: `Bearer ${o.apiKey}`, ...(o.body === undefined ? {} : { "Content-Type": "application/json" }) },37 body: o.body === undefined ? undefined : JSON.stringify(o.body),38 signal: AbortSignal.timeout(o.timeoutMs ?? 120_000),39 });40 const text = await res.text();41 let data: unknown = null;42 try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text.slice(0, 500) }; }43 if (res.ok) { b.failures = 0; return { status: res.status, data: data as T }; }44 const msg = redact(extractMessage(data) ?? `HTTP ${res.status}`);45 const retryable = res.status === 429 || res.status >= 500;46 last = new ProviderError(`xAI ${path} failed (${res.status}): ${msg}`, { status: res.status, retryable, code: res.status === 429 ? "rate_limited" : undefined });47 if (!retryable) throw last;48 } catch (e) {49 if (e instanceof ProviderError) { if (!e.retryable) throw e; last = e; }50 else last = new ProviderError(`xAI ${path} network error: ${redact((e as Error).message)}`, { retryable: true, code: "network" });51 }52 log("warn", "xai.retry", { path, attempt, error: last?.message });53 }54 b.failures += 1;55 if (b.failures >= BREAKER_THRESHOLD) { b.openUntil = Date.now() + BREAKER_OPEN_MS; log("error", "xai.circuit_open", { key }); }56 throw last ?? new ProviderError("xAI call failed", { retryable: true });57}5859function extractMessage(d: unknown): string | null {60 if (!d || typeof d !== "object") return null;61 const o = d as Record<string, unknown>;62 if (typeof o.error === "string") return o.error;63 if (o.error && typeof o.error === "object" && typeof (o.error as Record<string, unknown>).message === "string") return (o.error as { message: string }).message;64 if (typeof o.message === "string") return o.message;65 return typeof o.raw === "string" ? o.raw : null;66}6768/** Download generated media. Only xAI hosts over HTTPS; size-capped. */69export async function downloadMedia(url: string, maxBytes = 400 * 1024 * 1024): Promise<Buffer> {70 const u = new URL(url);71 if (u.protocol !== "https:" || !MEDIA_HOSTS.some((re) => re.test(u.hostname))) {72 throw new ProviderError(`Refusing to download from unexpected host ${u.hostname}`, { retryable: false, code: "ssrf_blocked" });73 }74 let last: Error | null = null;75 for (let attempt = 0; attempt < 6; attempt++) {76 if (attempt > 0) await sleep(Math.min(20_000, 2000 * 2 ** (attempt - 1)));77 try {78 const res = await fetch(u, { signal: AbortSignal.timeout(180_000), redirect: "error" });79 if (!res.ok) throw new Error(`HTTP ${res.status}`);80 const buf = Buffer.from(await res.arrayBuffer());81 if (buf.length === 0 || buf.length > maxBytes) throw new Error(`unexpected size ${buf.length}`);82 return buf;83 } catch (e) { last = e as Error; }84 }85 throw new ProviderError(`Media download failed: ${last?.message}`, { retryable: true, code: "download" });86}87export const isAllowedMediaHost = (url: string) => { try { const u = new URL(url); return u.protocol === "https:" && MEDIA_HOSTS.some((re) => re.test(u.hostname)); } catch { return false; } };88