JSPM

  • Created
  • Published
  • Downloads 707
  • Score
    100M100P100Q115532F
  • License MIT

X.AI / Grok provider for chat and search.

Package Exports

  • @apicity/xai
  • @apicity/xai/zod

Readme

@apicity/xai

npm zero dependencies TypeScript

X.AI / Grok provider for chat and search.

Installation

npm install @apicity/xai
# or
pnpm add @apicity/xai

Quick 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: false keeps the response off xAI's history surface. Flip to true to chain follow-ups via previous_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.type before reading content; TypeScript's narrowing keeps you honest.
  • For raw chat-style usage without the Responses wrapping, use xai.post.v1.chat.completions instead — same auth, same model catalog, just OpenAI-compatible request/response shapes.
  • Errors surface as XaiError with status and the parsed body attached, so try { ... } 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 xai.v1.batches

GET https://api.x.ai/v1/batches/{paramsOrIdOrSignal}

Upstream docs ↗

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}

Upstream docs ↗

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}

Upstream docs ↗

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

Upstream docs ↗

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

Upstream docs ↗

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

Upstream docs ↗

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}

Upstream docs ↗

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

Upstream docs ↗

const res = await xai.v1.chat.completions({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

collections

DELETE xai.v1.collections

DELETE https://api.x.ai/v1/collections/{collectionId}

Upstream docs ↗

const res = await xai.v1.collections({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

DELETE xai.v1.collections.documents

DELETE https://api.x.ai/v1/collections/{collectionId}/documents/{fileId}

Upstream docs ↗

const res = await xai.v1.collections.documents({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

GET xai.v1.collections

GET https://api.x.ai/v1/collections/{paramsOrIdOrSignal}

Upstream docs ↗

const res = await xai.v1.collections({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

GET xai.v1.collections.documents

GET https://api.x.ai/v1/collections/{collectionId}/documents/{paramsOrFileId}

Upstream docs ↗

const res = await xai.v1.collections.documents({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

GET xai.v1.collections.documents.batchGet

GET https://api.x.ai/v1/collections/{collectionId}/documents:batchGet{query}

Upstream docs ↗

const res = await xai.v1.collections.documents.batchGet({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

PATCH xai.v1.collections.documents

PATCH https://api.x.ai/v1/collections/{collectionId}/documents/{fileId}

Upstream docs ↗

const res = await xai.v1.collections.documents({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

POST xai.v1.collections

POST https://api.x.ai/v1/collections

Upstream docs ↗

const res = await xai.v1.collections({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

POST xai.v1.collections.documents

POST https://api.x.ai/v1/collections/{collectionId}/documents/{fileId}

Upstream docs ↗

const res = await xai.v1.collections.documents({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

PUT xai.v1.collections

PUT https://api.x.ai/v1/collections/{collectionId}

Upstream docs ↗

const res = await xai.v1.collections({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

customVoices

POST xai.v1.customVoices

POST https://api.x.ai/v1/custom-voices

Upstream docs ↗

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

Upstream docs ↗

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}

Upstream docs ↗

const res = await xai.v1.files({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

GET xai.v1.files

GET https://api.x.ai/v1/files/{fileIdOrSignal}

Upstream docs ↗

const res = await xai.v1.files({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

POST xai.v1.files

POST https://api.x.ai/v1/files

Upstream docs ↗

const res = await xai.v1.files({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

imageGenerationModels

GET xai.v1.imageGenerationModels

GET https://api.x.ai/v1/image-generation-models/{modelIdOrSignal}

Upstream docs ↗

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

Upstream docs ↗

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

Upstream docs ↗

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}

Upstream docs ↗

const res = await xai.v1.languageModels({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

models

GET xai.v1.models

GET https://api.x.ai/v1/models/{modelIdOrSignal}

Upstream docs ↗

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

Upstream docs ↗

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}

Upstream docs ↗

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}

Upstream docs ↗

const res = await xai.v1.responses({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

POST xai.v1.responses

POST https://api.x.ai/v1/responses

Upstream docs ↗

const res = await xai.v1.responses({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

stt

POST xai.v1.stt

POST https://api.x.ai/v1/stt

Upstream docs ↗

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

Upstream docs ↗

const res = await xai.v1.tokenizeText({ /* ... */ });

Source: packages/provider/xai/src/xai.ts

tts

POST xai.v1.tts

POST https://api.x.ai/v1/tts

Upstream docs ↗

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}

Upstream docs ↗

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}

Upstream docs ↗

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

Upstream docs ↗

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

Upstream docs ↗

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

Upstream docs ↗

const res = await xai.v1.videos.generations({ /* ... */ });

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.