xai-org/xai-sdk-ts
The official TypeScript SDK for the SpaceXAI API. Contribute to xai-org/xai-sdk-ts development by creating an account on GitHub.
26 stars · 0 forks
README
SpaceXAI TypeScript SDK The official TypeScript SDK for the SpaceXAI API API keys · Issues
Use Grok from TypeScript with a typed, ESM client built on the SpaceXAI REST API. The SDK has no runtime dependencies and includes streaming, structured output, function tools, image input, image and video generation, file uploads, batch processing, text to speech and transcription, multi-turn conversations, and access to usage and HTTP metadata.
Experimental. This SDK is in early development. It currently covers the Responses API, image and video generation, the Files, Batch, and Voice APIs, tokenization, and model and account lookup, and its interfaces may change between releases before 1.0. Pin an exact version and read the changelog when upgrading. Feedback and bug reports are welcome in issues.
Requirements
Node.js 22.13 or later A SpaceXAI API key An ESM project
Installation Install the package with your preferred package manager: npm install @xai-official/sdk pnpm add @xai-official/sdk Quickstart Set your API key in the environment. The client reads XAI_API_KEY automatically. export XAI_API_KEY="your-api-key" import { SpaceXAI } from "@xai-official/sdk";
const client = new SpaceXAI();
const response = await client.responses.create({ model: "grok-4.7", input: "Explain why the sky is blue in one sentence.", });
console.log(response.toText()); Keep API keys on the server. The SDK blocks browser and Worker use by default because shipping a secret key to client-side code exposes it to users. Streaming Set stream: true to receive output as it's generated. Listen for answer text with on("text"), then await stream.done() for the final response: const stream = await client.responses.create({ model: "grok-4.7", input: "Write a short story about a curious robot.", stream: true, });
const response = await stream .on("text", (text) => process.stdout.write(text)) .done();
console.log(`\n${response.usage.total_tokens} tokens`); done() resolves to the same response object a non-streamed request returns. It rejects if the stream fails or closes before the response completes. Besides "text", on() has helper events for the rest of a response:
"reasoning" for each chunk of reasoning text or reasoning summary "tool_call" for each tool call once its arguments are complete, whether your code or SpaceXAI runs it. Check call.type to tell them apart "client_tool_call" for each call that your code runs: your function tools and shell commands "server_tool_call" for each call to a tool that SpaceXAI runs, such as web search or code execution "image" for each finished image generation call, with the base64 image in result "citation" for each URL citation in the answer
Each tool call fires once, when its arguments are complete. For your function tools, that's when to run the function, since nothing has run it yet. To show that a call has started before its arguments arrive, listen for "response.output_item.added". const response = await stream .on("reasoning", (text) => process.stderr.write(text)) .on("text", (text) => process.stdout.write(text)) .on("tool_call", (call) => console.error(`\n${call.type} ${call.status}`)) .done(); on() also takes any server-sent event type, such as "response.completed", and passes the listener the typed event. Events the SDK doesn't recognize arrive as "unknown", with the original payload in event.raw. You can also iterate over the stream to handle events in a loop. After the loop, done() resolves right away with the final response: for await (const event of stream) { if (event.type === "response.function_call_arguments.delta") { process.stdout.write(event.delta); } }
const response = await stream.done(); console.log(response.toText()); stream.http has the HTTP status, headers, and request IDs as soon as create() returns. If you stop consuming a stream early, call await stream.close() to cancel its response body. Multi-turn conversations Responses are not stored by default. Use toInput() to carry the model output, including encrypted reasoning content, into the next turn. Reuse one prompt_cache_key across the conversation to improve prompt cache routing: import { randomUUID } from "node:crypto"; import { type InputItem, SpaceXAI } from "@xai-official/sdk";
const client = new SpaceXAI(); const promptCacheKey = `conversation:${randomUUID()}`; const input: Array<InputItem> = [ { role: "user", content: "My name is Ada. Remember it." }, ];
const first = await client.responses.create({ model: "grok-4.7", input, prompt_cache_key: promptCacheKey, });
const second = await client.responses.create({ model: "grok-4.7", input: [ ...input, ...first.toInput(), { role: "user", content: "What is my name?" }, ], prompt_cache_key: promptCacheKey, });
console.log(second.toText()); Use a different cache key for each unrelated conversation. To continue a stored response by ID, opt in to storage: const first = await client.responses.create({ model: "grok-4.7", input: "My name is Ada. Remember it.", store: true, });
const second = await client.responses.create({ model: "grok-4.7", input: "What is my name?", previous_response_id: first.id, store: true, }); Compacting long conversations Every turn resends the whole conversation, so input tokens grow as it gets longer. Compact the conversation into a single encrypted item with responses.compact(), then start the next input with the compacted output. Continuing the toInput() example: const compacted = await client.responses.compact({ model: "grok-4.7", input: [ ...input, ...first.toInput(), { role: "user", content: "What is my name?" }, ...second.toInput(), ], });
const third = await client.responses.create({ model: "grok-4.7", input: [ ...compacted.output, { role: "user", content: "Spell my name backwards." }, ], prompt_cache_key: promptCacheKey, });
console.log(third.toText()); Pass compacted.output unchanged and add new turns after it. The conversation must still fit in the model's context window when you compact it. compacted.usage reports the tokens the compaction used and dropped_message_count, the number of messages it replaced. Image input Pass an image URL alongside text: const response = await client.responses.create({ model: "grok-4.7", input: [ { role: "user", content: [ { type: "input_text", text: "Describe this image." }, { type: "input_image", image_url: "https://example.com/image.jpg", detail: "high", }, ], }, ], });
console.log(response.toText()); For a local image, pass a Blob or File as image. The SDK converts it to a data URL before sending the request, and detects JPEG, PNG, or WebP from the bytes when the Blob has no MIME type. import { readFile } from "node:fs/promises";
const bytes = new Uint8Array(await readFile("./image.png")); const image = new Blob([bytes], { type: "image/png" });
const response = await client.responses.create({ model: "grok-4.7", input: [ { role: "user", content: [ { type: "input_text", text: "What is in this image?" }, { type: "input_image", image }, ], }, ], }); Structured output Provide a JSON Schema through text.format. Call toJson() to parse the completed text output. const response = await client.responses.create({ model: "grok-4.7", input: "Give me a city to visit in Japan.", text: { format: { type: "json_schema", name: "travel_suggestion", schema: { type: "object", properties: { city: { type: "string" }, reason: { type: "string" }, }, required: ["city", "reason"], additionalProperties: false, }, }, }, });
const suggestion = response.toJson(); console.log(suggestion); toJson() returns unknown. Validate the result before using it at a trust boundary. The non-throwing response.parsed getter returns null when the output is incomplete or is not valid JSON. Tools Describe functions with JSON Schema, run the requested function in your application, then return its output to the model: import { type InputItem, type Tool, isFunctionCall, SpaceXAI } from "@xai-official/sdk";
const client = new SpaceXAI(); const prompt = "What is the weather in San Francisco?"; const input: Array<InputItem> = [{ role: "user", content: prompt }]; const getWeatherTool: Tool = { type: "function", name: "get_weather", description: "Get the current weather for a location.", parameters: { type: "object", properties: { location: { type: "string" }, }, required: ["location"], additionalProperties: false, }, };
async function getWeather(args: unknown) { if ( typeof args !== "object" || args === null || !("location" in args) || typeof args.location !== "string" ) { throw new Error("get_weather expects a location string"); } return { location: args.location, temperatureC: 18, conditions: "sunny" }; }
const response = await client.responses.create({ model: "grok-4.7", input, parallel_tool_calls: false, tools: [getWeatherTool], });
const call = response.output.find(isFunctionCall); if (!call) throw new Error("The model did not call get_weather"); const result = await getWeather(JSON.parse(call.arguments));
const answer = await client.responses.create({ model: "grok-4.7", input: [ ...input, ...response.toInput(), { type: "function_call_output", call_id: call.call_id, output: JSON.stringify(result) }, ], });
console.log(answer.toText()); parallel_tool_calls: false limits the model to one function call per turn, so this example only has to handle one. By default the model can ask for several at once, which the tool call loop handles. Treat function names and arguments as untrusted input. Only dispatch functions you have explicitly allowed, and validate arguments before executing them, as getWeather() does. Tool call loop To let the model call tools until it has an answer, run a loop. Stream a turn, run each function call as soon as it finishes streaming, then send the outputs back along with the model's output. Stop when a turn makes no function calls, and cap the number of turns so a model that keeps calling tools can't loop forever. This reuses getWeatherTool and getWeather from the example above: import { type FunctionToolCall, type InputItem, type Tool, SpaceXAI } from "@xai-official/sdk";
const client = new SpaceXAI(); const tools: Array<Tool> = [getWeatherTool]; const handlers: Record<string, (args: unknown) => Promise<unknown>> = { get_weather: getWeather, };
async function runTool(call: FunctionToolCall): Promise<InputItem> { let output: unknown; try { const handler = handlers[call.name]; if (!handler) throw new Error(`Unknown tool: ${call.name}`); output = await handler(JSON.parse(call.arguments)); } catch (err) { output = { error: err instanceof Error ? err.message : String(err) }; } return { type: "function_call_output", call_id: call.call_id, output: JSON.stringify(output), }; }
const input: Array<InputItem> = [ { role: "user", content: "Compare the weather in Paris and Tokyo." }, ];
for (let turn = 0; turn < 10; turn++) { const toolRuns: Array<Promise<InputItem>> = []; const stream = await client.responses.create({ model: "grok-4.7", input, tools, stream: true, }); const response = await stream .on("text", (text) => process.stdout.write(text)) .on("client_tool_call", (call) => { if (call.type === "function_call") toolRuns.push(runTool(call)); }) .done(); if (toolRuns.length === 0) break;
const toolOutputs = await Promise.all(toolRuns); input.push(...response.toInput(), ...toolOutputs); } The handlers map is the list of functions the model may call. runTool() returns errors to the model instead of throwing, so the model can recover, and a failing tool can't crash the loop while the stream is still running. Shell commands With the shell tool, the model writes shell commands and your application runs them. Use it for agents that work on your machine, such as exploring a repository, running tests, or checking disk space. Each call arrives through the client_tool_call listener as a shell_call, with the commands in action.commands. The model writes these commands, so run them in a sandbox or container, or check each one before running it: import { exec } from "node:child_process"; import { SpaceXAI } from "@xai-official/sdk";
const client = new SpaceXAI();
const stream = await client.responses.create({ model: "grok-4.7", input: "How much free disk space does this machine have?", tools: [{ type: "shell", environment: { type: "local" } }], stream: true, });
await stream .on("client_tool_call", (call) => { if (call.type === "shell_call") { for (const command of call.action.commands) { exec(command, (error, stdout, stderr) => console.log(stdout || stderr)); } } }) .done(); To let the model use the results, send them back in the next request, like the function outputs in the tool call loop: { type: "shell_call_output", call_id: call.call_id, output: [{ stdout, stderr, outcome: { type: "exit", exit_code: 0 } }] }. To give the model skills, list them in the tool's environment. A skill is a directory with a SKILL.md file of instructions. The model sees each skill's name and description, and when a task matches one, it reads the SKILL.md through your shell tool and follows it. If you only allow certain commands, allow reading the skill's directory: const shell: Tool = { type: "shell", environment: { type: "local", skills: [ { name: "release-notes", description: "Write release notes from this repo's git log in our house style.", path: "./skills/release-notes", }, ], }, }; Asked to write release notes, the model reads ./skills/release-notes/SKILL.md, runs the git log command it describes, and writes the notes in the format it specifies. Built-in tools Besides your own functions, the API has built-in tools. Create them with the helpers from @xai-official/sdk/tools, which check each tool's options as you type. SpaceXAI runs these tools and includes their results in the response:
webSearch() (web_search) searches the web. Options include allowed_domains, excluded_domains, user_location, and search_context_size. xSearch() (x_search) searches posts on X. Options include allowed_x_handles, excluded_x_handles, from_date, and to_date. codeExecution() (code_interpreter) writes and runs Python code to answer the prompt. collectionsSearch() (file_search) searches the collections listed in vector_store_ids. imageGeneration() (image_generation) creates or edits images. mcp() (mcp) calls tools on the remote MCP server at server_url, identified by server_label. toolSearch() (tool_search) loads the definitions of tools marked defer_loading: true when the model needs them, instead of putting every definition in the prompt.
Two kinds of tools run in your application instead: function for your own functions, as shown in Tools, and shell, where the model writes shell commands for your application to run, as shown in Shell commands. When you stream, calls to both arrive through the client_tool_call listener. The helpers return plain tool objects, so you can also write { type: "web_search" } yourself. Tool autocompletes the known types and accepts any other type, such as a tool released after this SDK version, but it doesn't check options the way the helpers do. See the SpaceXAI documentation for each tool's options. Web search Add the web search tool when a prompt needs current information: import { SpaceXAI } from "@xai-official/sdk"; import { webSearch } from "@xai-official/sdk/tools";
const client = new SpaceXAI();
const response = await client.responses.create({ model: "grok-4.7", input: "What are the latest developments in commercial spaceflight?", tools: [webSearch()], });
console.log(response.toText()); console.log(response.usage.num_server_side_tools_used); X search Search posts on X, optionally limited to certain accounts and dates: import { xSearch } from "@xai-official/sdk/tools";
const response = await client.responses.create({ model: "grok-4.7", input: "What has SpaceXAI announced on X this month?", tools: [xSearch({ allowed_x_handles: ["xai"], from_date: "2026-09-01" })], }); allowed_x_handles and excluded_x_handles each take up to 20 handles and can't be used together. Set enable_image_understanding or enable_video_understanding to let the model look at media in posts. When you stream, each finished search reaches the server_tool_call listener as a custom_tool_call. Its name is the search that ran, such as x_keyword_search, and input holds the search arguments as a JSON string: await stream .on("server_tool_call", (call) => { if (call.type === "custom_tool_call") console.log(call.name, call.input); }) .done(); Code execution Let the model write and run Python for calculations and data analysis: import { codeExecution } from "@xai-official/sdk/tools";
const response = await client.responses.create({ model: "grok-4.7", input: "What is the standard deviation of 12, 15, 19, 22, and 31?", tools: [codeExecution()], }); The code runs in a sandbox with common libraries installed. The tool takes no options. Collections search Search documents you've added to collections: import { collectionsSearch } from "@xai-official/sdk/tools";
const response = await client.responses.create({ model: "grok-4.7", input: "What does our refund policy say about digital purchases?", tools: [collectionsSearch({ vector_store_ids: ["your-collection-id"], max_num_results: 10 })], }); Remote MCP servers Give the model the tools of a remote MCP server. SpaceXAI connects to the server and calls its tools during the response: import { mcp } from "@xai-official/sdk/tools";
const response = await client.responses.create({ model: "grok-4.7", input: "What is the modelcontextprotocol/typescript-sdk repository for?", tools: [mcp({ server_url: "https://mcp.deepwiki.com/mcp", server_label: "deepwiki" })], }); The server must use the Streaming HTTP or SSE transport. Limit the model to some of the server's tools with allowed_tools, and pass credentials with authorization or headers. require_approval and connector_id aren't supported yet. For a server with many tools, set defer_loading: true on it and add toolSearch(), so the model loads only the tool definitions it needs. Image generation tool Add the image generation tool to let the model create or edit images as one step of a response. Each image arrives as an image_generation_call output item whose result holds base64 image data: import { writeFile } from "node:fs/promises"; import { isImageGenerationCall, SpaceXAI } from "@xai-official/sdk"; import { imageGeneration } from "@xai-official/sdk/tools";
const client = new SpaceXAI();
const response = await client.responses.create({ model: "grok-4.7", input: "Generate an image of a corgi surfing a big wave, in the style of a Japanese woodblock print.", tools: [imageGeneration()], });
console.log(response.toText());
const call = response.output.find(isImageGenerationCall); if (call?.result) { await writeFile("corgi.jpg", Buffer.from(call.result, "base64")); } Pass action: "generate" or action: "edit" to imageGeneration() to allow only one of those capabilities. When streaming, each call emits response.image_generation_call.in_progress, response.image_generation_call.generating, and response.image_generation_call.completed events, then a response.output_item.done event carries the finished item. To generate or edit an image directly with full control over its size and format, use the image generation and image editing methods instead. Working with responses Every completed re