Package Exports
- @apicity/xai
- @apicity/xai/zod
Readme
@apicity/xai
X.AI / Grok provider for chat and search.
Runtime dependencies:
zod@^4.4.3— request schemas attached to every POST endpoint as.schema
Installation
npm install @apicity/xai
# or
pnpm add @apicity/xaiQuick Start
import { 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 { 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.
Imagine Files API integration
xAI Imagine image and video endpoints can reference private Files API assets directly and can persist generated assets back to Files storage.
- Inputs: anywhere Imagine accepts a public URL or base64 image/video,
pass a stored
file_idinstead. Apicity accepts the raw REST shape (image: { file_id },images: [{ file_id }],video: { file_id },reference_images: [{ file_id }]) plus convenience aliases (image_file_id,image_file_ids,video_file_id, andreference_image_file_ids) that are normalized before the HTTP request. - Outputs: pass
storage_optionswith a requiredfilenameto persist the generated image or video. Omitpublic_urlor set it tofalsefor a private file; setpublic_url: trueorpublic_url: { expires_after: 86400 }to create a shareable URL. - Responses still include the default ephemeral
imgen.x.aiorvidgen.x.aigeneration URL. When storage is requested, the persistent Files metadata is returned asfile_outputon the generated image or completed video.
Stored input field map
Use these fields when an image or video already lives in xAI Files
storage. Apicity normalizes the convenience aliases into the REST
file_id object shape before sending the request.
| Apicity call | Stored input field | Sent to xAI |
|---|---|---|
xai.post.v1.images.edits |
image_file_id |
image: { file_id } |
xai.post.v1.images.edits |
image_file_ids |
images: [{ file_id }] |
xai.post.v1.videos.generations |
image_file_id |
image: { file_id } |
xai.post.v1.videos.generations |
reference_image_file_ids |
reference_images: [{ file_id }] |
xai.post.v1.videos.generations.imageToVideo |
image_file_id |
image: { file_id } |
xai.post.v1.videos.edits |
video_file_id |
video: { file_id } |
xai.post.v1.videos.extensions |
video_file_id |
video: { file_id } |
You can also pass the raw REST fields directly. images and
reference_images entries can mix { file_id } and { url }
items in the same request, which is useful when only some references
are already private Files assets. Stored images must be PNG, JPEG,
or WebP; stored videos must be MP4; and the file upload must be
complete before it is referenced by an Imagine endpoint.
const gen = await xai.post.v1.images.generations({
prompt: "A futuristic city skyline at night",
model: "grok-imagine-image-quality",
storage_options: { filename: "city.jpg" },
});
const city = gen.data[0].file_output!.file_id!;
const edit = await xai.post.v1.images.edits({
prompt: "Add neon signs to the buildings",
model: "grok-imagine-image-quality",
image_file_id: city,
storage_options: { filename: "city-neon.jpg" },
});
const neonCity = edit.data[0].file_output!.file_id!;
const video = await xai.post.v1.videos.generations({
prompt: "A camera pulls back through the city",
model: "grok-imagine-video",
duration: 5,
image_file_id: neonCity,
storage_options: {
filename: "city-loop.mp4",
public_url: true,
},
});
const done = await xai.get.v1.videos(video.request_id);
console.log(done.video?.url);
console.log(done.video?.file_output?.public_url);See xAI's Imagine Files API integration, Referencing Files as Input, Persisting Generated Output, Managing Files, and Files Public URLs docs for uploads, expiration, and public URL lifecycle details.
Files Public URLs
Files uploaded to xAI storage are private by default. Use
xai.post.v1.files.publicUrl(fileId) to create a shareable
xAI CDN URL for an existing file, then revoke that URL independently
with xai.post.v1.files.publicUrl.revoke(fileId) when sharing
should stop. Revoking the public URL leaves the private file intact.
const file = await xai.post.v1.files(
new Blob(["diagram"], { type: "image/png" }),
"diagram.png",
"assistants"
);
const created = await xai.post.v1.files.publicUrl(file.id, {
expires_after: 86400,
});
console.log(created.public_url);
console.log(created.expires_at);
const withPublicUrls = await xai.get.v1.files({
filter: "public_url != null",
});
console.log(withPublicUrls.data[0]?.public_url);
await xai.post.v1.files.publicUrl.revoke(file.id);Public URL lifecycle
- Empty create bodies use xAI defaults. Pass
expires_afterin seconds to auto-revoke the URL after 1 hour to 30 days. - A public URL cannot outlive its file. If the file has its own
expires_at, an omitted public URL expiry inherits the file expiry; an explicitexpires_aftermust fit inside the file's remaining lifetime. - Create is idempotent while a file already has an active public URL: repeated calls return the same URL token and can update its expiry.
get.v1.files(fileId)andget.v1.files({ filter })preservepublic_urlandpublic_url_expires_atmetadata so callers can audit which files are currently public.
See xAI's
Files Public URLs,
Managing Files,
and Imagine Files API integration
docs for supported content types, size limits, and the
storage_options.public_url generation path.
API Reference
54 endpoints across 18 groups. Each method mirrors an upstream URL path.
apiKey
GET xai.v1.apiKey
GET https://api.x.ai/v1/api-key
const res = await xai.v1.apiKey({ /* ... */ });Source: packages/provider/xai/src/xai.ts
batches
GET xai.v1.batches
GET https://api.x.ai/v1/batches/{paramsOrIdOrSignal}
const res = await xai.v1.batches({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.v1.batches.requests
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 xai.v1.batches.results
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 xai.v1.batches
POST https://api.x.ai/v1/batches
const res = await xai.v1.batches({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.batches.cancel
POST https://api.x.ai/v1/batches/{batchId}:cancel
const res = await xai.v1.batches.cancel({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.batches.requests
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 xai.v1.chat.deferredCompletion
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 xai.v1.chat.completions
POST https://api.x.ai/v1/chat/completions
const res = await xai.v1.chat.completions({ /* ... */ });Source: packages/provider/xai/src/xai.ts
customVoices
DELETE xai.v1.customVoices
DELETE https://api.x.ai/v1/custom-voices/{voiceId}
const res = await xai.v1.customVoices({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.v1.customVoices
GET https://api.x.ai/v1/custom-voices/{paramsOrVoiceIdOrSignal}
const res = await xai.v1.customVoices({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.v1.customVoices.audio
GET https://api.x.ai/v1/custom-voices/{voiceId}/audio
const res = await xai.v1.customVoices.audio({ /* ... */ });Source: packages/provider/xai/src/xai.ts
PATCH xai.v1.customVoices
PATCH https://api.x.ai/v1/custom-voices/{voiceId}
const res = await xai.v1.customVoices({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.customVoices
POST https://api.x.ai/v1/custom-voices
const res = await xai.v1.customVoices({ /* ... */ });Source: packages/provider/xai/src/xai.ts
documents
POST xai.v1.documents.search
POST https://api.x.ai/v1/documents/search
const res = await xai.v1.documents.search({ /* ... */ });Source: packages/provider/xai/src/xai.ts
files
DELETE xai.v1.files
DELETE https://api.x.ai/v1/files/{fileId}
const res = await xai.v1.files({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.v1.files
GET https://api.x.ai/v1/files/{paramsOrFileIdOrSignal}
const res = await xai.v1.files({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.v1.files.content
GET https://api.x.ai/v1/files/{fileId}/content
const res = await xai.v1.files.content({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.files
POST https://api.x.ai/v1/files
const res = await xai.v1.files({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.files.publicUrl
POST https://api.x.ai/v1/files/{fileId}/public-url
const res = await xai.v1.files.publicUrl({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.files.publicUrl.revoke
POST https://api.x.ai/v1/files/{fileId}/public-url/revoke
const res = await xai.v1.files.publicUrl.revoke({ /* ... */ });Source: packages/provider/xai/src/xai.ts
imageGenerationModels
GET xai.v1.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 xai.v1.images.edits
POST https://api.x.ai/v1/images/edits
const res = await xai.v1.images.edits({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.images.generations
POST https://api.x.ai/v1/images/generations
const res = await xai.v1.images.generations({ /* ... */ });Source: packages/provider/xai/src/xai.ts
languageModels
GET xai.v1.languageModels
GET https://api.x.ai/v1/language-models/{modelIdOrSignal}
const res = await xai.v1.languageModels({ /* ... */ });Source: packages/provider/xai/src/xai.ts
managementApi
DELETE xai.managementApi.v1.collections
DELETE https://management-api.x.ai/v1/collections/{collectionId}
const res = await xai.managementApi.v1.collections({ /* ... */ });Source: packages/provider/xai/src/xai.ts
DELETE xai.managementApi.v1.collections.documents
DELETE https://management-api.x.ai/v1/collections/{collectionId}/documents/{fileId}
const res = await xai.managementApi.v1.collections.documents({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.managementApi.auth.teams.apiKeys
GET https://management-api.x.ai/auth/teams/{teamId}/api-keys{query}
const res = await xai.managementApi.auth.teams.apiKeys({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.managementApi.v1.billing.teams.postpaid.invoice.preview
GET https://management-api.x.ai/v1/billing/teams/{teamId}/postpaid/invoice/preview
const res = await xai.managementApi.v1.billing.teams.postpaid.invoice.preview({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.managementApi.v1.billing.teams.postpaid.spendingLimits
GET https://management-api.x.ai/v1/billing/teams/{teamId}/postpaid/spending-limits
const res = await xai.managementApi.v1.billing.teams.postpaid.spendingLimits({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.managementApi.v1.billing.teams.prepaid.balance
GET https://management-api.x.ai/v1/billing/teams/{teamId}/prepaid/balance
const res = await xai.managementApi.v1.billing.teams.prepaid.balance({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.managementApi.v1.collections
GET https://management-api.x.ai/v1/collections/{paramsOrIdOrSignal}
const res = await xai.managementApi.v1.collections({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.managementApi.v1.collections.documents
GET https://management-api.x.ai/v1/collections/{collectionId}/documents/{paramsOrFileId}
const res = await xai.managementApi.v1.collections.documents({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.managementApi.v1.collections.documents.batchGet
GET https://management-api.x.ai/v1/collections/{collectionId}/documents:batchGet{query}
const res = await xai.managementApi.v1.collections.documents.batchGet({ /* ... */ });Source: packages/provider/xai/src/xai.ts
PATCH xai.managementApi.v1.collections.documents
PATCH https://management-api.x.ai/v1/collections/{collectionId}/documents/{fileId}
const res = await xai.managementApi.v1.collections.documents({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.managementApi.v1.billing.teams.usage
POST https://management-api.x.ai/v1/billing/teams/{teamId}/usage
const res = await xai.managementApi.v1.billing.teams.usage({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.managementApi.v1.collections
POST https://management-api.x.ai/v1/collections
const res = await xai.managementApi.v1.collections({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.managementApi.v1.collections.documents
POST https://management-api.x.ai/v1/collections/{collectionId}/documents/{fileId}
const res = await xai.managementApi.v1.collections.documents({ /* ... */ });Source: packages/provider/xai/src/xai.ts
PUT xai.managementApi.v1.collections
PUT https://management-api.x.ai/v1/collections/{collectionId}
const res = await xai.managementApi.v1.collections({ /* ... */ });Source: packages/provider/xai/src/xai.ts
models
GET xai.v1.models
GET https://api.x.ai/v1/models/{modelIdOrSignal}
const res = await xai.v1.models({ /* ... */ });Source: packages/provider/xai/src/xai.ts
realtime
POST xai.v1.realtime.clientSecrets
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 xai.v1.responses
DELETE https://api.x.ai/v1/responses/{id}
const res = await xai.v1.responses({ /* ... */ });Source: packages/provider/xai/src/xai.ts
GET xai.v1.responses
GET https://api.x.ai/v1/responses/{id}
const res = await xai.v1.responses({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.responses
POST https://api.x.ai/v1/responses
const res = await xai.v1.responses({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.responses.compact
POST https://api.x.ai/v1/responses/compact
const res = await xai.v1.responses.compact({ /* ... */ });Source: packages/provider/xai/src/xai.ts
stt
POST xai.v1.stt
const res = await xai.v1.stt({ /* ... */ });Source: packages/provider/xai/src/xai.ts
tokenizeText
POST xai.v1.tokenizeText
POST https://api.x.ai/v1/tokenize-text
const res = await xai.v1.tokenizeText({ /* ... */ });Source: packages/provider/xai/src/xai.ts
tts
POST xai.v1.tts
const res = await xai.v1.tts({ /* ... */ });Source: packages/provider/xai/src/xai.ts
videoGenerationModels
GET xai.v1.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 xai.v1.videos
GET https://api.x.ai/v1/videos/{requestId}
const res = await xai.v1.videos({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.videos.edits
POST https://api.x.ai/v1/videos/edits
const res = await xai.v1.videos.edits({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.videos.extensions
POST https://api.x.ai/v1/videos/extensions
const res = await xai.v1.videos.extensions({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.videos.generations
POST https://api.x.ai/v1/videos/generations
const res = await xai.v1.videos.generations({ /* ... */ });Source: packages/provider/xai/src/xai.ts
POST xai.v1.videos.generations.imageToVideo
POST https://api.x.ai/v1/videos/generations
const res = await xai.v1.videos.generations.imageToVideo({ /* ... */ });Source: packages/provider/xai/src/xai.ts
Middleware
import { 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 {
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.