npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@apicity/xai

v0.1.0

Published

X.AI / Grok provider for chat and search.

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 { 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: 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 https://api.x.ai/v1/batches/{paramsOrIdOrSignal}

Upstream docs ↗

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

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

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 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 https://api.x.ai/v1/batches

Upstream docs ↗

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

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

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 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 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 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 https://api.x.ai/v1/collections/{collectionId}

Upstream docs ↗

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

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

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 https://api.x.ai/v1/collections/{paramsOrIdOrSignal}

Upstream docs ↗

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

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

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 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 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 https://api.x.ai/v1/collections

Upstream docs ↗

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

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

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 https://api.x.ai/v1/collections/{collectionId}

Upstream docs ↗

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

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

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 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 https://api.x.ai/v1/files/{fileId}

Upstream docs ↗

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

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

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

Upstream docs ↗

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

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

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

Upstream docs ↗

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

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

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 https://api.x.ai/v1/images/edits

Upstream docs ↗

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

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

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 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 https://api.x.ai/v1/models/{modelIdOrSignal}

Upstream docs ↗

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

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

realtime

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 https://api.x.ai/v1/responses/{id}

Upstream docs ↗

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

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

GET https://api.x.ai/v1/responses/{id}

Upstream docs ↗

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

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

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

Upstream docs ↗

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

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

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 https://api.x.ai/v1/tokenize-text

Upstream docs ↗

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

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

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 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 https://api.x.ai/v1/videos/{requestId}

Upstream docs ↗

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

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

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 https://api.x.ai/v1/videos/extensions

Upstream docs ↗

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

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

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 { 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.