@apicity/xai
v0.1.0
Published
X.AI / Grok provider for chat and search.
Maintainers
Readme
@apicity/xai
X.AI / Grok provider for chat and search.
Installation
npm install @apicity/xai
# or
pnpm add @apicity/xaiQuick Start
import { xai as createXai } from "@apicity/xai";
const xai = createXai({ apiKey: process.env.XAI_API_KEY! });Real-world example: structured vision analysis with Grok-4
Hand Grok-4 a portrait, a system prompt that nails down the output schema,
and text.format.type: "json_object" — get back a reproduction-ready
JSON description with deterministic shot/pose vocabulary. The flow below
is taken verbatim from
tests/integration/xai-vision-json.test.ts
and replays against
tests/recordings/xai_3613880225/vision-analysis-json_243984103/recording.har,
so the response shapes match what xAI actually returns.
import { readFile } from "node:fs/promises";
import { xai as createXai } from "@apicity/xai";
const xai = createXai({ apiKey: process.env.XAI_API_KEY! });
// 1. Load the image and inline it as a data URL. xAI also accepts
// https:// URLs, but inlining keeps the call self-contained and
// works against private hosts.
const image = await readFile("./portrait.jpg");
const base64 = image.toString("base64");
// 2. The system prompt enumerates the legal vocabulary for `shot` and
// constrains `pose` to body geometry only. Combined with
// `text.format.type: "json_object"` this gives Grok no room to drift
// off-schema — temperature 0 keeps the result reproducible.
const SYSTEM_PROMPT = [
"You are an expert image-to-prompt analyst.",
"Return only a JSON object with keys prompt, shot, and pose.",
"prompt: a single-paragraph reproduction-ready image prompt, 1900 characters or fewer, with no line breaks.",
'shot: exactly "<size>, <angle>" where size is one of extreme close-up, close-up, medium close-up, medium shot, medium long shot, long shot, or extreme long shot, and angle is one of eye-level, low-angle, high-angle, overhead, or dutch.',
"pose: only body geometry for human figures, with no clothing, hair, background, or lighting details.",
].join(" ");
// 3. Multimodal Responses request: system turn + a user turn whose
// content is an array of `input_image` + `input_text` parts.
const result = await xai.post.v1.responses({
model: "grok-4",
input: [
{ role: "system", content: SYSTEM_PROMPT },
{
role: "user",
content: [
{
type: "input_image",
image_url: `data:image/jpeg;base64,${base64}`,
detail: "high",
},
{
type: "input_text",
text: 'Analyze this image and produce a reproduction-ready JSON description with keys "prompt", "shot", and "pose".',
},
],
},
],
text: { format: { type: "json_object" } },
store: false,
temperature: 0,
max_output_tokens: 300,
});
// 4. The Responses API wraps output in a typed item array. Find the
// assistant message, then the first `output_text` part inside it.
// Discriminated unions narrow `item.type === "message"` so
// `item.content` is statically typed.
const message = result.output.find((item) => item.type === "message");
const outputText =
message?.type === "message"
? message.content.find((part) => part.type === "output_text")?.text
: undefined;
if (!outputText) throw new Error("Grok did not return output_text");
const analysis = JSON.parse(outputText) as {
prompt: string;
shot: string;
pose: string;
};
console.log(analysis.shot);
// → "medium close-up, eye-level"
console.log(analysis.pose);
// → "upright torso facing forward, head straight and centered, shoulders squared, arms relaxed downward (implied)"
// 5. Reasoning-token accounting. Grok-4 spent 623 of its 728 output
// tokens reasoning before emitting the 105-token JSON answer —
// surfaced in `usage.output_tokens_details.reasoning_tokens`.
console.log(result.usage);
// → {
// input_tokens: 2684,
// input_tokens_details: { cached_tokens: 679 },
// output_tokens: 728,
// output_tokens_details: { reasoning_tokens: 623 },
// total_tokens: 3412,
// }Notes
store: falsekeeps the response off xAI's history surface. Flip totrueto chain follow-ups viaprevious_response_id— useful for multi-turn refinement ("now describe the wardrobe") without re-uploading the image each time.- The Responses output array also carries reasoning items and tool calls
when present. Always discriminate on
item.typebefore reading content; TypeScript's narrowing keeps you honest. - For raw chat-style usage without the Responses wrapping, use
xai.post.v1.chat.completionsinstead — same auth, same model catalog, just OpenAI-compatible request/response shapes. - Errors surface as
XaiErrorwithstatusand the parsed body attached, sotry { ... } catch (e) { if (e instanceof XaiError) ... }gives you the upstream error directly.
API Reference
39 endpoints across 17 groups. Each method mirrors an upstream URL path.
batches
GET https://api.x.ai/v1/batches/{paramsOrIdOrSignal}
const res = await xai.v1.batches({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET https://api.x.ai/v1/batches/{batchId}/requests{query}
const res = await xai.v1.batches.requests({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET https://api.x.ai/v1/batches/{batchId}/results{query}
const res = await xai.v1.batches.results({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/batches
const res = await xai.v1.batches({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/batches/{batchId}:cancel
const res = await xai.v1.batches.cancel({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/batches/{batchId}/requests
const res = await xai.v1.batches.requests({ /* ... */ });Source: packages/provider/xai/src/xai.ts
chat
GET https://api.x.ai/v1/chat/deferred-completion/{requestId}
const res = await xai.v1.chat.deferredCompletion({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/chat/completions
const res = await xai.v1.chat.completions({ /* ... */ });Source: packages/provider/xai/src/xai.ts
collections
DELETE https://api.x.ai/v1/collections/{collectionId}
const res = await xai.v1.collections({ /* ... */ });Source: packages/provider/xai/src/xai.ts
DELETE https://api.x.ai/v1/collections/{collectionId}/documents/{fileId}
const res = await xai.v1.collections.documents({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET https://api.x.ai/v1/collections/{paramsOrIdOrSignal}
const res = await xai.v1.collections({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET https://api.x.ai/v1/collections/{collectionId}/documents/{paramsOrFileId}
const res = await xai.v1.collections.documents({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET https://api.x.ai/v1/collections/{collectionId}/documents:batchGet{query}
const res = await xai.v1.collections.documents.batchGet({ /* ... */ });Source: packages/provider/xai/src/xai.ts
PATCH https://api.x.ai/v1/collections/{collectionId}/documents/{fileId}
const res = await xai.v1.collections.documents({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/collections
const res = await xai.v1.collections({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/collections/{collectionId}/documents/{fileId}
const res = await xai.v1.collections.documents({ /* ... */ });Source: packages/provider/xai/src/xai.ts
PUT https://api.x.ai/v1/collections/{collectionId}
const res = await xai.v1.collections({ /* ... */ });Source: packages/provider/xai/src/xai.ts
customVoices
POST https://api.x.ai/v1/custom-voices
const res = await xai.v1.customVoices({ /* ... */ });Source: packages/provider/xai/src/xai.ts
documents
POST https://api.x.ai/v1/documents/search
const res = await xai.v1.documents.search({ /* ... */ });Source: packages/provider/xai/src/xai.ts
files
DELETE https://api.x.ai/v1/files/{fileId}
const res = await xai.v1.files({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET https://api.x.ai/v1/files/{fileIdOrSignal}
const res = await xai.v1.files({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/files
const res = await xai.v1.files({ /* ... */ });Source: packages/provider/xai/src/xai.ts
imageGenerationModels
GET https://api.x.ai/v1/image-generation-models/{modelIdOrSignal}
const res = await xai.v1.imageGenerationModels({ /* ... */ });Source: packages/provider/xai/src/xai.ts
images
POST https://api.x.ai/v1/images/edits
const res = await xai.v1.images.edits({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/images/generations
const res = await xai.v1.images.generations({ /* ... */ });Source: packages/provider/xai/src/xai.ts
languageModels
GET https://api.x.ai/v1/language-models/{modelIdOrSignal}
const res = await xai.v1.languageModels({ /* ... */ });Source: packages/provider/xai/src/xai.ts
models
GET https://api.x.ai/v1/models/{modelIdOrSignal}
const res = await xai.v1.models({ /* ... */ });Source: packages/provider/xai/src/xai.ts
realtime
POST https://api.x.ai/v1/realtime/client_secrets
const res = await xai.v1.realtime.clientSecrets({ /* ... */ });Source: packages/provider/xai/src/xai.ts
responses
DELETE https://api.x.ai/v1/responses/{id}
const res = await xai.v1.responses({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET https://api.x.ai/v1/responses/{id}
const res = await xai.v1.responses({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/responses
const res = await xai.v1.responses({ /* ... */ });Source: packages/provider/xai/src/xai.ts
stt
POST https://api.x.ai/v1/stt
const res = await xai.v1.stt({ /* ... */ });Source: packages/provider/xai/src/xai.ts
tokenizeText
POST https://api.x.ai/v1/tokenize-text
const res = await xai.v1.tokenizeText({ /* ... */ });Source: packages/provider/xai/src/xai.ts
tts
POST https://api.x.ai/v1/tts
const res = await xai.v1.tts({ /* ... */ });Source: packages/provider/xai/src/xai.ts
videoGenerationModels
GET https://api.x.ai/v1/video-generation-models/{modelIdOrSignal}
const res = await xai.v1.videoGenerationModels({ /* ... */ });Source: packages/provider/xai/src/xai.ts
videos
GET https://api.x.ai/v1/videos/{requestId}
const res = await xai.v1.videos({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/videos/edits
const res = await xai.v1.videos.edits({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/videos/extensions
const res = await xai.v1.videos.extensions({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST https://api.x.ai/v1/videos/generations
const res = await xai.v1.videos.generations({ /* ... */ });Source: packages/provider/xai/src/xai.ts
Middleware
import { xai as createXai, withRetry } from "@apicity/xai";
const xai = createXai({ apiKey: process.env.XAI_API_KEY! });
const models = withRetry(xai.get.v1.models, { retries: 3 });Rate Limiting
Client-side rate limiting that queues requests to stay within xAI API limits.
import {
xai as createXai,
withRateLimit,
withRetry,
createRateLimiter,
XAI_RATE_LIMITS,
} from "@apicity/xai";
const xai = createXai({ apiKey: process.env.XAI_API_KEY! });Using xAI tier presets
// Use built-in tier presets (free, tier1, tier2, tier3, tier4)
const limiter = createRateLimiter(XAI_RATE_LIMITS.tier1);
// => { rpm: 60, concurrent: 10 }
const chat = withRateLimit(xai.post.v1.chat.completions, limiter);Custom limits
const limiter = createRateLimiter({ rpm: 30, concurrent: 5 });
const chat = withRateLimit(xai.post.v1.chat.completions, limiter);Shared limiter across endpoints
RPM limits apply globally, so share a single limiter across all endpoints:
const limiter = createRateLimiter(XAI_RATE_LIMITS.tier2);
const chat = withRateLimit(xai.post.v1.chat.completions, limiter);
const responses = withRateLimit(xai.post.v1.responses, limiter);
const images = withRateLimit(xai.post.v1.images.generations, limiter);Composing with retry
Place withRateLimit innermost so retries count against the limit:
const limiter = createRateLimiter(XAI_RATE_LIMITS.tier1);
const chat = withRetry(
withRateLimit(xai.post.v1.chat.completions, limiter),
{ retries: 2 }
);Batch processing
Fire requests in parallel — the limiter handles pacing automatically:
const limiter = createRateLimiter(XAI_RATE_LIMITS.tier1);
const chat = withRateLimit(xai.post.v1.chat.completions, limiter);
const results = await Promise.all(
prompts.map((p) =>
chat({
model: "grok-3",
messages: [{ role: "user", content: p }],
})
)
);xAI rate limit tiers
| Preset | RPM | Concurrent | Spend threshold |
|--------|-----|------------|-----------------|
| free | 5 | 2 | $0 |
| tier1 | 60 | 10 | $0+ |
| tier2 | 200 | 25 | $100+ |
| tier3 | 500 | 50 | $500+ |
| tier4 | 1000 | 100 | $1,000+ |
Part of the apicity monorepo.
License
MIT — see LICENSE.
