TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import OpenAI from "openai";2import type { ChatCompletionCreateParamsStreaming, ChatCompletionTool } from "openai/resources/chat/completions";3import type { ResponseCreateParamsStreaming, Tool as ResponsesTool } from "openai/resources/responses/responses";4import {5 type AIProviderAdapter,6 type PolyModel,7 type UnifiedChatRequest,8 type UnifiedChatResponse,9 type UnifiedStreamEvent,10 type ValidationResult,11 type TokenEstimate,12 PolyProviderError,13 modelKey,14} from "@/lib/ai/core/types";15import { normalizeGenericError, refineByMessage, codeFromStatus, isRetryableStatus, parseRetryAfter } from "@/lib/ai/core/errors";16import { filterSettings, heuristicTokens } from "@/lib/ai/core/normalize";17import { collectStream } from "@/lib/ai/core/stream-utils";18import { toChatCompletionMessages, normalizeChatCompletionStream } from "../shared/openai-compat/chat-completions";19import { toResponsesInput, normalizeResponsesStream } from "../shared/openai-compat/responses";20import { XAI_CATALOG } from "./catalog";2122const BASE_URL = "https://api.x.ai/v1";23const DEFAULT_TIMEOUT_MS = 10 * 60_000;2425function client(apiKey: string, timeoutMs = DEFAULT_TIMEOUT_MS) {26 return new OpenAI({ apiKey, baseURL: BASE_URL, maxRetries: 1, timeout: timeoutMs });27}2829interface XaiLanguageModel {30 id: string;31 input_modalities?: string[];32 output_modalities?: string[];33 prompt_text_token_price?: number;34 cached_prompt_text_token_price?: number;35 completion_text_token_price?: number;36 prompt_text_token_price_long_context?: number;37 cached_prompt_text_token_price_long_context?: number;38 completion_text_token_price_long_context?: number;39 long_context_threshold?: number;40 aliases?: string[];41 created?: number;42}4344/** xAI prices are cents per 100M tokens → $/M = value / 10 000 (verified against the pricing page). */45const perMillion = (v?: number) => (typeof v === "number" ? v / 10_000 : undefined);4647export function normalizeXaiModel(m: XaiLanguageModel): PolyModel {48 const cat = XAI_CATALOG.get(m.id);49 const vision = m.input_modalities?.includes("image") ?? true;50 return {51 key: modelKey("xai", m.id),52 id: m.id,53 provider: "xai",54 displayName: cat?.displayName ?? prettify(m.id),55 family: cat?.family ?? m.id.split("-").slice(0, 2).join(" "),56 capabilities: cat?.capabilities ?? {57 text: true,58 vision,59 audioInput: false,60 audioOutput: false,61 imageGeneration: false,62 video: false,63 reasoning: !m.id.includes("non-reasoning"),64 tools: !m.id.includes("multi-agent"),65 structuredOutput: true,66 streaming: true,67 files: false,68 webSearch: true,69 },70 limits: cat?.limits ?? {},71 parameters: cat?.parameters ?? { temperature: true, topP: true, topK: true, maxTokens: true, seed: true, stop: false, frequencyPenalty: false, presencePenalty: false, reasoningEffort: false },72 status: cat?.status ?? "unknown",73 pricing: {74 inputPerMillion: perMillion(m.prompt_text_token_price),75 cachedInputPerMillion: perMillion(m.cached_prompt_text_token_price),76 outputPerMillion: perMillion(m.completion_text_token_price),77 longContext: m.long_context_threshold78 ? {79 thresholdTokens: m.long_context_threshold,80 inputPerMillion: perMillion(m.prompt_text_token_price_long_context),81 cachedInputPerMillion: perMillion(m.cached_prompt_text_token_price_long_context),82 outputPerMillion: perMillion(m.completion_text_token_price_long_context),83 }84 : undefined,85 source: "xai:/v1/language-models",86 asOf: new Date().toISOString().slice(0, 10),87 },88 metadata: { ...(cat?.metadata ?? {}), aliases: m.aliases ?? [], sortWeight: cat?.sortWeight ?? 0, createdAt: m.created ? new Date(m.created * 1000).toISOString() : undefined },89 };90}9192function prettify(id: string): string {93 return id.replace(/^grok/, "Grok").replace(/-/g, " ").replace(/\b(\w)/g, (c) => c.toUpperCase());94}9596async function fetchLanguageModels(apiKey: string, signal?: AbortSignal): Promise<XaiLanguageModel[]> {97 const res = await fetch(`${BASE_URL}/language-models`, { headers: { Authorization: `Bearer ${apiKey}` }, signal });98 if (!res.ok) {99 const body = (await res.json().catch(() => ({}))) as { code?: string; error?: string };100 throw Object.assign(new Error(body.error ?? `HTTP ${res.status}`), { status: res.status, error: body, headers: res.headers });101 }102 const data = (await res.json()) as { models?: XaiLanguageModel[] };103 return data.models ?? [];104}105106function buildChatParams(req: UnifiedChatRequest): ChatCompletionCreateParamsStreaming {107 const { settings } = filterSettings(req.settings, req.modelInfo);108 const params: ChatCompletionCreateParamsStreaming = {109 model: req.model,110 messages: toChatCompletionMessages(req.system, req.messages, { systemRole: "system", inlineFiles: true }),111 stream: true,112 stream_options: { include_usage: true },113 };114 if (settings.maxTokens !== undefined) params.max_completion_tokens = settings.maxTokens;115 if (settings.temperature !== undefined) params.temperature = settings.temperature;116 if (settings.topP !== undefined) params.top_p = settings.topP;117 if (settings.seed !== undefined) params.seed = settings.seed;118 if (settings.stop?.length) params.stop = settings.stop.slice(0, 4);119 if (settings.topK !== undefined) (params as unknown as Record<string, unknown>).top_k = settings.topK;120 if (settings.reasoningEffort) {121 const effort = settings.reasoningEffort === "minimal" ? "low" : settings.reasoningEffort === "max" ? "xhigh" : settings.reasoningEffort;122 (params as unknown as Record<string, unknown>).reasoning_effort = effort;123 }124 if (settings.responseFormat?.type === "json_schema" && settings.responseFormat.schema) {125 params.response_format = { type: "json_schema", json_schema: { name: settings.responseFormat.schemaName ?? "response", schema: settings.responseFormat.schema, strict: settings.responseFormat.strict ?? true } };126 } else if (settings.responseFormat?.type === "json") {127 params.response_format = { type: "json_object" };128 }129 if (req.tools?.length) {130 params.tools = req.tools.map<ChatCompletionTool>((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters, strict: t.strict } }));131 const tc = settings.toolChoice;132 if (tc === "none" || tc === "required" || tc === "auto") params.tool_choice = tc;133 else if (tc && typeof tc === "object") params.tool_choice = { type: "function", function: { name: tc.name } };134 }135 return params;136}137138function buildResponsesParams(req: UnifiedChatRequest): ResponseCreateParamsStreaming {139 const { settings } = filterSettings(req.settings, req.modelInfo);140 const params: ResponseCreateParamsStreaming = {141 model: req.model,142 input: toResponsesInput(req.messages, { nativeFiles: false, replayReasoning: false }),143 stream: true,144 store: false,145 };146 if (req.system?.trim()) params.instructions = req.system;147 if (settings.maxTokens !== undefined) params.max_output_tokens = settings.maxTokens;148 if (settings.temperature !== undefined) params.temperature = settings.temperature;149 if (settings.topP !== undefined) params.top_p = settings.topP;150 if (settings.reasoningEffort) {151 const effort = settings.reasoningEffort === "minimal" ? "low" : settings.reasoningEffort === "max" ? "xhigh" : settings.reasoningEffort;152 params.reasoning = { effort: effort as "low" | "medium" | "high", summary: "auto" };153 } else if (req.modelInfo?.capabilities.reasoning && settings.includeReasoning !== false) {154 params.reasoning = { summary: "auto" };155 }156 if (settings.responseFormat?.type === "json_schema" && settings.responseFormat.schema) {157 params.text = { format: { type: "json_schema", name: settings.responseFormat.schemaName ?? "response", schema: settings.responseFormat.schema, strict: settings.responseFormat.strict ?? true } };158 }159 const tools: ResponsesTool[] = [];160 if (settings.webSearch) tools.push({ type: "web_search" } as ResponsesTool);161 for (const t of req.tools ?? []) tools.push({ type: "function", name: t.name, description: t.description, parameters: t.parameters, strict: t.strict ?? true });162 if (tools.length) params.tools = tools;163 return params;164}165166export const xaiAdapter: AIProviderAdapter = {167 id: "xai",168 name: "xAI",169 keyDocsUrl: "https://console.x.ai",170 keyPrefixHint: "xai-",171172 async validateApiKey(apiKey, signal): Promise<ValidationResult> {173 const t0 = Date.now();174 try {175 const models = await fetchLanguageModels(apiKey, signal);176 return { ok: true, modelsAvailable: models.length, latencyMs: Date.now() - t0 };177 } catch (e) {178 return { ok: false, error: this.normalizeError(e).toJSON(), latencyMs: Date.now() - t0 };179 }180 },181182 async listModels(apiKey, signal): Promise<PolyModel[]> {183 try {184 const models = await fetchLanguageModels(apiKey, signal);185 return models.filter((m) => (m.output_modalities ?? ["text"]).includes("text")).map(normalizeXaiModel);186 } catch (e) {187 throw this.normalizeError(e);188 }189 },190191 async chat(req): Promise<UnifiedChatResponse> {192 return collectStream("xai", req.model, this.streamChat(req));193 },194195 async *streamChat(req: UnifiedChatRequest): AsyncIterable<UnifiedStreamEvent> {196 const meta = (req.modelInfo?.metadata ?? {}) as Record<string, unknown>;197 const useResponses = Boolean(req.settings?.webSearch) || meta.responsesOnly === true;198 const c = client(req.apiKey, req.timeoutMs);199 try {200 if (useResponses) {201 const stream = await c.responses.create(buildResponsesParams(req), { signal: req.signal });202 yield* normalizeResponsesStream(stream, "xai");203 } else {204 const stream = await c.chat.completions.create(buildChatParams(req), { signal: req.signal });205 yield* normalizeChatCompletionStream(stream, "xai");206 }207 } catch (e) {208 yield { type: "error", error: this.normalizeError(e).toJSON() };209 }210 },211212 async estimateTokens(req): Promise<TokenEstimate> {213 const text = req.messages.map((m) => m.content.map((p) => (p.type === "text" ? p.text : "")).join(" ")).join(" ") + (req.system ?? "");214 return { inputTokens: heuristicTokens(text), method: "heuristic" };215 },216217 normalizeError(error: unknown): PolyProviderError {218 if (error instanceof PolyProviderError) return error;219 const e = error as { status?: number; error?: { code?: string; error?: string; message?: string } | string; message?: string; headers?: Headers };220 const body = typeof e?.error === "object" ? e.error : undefined;221 const message = body?.error ?? body?.message ?? e?.message ?? String(error);222 const status = e?.status;223 if (status !== undefined || body) {224 let code = codeFromStatus(status);225 // xAI returns HTTP 400 for bad keys and for unknown models — disambiguate by text.226 if (/incorrect api key|bad credentials|unauthenticated|no credentials/i.test(message) || body?.code?.startsWith("unauthenticated")) code = "INVALID_API_KEY";227 else if (/model not found/i.test(message)) code = "MODEL_NOT_FOUND";228 else if (/does not support parameter|invalid-argument|invalid_image/i.test(message + (body?.code ?? ""))) code = refineByMessage("INVALID_PARAMETER", message);229 else code = refineByMessage(code, message);230 if (body?.code === "invalid_image") code = "INVALID_PARAMETER";231 return new PolyProviderError({232 code,233 message: code === "INVALID_API_KEY" ? "Invalid API key" : message.slice(0, 600),234 provider: "xai",235 status,236 retryable: isRetryableStatus(status) && code !== "INVALID_API_KEY",237 retryAfterMs: parseRetryAfter(e?.headers ?? null),238 providerCode: body?.code,239 cause: error,240 });241 }242 return normalizeGenericError("xai", error);243 },244};245