xAIkit
0.4.0. Not an official xAI package. The API may still change. Problems or feedback: open an issue.
Unofficial Python kit for the xAI (Grok) API — one typed client, living model catalog, usage metering, media, and realtime voice. Not a multi-provider SDK.
Requires Python 3.10+, the same floor as the official xAI Python SDK.
Built for apps that want a typed Grok/xAI client with purpose-tagged usage events, catalog resolve (cheapest / economy / best), inject credential stores, and a mock provider for CI — without pulling in a multi-provider marketplace.
What it does
A Python library for the Grok / xAI API — one typed client so your app can chat, generate images and video, speak, search collections, and meter what each feature costs. Not a chatbot UI. Not LiteLLM, OpenRouter, or another multi-provider gateway. Just xAI, as a kit you drop into an existing codebase.
| You want to… | xAIkit gives you |
|---|---|
| Call Grok from Python (chat, stream, tools, vision, structured JSON) | XaiClient and AsyncXaiClient — same method names; JSON dicts, not protobuf |
| Generate or edit images, make video, or do speech on xAI | Imagine generate/edit, video + extend, REST + streaming STT/TTS, realtime speech-to-speech |
| Pick a model without hardcoding IDs that churn | resolve_model("cheapest" | "economy" | "best") per role (chat / image / video / voice); pin= still wins |
| See what a feature costs (tokens, estimated USD, OpenTelemetry) | Purpose-tagged UsageMeter — purpose= is required when a meter is attached |
| Test without an API key or live spend | MockChatProvider + inject_catalog — CI stays offline |
| Keep keys out of the browser for realtime voice | Server-side create_realtime_client_secret (and the sec-websocket-protocol helper) |
| Bring your own credentials / IdP | api_key= or a CredentialStore; OAuth URLs are caller-supplied, never hardcoded |
| Use the rest of the xAI surface | Files, embeddings, tokenize, batch, collections, Responses, priority and deferred chat |
Not an official xAI package. Domain schemas and the tool loop stay in your app.
How this differs from the official SDK
The official xAI Python SDK (xai_sdk) is the gRPC client xAI ships. xAIkit uses it for live chat and adds a kit around it:
| Official SDK | xAIkit | |
|---|---|---|
| Status | Official | Unofficial kit (this repo / xaikit-py) |
| Chat live path | xai_sdk.Client / protobuf |
Same SDK under XaiClient; you see JSON dicts, not protobuf |
| Tests without a key | You mock gRPC | MockChatProvider + inject_catalog |
| Model pick | You hardcode ids | resolve_model("cheapest" | "economy" | "best") per role |
| Cost | None | Purpose-tagged UsageMeter + a copied public price table (estimates, not invoices) |
| REST / WS extras | Separate HTTP/WS samples | Image, video, STT/TTS, realtime voice, Files, batch, collections on the same client |
| Auth | API key | API key, CredentialStore, or caller-supplied OAuth URLs — no User types, no grok.com login |
Install stays xaikit-py; import xaikit. You can use both packages in one app.
Install
The import stays xaikit (the PyPI name is xaikit-py because xaikit was too close to an existing explainable-AI project).
uv add xaikit-py
# or: pip install xaikit-py
# From a git tag
uv add "xaikit-py @ git+https://github.com/BrianCLowe/xAIkit@v0.4.0"
# Editable neighbor checkout
uv add --editable ../xAIkit
Problems and feedback
If something breaks, the docs are wrong, or an API is missing, open an issue. Include the package version (python -c "import xaikit; print(xaikit.__version__)"), how you installed (PyPI / git / editable), and a short repro. Redact API keys.
Quick usage
from xaikit import (
InMemoryUsageSink,
MockChatProvider,
UsageMeter,
XaiClient,
)
meter = UsageMeter(sink=InMemoryUsageSink())
client = XaiClient(
provider=MockChatProvider(replies="hi"),
model="grok-4.7",
usage_meter=meter,
)
resp = client.chat([{"role": "user", "content": "hello"}], purpose="demo.chat")
Async twin (AsyncXaiClient) uses the same method names; REST uses httpx.AsyncClient and live chat uses xai_sdk.AsyncClient. MockChatProvider works with both clients:
import asyncio
from xaikit import AsyncXaiClient, MockChatProvider
async def main() -> None:
client = AsyncXaiClient(provider=MockChatProvider(replies="hi"), model="grok-4.7")
resp = await client.chat([{"role": "user", "content": "hello"}])
print(resp.content)
asyncio.run(main())
When usage_meter is attached, purpose is required. Without a meter, purpose is optional.
Pricing estimates (not invoices)
UsageMeter can attach a PriceTable. The bootstrap table is a manual copy of xAI’s public list prices — not a live billing feed and not an invoice.
- Source: docs.x.ai/developers/pricing (chat also docs.x.ai/docs/models)
- Last copied into the kit:
default_price_table().fetched(alsoPRICE_TABLE_FETCHED) - Refresh: the meter prices a call from the response’s
cost_in_usd_tickswhen present, then rates on the in-process model list, then the shipped table. It fetches a public gap file this repo publishes only when both leave the price unset, at most once per process per 24 hours. A fetch error uses the shipped table and does not fail the call. The shipped table bundles long and cached rates for every model the gap file lists with those rates, so those tiers price offline. When a list row lacks one of those rates, the meter copies the missing field from the shipped table, looked up by the resolved catalog row id and then the request model string, and only when the call needs it. A prompt of at least 200,000 tokens needs the long rates. A cached prompt count needs the cached rate. Both together also need the long cached rate. A value already on the list row wins. The gap file is only a refresh for an id or rate that neither the list nor the shipped table can price. The shipped table inpricing.pymatches an exact id, or a prefix whose remainder is empty or starts with-(grok-4.7-latestfollowsgrok-4.7;grok-4.8does not followgrok-4). When usage includes a cached prompt count (cached_tokens,cached_prompt_tokens, orcached_prompt_text_tokens) and the price row has a cached input rate, that slice is priced at the cached rate and the rest ofprompt_tokensat the normal input rate. Unknown ids leaveestimated_usdunset. There is no fetch on import. Operators can still overlay JSON withload_price_table("prices.json"). - No public rate → no invented USD. Embeddings, tokenizer, batch, collections, Responses, Files, REST TTS, and similar still record purpose/tokens/success;
estimated_usdstays unset.
from xaikit import PRICE_TABLE_FETCHED, PRICE_TABLE_SOURCE_URL, default_price_table
table = default_price_table()
assert table.source_url == PRICE_TABLE_SOURCE_URL
assert table.fetched == PRICE_TABLE_FETCHED
# table.price_for("grok-4.7").input_per_million
Optional OpenTelemetry export (pip install 'xaikit-py[otel]'): OpenTelemetryUsageSink increments xaikit.usage.calls / xaikit.usage.tokens (attributes: purpose, model, modality, success). It is export-only — pair with InMemoryUsageSink via CompositeUsageSink to inspect events.
Credentials and OAuth
Pass api_key= or a CredentialStore (EnvCredentialStore / DictCredentialStore). The kit does not hardcode xAI portal URLs. OAuth helpers take caller-supplied authorize_url and token_url. An authorize_url that already has a query keeps those parameters, with kit values winning on client_id, redirect_uri, response_type, scope, and state, and any fragment stays after the query.
from xaikit import (
build_oauth_authorize_url,
exchange_oauth_code,
oauth_is_configured,
)
# Your app supplies the IdP endpoints — not the kit.
authorize = "https://idp.example.com/authorize"
token = "https://idp.example.com/token"
assert oauth_is_configured(client_id="app-id", client_secret="app-secret")
url = build_oauth_authorize_url(
client_id="app-id",
redirect_uri="https://app.example.com/callback",
state="nonce-1",
authorize_url=authorize,
)
# tokens = exchange_oauth_code(
# code,
# client_id="app-id",
# client_secret="app-secret",
# redirect_uri="https://app.example.com/callback",
# token_url=token,
# )
This is not grok.com / SuperGrok login. A Grok subscription’s weekly usage pool (Settings → Usage in the Grok app) has no public API. The kit does not scrape it, does not show “how much you have left,” and does not mint User/Session objects. UsageMeter only records calls your app made through XaiClient. Team API prepaid remaining lives in the xAI Console Usage Explorer (a management-key billing API, not OAuth).
Custom base URL
XaiClient and AsyncXaiClient take base_url. When that argument is omitted or blank, the client reads XAI_BASE_URL. When the variable is also omitted or blank, the base is https://api.x.ai/v1. Precedence is the constructor argument, then XAI_BASE_URL, then the default. The choice is fixed when the client is constructed.
REST calls use that base. The speech-to-text, text-to-speech, and realtime websockets use it too: https becomes wss and http becomes ws, with the same host, port, and path. One trailing slash is removed. The path is used as written, so a base without /v1 does not gain one. Any host is accepted. A scheme other than http or https, a missing host, a query, a fragment, or a username and password raises ValueError.
Chat whose base path is empty or /v1 stays on gRPC at that host and port (http uses an insecure channel). Any other path sends chat and chat_stream as REST POST <base>/chat/completions (chat_stream sets stream to true). An injected provider is used instead of that REST call. A non-default base loads the model list with GET <base>/models. Collections and batch stay on the SDK default hosts, including when chat uses a custom gRPC host. OAuth connect and the usage meter price fetch do not follow base_url.
import os
from xaikit import XaiClient
client = XaiClient(
api_key=os.environ["XAI_API_KEY"],
base_url="https://proxy.example:8443/llm/v1",
)
Catalog resolve
Callers pass cheapest / economy / best (and optional role=). Chat is the default pool. Pin still wins when pin= is set.
from xaikit import ModelInfo, feature_options, inject_catalog, resolve_model, resolve_model_selection
inject_catalog(
[
ModelInfo(id="grok-4.7", capabilities=["chat"], input_per_million=20.0, created=2),
ModelInfo(id="grok-imagine-image", capabilities=["image"], created=1),
ModelInfo(id="grok-imagine-image-quality", capabilities=["image"], created=2),
]
)
chat_id = resolve_model(intent="economy") # role="chat" default
image = resolve_model_selection(intent="best", role="image")
video_id = resolve_model(intent="cheapest", role="video")
voice_id = resolve_model(intent="economy", role="voice")
extend_id = resolve_model(intent="best", role="video", need="video_extend")
role is chat | image | video | voice. Offline tests inject fixtures with inject_catalog — do not hit the network.
feature_options(model=) lists extra capabilities for settings UIs (not role tags). No model → Grok 4.7 chat extras, the same set as Grok 4.6+ (web_search, x_search, code_execution, file_attachments, collections_search, image_understanding, x_video_understanding, mcp). Imagine quality (grok-imagine-video) reports video_extend / video_edit / r2v; grok-imagine-video-1.5 reports 1080p / r2v and not extend; grok-imagine-video-1.5-lite reports 1080p only. Unknown or older SKUs return []. Pass the same ids as need= on resolve so best is best for that job (quality over 1.5 when the job is extend). Video best stays on grok-imagine-video-1.5 even when a dated lite id is newer; cheapest can pick the lite SKU when it is on the list.
When model is omitted, an unpinned client resolves chat from the live list (the API key is passed into list_models) and re-resolves on later calls when that cache expires (one hour). BOOTSTRAP_MODEL (grok-4.7) is only the offline fallback. A pin on the client or on the call stays fixed. Image defaults follow the image list the same way. Video and voice stay on their role default until the SDK lists them. Offline with no API key or fixture, list_models injects grok-4.7 plus cheaper-band grok-4.3. Pass persist_path= to write a JSON snapshot after a live SDK fetch and reload it later; there is no default disk path. Newest chat ids compare integer major then minor (grok-5 above grok-4.10 above grok-4.7). grok-4.20 stays older than grok-4.3.
Image generation and edit
REST Imagine images on XaiClient (mocked HTTP in tests; live calls need XAI_API_KEY). edit_image posts JSON to /v1/images/edits (not OpenAI multipart). One source is a public URL, data URI, or a file_id from upload_file. Pass images= (2–3 entries, mixable kinds) for multi-image edit; the prompt may refer to <IMAGE_0>, <IMAGE_1>, <IMAGE_2>.
from xaikit import MockChatProvider, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
out = client.generate_image(
"A red cube on a table",
aspect_ratio="1:1",
resolution="2k",
response_format="b64_json",
)
# quality= ("low" | "medium") is grok-imagine-image-2.0 only
edited = client.edit_image(
"Make it a pencil sketch",
image_url=out["url"], # or image_file_id="file-..."
)
# edited["url"] / edited["b64_json"] / edited["file_id"]
# collage = client.edit_image(
# "Put <IMAGE_0> in the style of <IMAGE_1>",
# images=["https://example.com/a.png", {"file_id": "file-style"}],
# )
Default model is grok-imagine-image-quality. Optional generate knobs: aspect_ratio (Imagine list, including auto / 19.5:9 / 20:9), resolution (1k | 2k), response_format (b64_json), and quality (low | medium) on grok-imagine-image-2.0 only. Unknown aspect/resolution values are omitted. When Imagine returns file_output.file_id, both methods surface it as file_id.
Files
REST Files on XaiClient (mocked HTTP in tests). upload_file posts multipart to /v1/files and returns {id, filename, bytes, created_at, …}. Kit purpose= is the usage-meter tag; file_purpose= (default "assistants") is the upstream multipart field. Optional get_file / delete_file hit /v1/files/{file_id}.
from xaikit import MockChatProvider, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
meta = client.upload_file(b"hello", "note.txt", content_type="text/plain")
# meta["id"] is the opaque file_id
# client.get_file(meta["id"])
# client.delete_file(meta["id"])
Uploads larger than 50 MB are rejected before HTTP.
Embeddings
REST embeddings on XaiClient (mocked HTTP in tests). embed posts JSON to /v1/embeddings and returns {object, model, data, usage} where data is [{index, embedding}, …]. model= is required — the kit does not invent a default. List live ids with GET /v1/embedding-models (OpenAPI’s v1 is an example and may 404; some teams have an empty roster). Collections index models are not this endpoint. Empty input is rejected before HTTP.
from xaikit import MockChatProvider, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
out = client.embed(["query: hello", "passage: world"], model="your-embed-sku")
vectors = [row["embedding"] for row in out["data"]]
When a usage meter is attached, purpose= is required. Events use modality="embed". The public pricing table has no embeddings rate, so the meter records tokens without inventing USD.
Tokenizer
REST tokenize on XaiClient (mocked HTTP in tests). tokenize posts JSON to /v1/tokenize-text and returns {tokens, count, model} where tokens is [{token_id, string, token_bytes}, …] (plain dicts, not protobuf). model= defaults to the client's chat model. Empty text is rejected before HTTP.
from xaikit import MockChatProvider, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
out = client.tokenize("Hello world")
n = out["count"]
pieces = [row["string"] for row in out["tokens"]]
When a usage meter is attached, purpose= is required. Events use modality="tokenize". The public pricing table has no tokenizer rate, so the meter records the token count without inventing USD.
Batch
SDK batch on XaiClient (mocked helper in tests — never hits gRPC). create_batch / add_batch_requests submit a job; get_batch polls status; list_batch_results reads completions as JSON dicts (no protobuf). Requests are chat-shaped dicts (model, messages, knobs). Live Batch rejects grok-4.6 and grok-4.5. grok-4.7 is documented as Batch API not supported. Omitted model and those SKUs remap to grok-4.3 (need=batch). Unknown pins stay. Empty name / batch id / requests are rejected before the RPC.
from xaikit import MockChatProvider, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
job = client.create_batch("nightly-capitals")
client.add_batch_requests(
job["id"],
[
{
"messages": [{"role": "user", "content": "Capital of France?"}],
"batch_request_id": "fr",
}
],
)
status = client.get_batch(job["id"])
# status["state"]["num_pending"] / num_success / …
# results = client.list_batch_results(job["id"])
When a usage meter is attached, purpose= is required. Events use modality="batch". The public pricing table has no batch rate, so the meter records purpose/success without inventing USD.
Collections
SDK collections on XaiClient (mocked helper in tests — never hits gRPC). create_collection / upload_document / search_collections cover the upload-and-query path; get_collection / list_collections / delete_collection are included. Returns JSON dicts (no protobuf). Empty name / collection id / query / file bytes are rejected before the RPC.
Live create / get / list / delete / upload use xAI's management API. Set XAI_MANAGEMENT_KEY in the environment (the SDK reads it). Search uses the regular API key. This client does not take a second key argument. A collection id can 404 on search until it is visible/indexed on the inference side — the kit does not wait or retry.
from xaikit import MockChatProvider, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"]) # plus XAI_MANAGEMENT_KEY in env
coll = client.create_collection("docs")
# coll["id"] / coll["name"]
listed = client.list_collections()
# listed["collections"]
got = client.get_collection(coll["id"])
client.upload_document(coll["id"], "note.txt", b"hello world")
hits = client.search_collections("hello", coll["id"])
# hits["matches"][0]["chunk_content"] / file_id / score
# client.delete_collection(coll["id"])
When a usage meter is attached, purpose= is required. Events use modality="collections". The public pricing table has no collections rate, so the meter records purpose/success without inventing USD.
Video generation
REST Imagine video on XaiClient (mocked HTTP in tests; live calls need XAI_API_KEY). into= is required — a VideoInbox, list, or callback the app keeps. The kit delivers request_id as soon as xAI accepts the job, then the terminal result. Do not rely on the return value alone: a sibling failure can cancel the await (asyncio.gather / TaskGroup) without voiding the receipt. inbox.cancel(request_id) is the only way to stop listening. Default wait=True polls until the clip is ready; wait=False returns request_id for poll_video.
from xaikit import MockChatProvider, VideoInbox, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
inbox = VideoInbox()
started = client.generate_video(
"A red cube rotating on a table",
duration=8,
aspect_ratio="16:9",
resolution="480p",
into=inbox,
wait=False,
)
status = client.poll_video(started["request_id"])
# bytes = client.download_video(status["url"]) # when status == "done"
# status["error"] is set when status is failed / expired (same text wait raises)
# inbox.receipts still has the ticket if a parallel await is cancelled
extend_video(prompt, video_url=..., into=inbox) continues a clip. Generate defaults to grok-imagine-video-1.5; extend remaps 1.5 and grok-imagine-video-1.5-lite to grok-imagine-video (neither can extend). 1080p is kept on 1.5 and 1.5 Lite for text-to-video and image-to-video; reference-to-video and older grok-imagine-video send 720p instead. Lite list rates are $0.02 / $0.03 / $0.14 per second at 480p / 720p / 1080p.
Realtime voice
Speech-to-speech over the documented xAI realtime WebSocket (wss://api.x.ai/v1/realtime). No mic, recorder, or playground in this library — apps own capture/playback. Offline tests mock the socket.
from xaikit import MockChatProvider, XaiClient, decode_realtime_audio
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
with client.open_realtime_session(
voice="eve",
instructions="You are a helpful assistant.",
) as session:
session.send_text("Hello!")
for _ in range(8):
event = session.recv(timeout=30)
pcm = decode_realtime_audio(event) if isinstance(event, dict) else None
if pcm:
break # app owns playback; this library has no speaker
# session.send_audio(pcm16_bytes) # app owns capture; this library has no mic
Default model is grok-voice-latest. Constructor voice_model= overrides like video_model=. Custom voice_id strings on voice= work the same as built-in names like eve. REST STT/TTS stay on transcribe / synthesize_speech. Streaming STT is open_stt_session; streaming TTS is open_tts_session (not speech-to-speech).
Mint a short-lived token on the server so the long-lived API key never reaches the browser. Pass value to the client (Authorization: Bearer <token>, or realtime_client_secret_protocol(token) for sec-websocket-protocol).
from xaikit import MockChatProvider, XaiClient, realtime_client_secret_protocol
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"]) # server-side only
secret = client.create_realtime_client_secret(expires_after=300)
token = secret["value"]
# Client WS: Authorization: Bearer {token}
protocol = realtime_client_secret_protocol(token) # "xai-client-secret.{token}"
Streaming speech-to-text
Unary-transcribe over wss://api.x.ai/v1/stt. Send raw PCM bytes (not base64). This is not the realtime voice (STS) socket.
from xaikit import MockChatProvider, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
pcm16_bytes = bytes(3200) # 100 ms of 16 kHz s16le PCM — apps own capture
with client.open_stt_session(language="en", interim_results=True) as session:
session.send_audio(pcm16_bytes)
session.audio_done()
for event in session.events():
if event.get("type") == "transcript.partial":
print(event.get("text"))
elif event.get("type") == "transcript.done":
break
REST file transcription stays on transcribe. Offline tests mock the socket.
Streaming text-to-speech
Bidirectional TTS over wss://api.x.ai/v1/tts. Send text deltas; receive base64 audio.delta chunks. This is not the realtime voice (STS) socket.
from xaikit import MockChatProvider, XaiClient, decode_tts_audio
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
with client.open_tts_session(language="en", voice="eve", codec="mp3") as session:
session.send_text("Hello from streaming TTS.")
session.text_done()
for event in session.events():
chunk = decode_tts_audio(event)
if chunk:
pass # apps own playback
elif event.get("type") == "audio.done":
break
REST unary synthesis stays on synthesize_speech. Optional knobs match the streaming set (codec, sample_rate, bit_rate, speed, optimize_streaming_latency, text_normalization, with_timestamps, replace) and nest codec / sample_rate / bit_rate as output_format on the wire. Text over 15,000 characters is rejected before HTTP. with_timestamps=True returns a JSON envelope (application/json) instead of raw audio bytes.
audio, content_type = client.synthesize_speech(
"Hello from REST TTS.",
voice_id="eve",
codec="wav",
sample_rate=24000,
speed=1.0,
)
Offline tests mock the socket.
List built-in TTS voices (not team-scoped custom clones):
voices = client.list_tts_voices()
# voices[0]["voice_id"] / ["name"] / ["language"]
# client.get_tts_voice("eve")
Streaming
for chunk in client.chat_stream(
[{"role": "user", "content": "hello"}],
purpose="demo.stream",
):
print(chunk.delta, end="", flush=True)
Tools, vision, and structured JSON
The kit wraps xAI chat extras as JSON dicts. It does not run tools — the app owns the loop.
from xaikit import MockChatProvider, XaiClient
weather_tool = {
"name": "get_weather",
"description": "Get the weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}
client = XaiClient(
provider=MockChatProvider(
replies=[
"a cube",
{
"tool_calls": [
{"id": "call_1", "name": "get_weather", "arguments": {"city": "NYC"}},
],
},
{"title": "blue"},
]
),
model="grok-4.7",
)
# Vision: content may be a string or a list of parts
client.chat(
[
{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "url": "https://example.com/cube.png"},
],
}
]
)
# Tools: defs in, tool_calls out (arguments are parsed JSON, typically a dict)
resp = client.chat(
[{"role": "user", "content": "Weather in NYC?"}],
tools=[weather_tool],
tool_choice="auto",
)
# resp.tool_calls → [{"id": "call_1", "name": "get_weather", "arguments": {"city": "NYC"}}]
# App runs the function, then sends the assistant turn + tool result:
# client.chat([
# {"role": "user", "content": "Weather in NYC?"},
# {"role": "assistant", "content": "", "tool_calls": resp.tool_calls},
# {"role": "tool", "content": "72F", "tool_call_id": resp.tool_calls[0]["id"]},
# ], tools=[weather_tool])
# Native structured outputs (fence-stripping remains the fallback)
schema = {
"type": "object",
"properties": {"title": {"type": "string"}},
"required": ["title"],
}
data = client.chat_json("Name a color", schema=schema)
Responses API (built-in tools)
Additive REST wrap of POST /v1/responses. Chat remains the default text path (chat / chat_stream). Built-in server tools (web search, X search, code interpreter, collections/file_search, image generation) are opt-in — they are never sent unless you pass tools=.
from xaikit import MockChatProvider, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
out = client.create_response(
"What is 101*3?",
tools=[{"type": "code_interpreter"}], # omit tools= for text-only
)
# out["id"] / out["output"] / out["usage"]
# client.get_response(out["id"])
When a usage meter is attached, purpose= is required. Events use modality="responses". The public pricing table has no Responses/tools rate, so the meter records tokens without inventing USD.
Priority processing and deferred chat
Optional service_tier="priority" (or "default") on chat / chat_stream / chat_json and create_response. Omit the knob for default. Invalid values are rejected before the network.
Deferred completions are a separate REST pair — not a second return type on chat:
from xaikit import MockChatProvider, XaiClient
client = XaiClient(provider=MockChatProvider(), api_key="test-key")
# Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
ticket = client.create_deferred_chat([{"role": "user", "content": "126/3=?"}])
# ticket["request_id"]
# result = client.get_deferred_chat(ticket["request_id"])
# result["status"] is "pending" (HTTP 202) or "complete" (HTTP 200 + completion fields)
Create and pending get meter modality="chat" without tokens. A complete get may record usage tokens. No invented USD.
Opt-in dev completion traces (default off)
from xaikit import CompletionTracer, InMemoryTraceSink, MockChatProvider, XaiClient
tracer = CompletionTracer(sink=InMemoryTraceSink())
client = XaiClient(
provider=MockChatProvider(replies="hi"),
model="grok-4.7",
completion_tracer=tracer,
)
client.chat([{"role": "user", "content": "hello"}])
Optional gap log (companion — default off)
uv run python -m xaikit.gaps --path ./gaps.jsonl
# or: xaikit-gaps --path ./gaps.jsonl --kind capability_gap
HTTP mounts (examples/docs only)
Thin FastAPI samples under examples/ — not required package surface.
License
MIT
Contributor / agent docs (not part of the installed package): docs/Master_Index.md.
Metadata
Release files for xaikit-py 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xaikit_py-0.4.0.tar.gz | 234.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xaikit_py-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 365.8 kB
Release files / xaikit_py-0.4.0.tar.gz
| Download URL | xaikit_py-0.4.0.tar.gz |
|---|---|
| Size | 234.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7c4a0cb02b70ae943c4fa28db6d3d069c780acb5c90830188be83438202be136
|
|
BLAKE2b-256 checksum How to use checksums |
c3343ab4af6fccfa6ba8a3b12527d85e107653e5a505b5b12aca84fd9c6478fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / xaikit_py-0.4.0-py3-none-any.whl
| Download URL | xaikit_py-0.4.0-py3-none-any.whl |
|---|---|
| Size | 131.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5560649bad5f4d64ba0ef71a5d9387094e178d2292b2bdad81a76cf2a27b53b9
|
|
BLAKE2b-256 checksum How to use checksums |
a30baf37d751df2b24032faed6e9b3aa66b3f165c88a3b55f0789c733c29b261
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|