xaikit-py 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. xaikit_py-0.1.0a1/.gitignore +13 -0
  2. xaikit_py-0.1.0a1/LICENSE +21 -0
  3. xaikit_py-0.1.0a1/PKG-INFO +546 -0
  4. xaikit_py-0.1.0a1/README.md +516 -0
  5. xaikit_py-0.1.0a1/pyproject.toml +68 -0
  6. xaikit_py-0.1.0a1/src/xaikit/__init__.py +210 -0
  7. xaikit_py-0.1.0a1/src/xaikit/__main__.py +16 -0
  8. xaikit_py-0.1.0a1/src/xaikit/async_client.py +2457 -0
  9. xaikit_py-0.1.0a1/src/xaikit/batch.py +402 -0
  10. xaikit_py-0.1.0a1/src/xaikit/catalog.py +957 -0
  11. xaikit_py-0.1.0a1/src/xaikit/client.py +3860 -0
  12. xaikit_py-0.1.0a1/src/xaikit/collections.py +393 -0
  13. xaikit_py-0.1.0a1/src/xaikit/connect.py +106 -0
  14. xaikit_py-0.1.0a1/src/xaikit/credentials.py +50 -0
  15. xaikit_py-0.1.0a1/src/xaikit/gaps.py +519 -0
  16. xaikit_py-0.1.0a1/src/xaikit/pricing.py +251 -0
  17. xaikit_py-0.1.0a1/src/xaikit/provider.py +936 -0
  18. xaikit_py-0.1.0a1/src/xaikit/py.typed +0 -0
  19. xaikit_py-0.1.0a1/src/xaikit/realtime.py +557 -0
  20. xaikit_py-0.1.0a1/src/xaikit/retry.py +218 -0
  21. xaikit_py-0.1.0a1/src/xaikit/stt_stream.py +613 -0
  22. xaikit_py-0.1.0a1/src/xaikit/traces.py +172 -0
  23. xaikit_py-0.1.0a1/src/xaikit/tts_stream.py +570 -0
  24. xaikit_py-0.1.0a1/src/xaikit/types.py +75 -0
  25. xaikit_py-0.1.0a1/src/xaikit/usage.py +477 -0
  26. xaikit_py-0.1.0a1/tests/conftest.py +10 -0
  27. xaikit_py-0.1.0a1/tests/test_async_client_wiring.py +411 -0
  28. xaikit_py-0.1.0a1/tests/test_batch_wiring.py +330 -0
  29. xaikit_py-0.1.0a1/tests/test_catalog.py +743 -0
  30. xaikit_py-0.1.0a1/tests/test_chat_knobs.py +251 -0
  31. xaikit_py-0.1.0a1/tests/test_chat_tools.py +322 -0
  32. xaikit_py-0.1.0a1/tests/test_collections_wiring.py +373 -0
  33. xaikit_py-0.1.0a1/tests/test_connect_auth.py +324 -0
  34. xaikit_py-0.1.0a1/tests/test_deferred_chat_wiring.py +299 -0
  35. xaikit_py-0.1.0a1/tests/test_embed_wiring.py +261 -0
  36. xaikit_py-0.1.0a1/tests/test_files_wiring.py +406 -0
  37. xaikit_py-0.1.0a1/tests/test_gaps.py +143 -0
  38. xaikit_py-0.1.0a1/tests/test_live_smoke.py +315 -0
  39. xaikit_py-0.1.0a1/tests/test_media_wiring.py +490 -0
  40. xaikit_py-0.1.0a1/tests/test_meter_mock.py +109 -0
  41. xaikit_py-0.1.0a1/tests/test_otel_usage_sink.py +182 -0
  42. xaikit_py-0.1.0a1/tests/test_realtime_client_secrets.py +272 -0
  43. xaikit_py-0.1.0a1/tests/test_realtime_wiring.py +393 -0
  44. xaikit_py-0.1.0a1/tests/test_responses_wiring.py +424 -0
  45. xaikit_py-0.1.0a1/tests/test_stream_traces.py +122 -0
  46. xaikit_py-0.1.0a1/tests/test_stt_stream_wiring.py +350 -0
  47. xaikit_py-0.1.0a1/tests/test_tokenize_wiring.py +220 -0
  48. xaikit_py-0.1.0a1/tests/test_tts_stream_wiring.py +373 -0
  49. xaikit_py-0.1.0a1/tests/test_tts_voices_wiring.py +292 -0
  50. xaikit_py-0.1.0a1/tests/test_video_wiring.py +434 -0
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ *.egg-info/
9
+ .coverage
10
+ htmlcov/
11
+ .DS_Store
12
+ .env
13
+ .env.*
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brian Lowe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,546 @@
1
+ Metadata-Version: 2.5
2
+ Name: xaikit-py
3
+ Version: 0.1.0a1
4
+ Summary: Extractable xAI transport + catalog + connect + usage metering kit
5
+ Project-URL: Homepage, https://github.com/BrianCLowe/xAIkit
6
+ Project-URL: Repository, https://github.com/BrianCLowe/xAIkit
7
+ Project-URL: Issues, https://github.com/BrianCLowe/xAIkit/issues
8
+ Project-URL: Release notes, https://github.com/BrianCLowe/xAIkit/releases
9
+ Author: Brian Lowe
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: grok,llm,sdk,xai
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: <4.0,>=3.10
23
+ Requires-Dist: httpx>=0.27.0
24
+ Requires-Dist: pydantic>=2.0
25
+ Requires-Dist: websockets<17,>=14.0
26
+ Requires-Dist: xai-sdk>=1.17.0
27
+ Provides-Extra: otel
28
+ Requires-Dist: opentelemetry-api>=1.20; extra == 'otel'
29
+ Description-Content-Type: text/markdown
30
+
31
+ <p align="center">
32
+ <img src="https://raw.githubusercontent.com/BrianCLowe/xAIkit/master/docs/readme-header.jpg" alt="xAIkit — kit for interacting with xAI models and APIs" width="100%" />
33
+ </p>
34
+
35
+ # xAIkit
36
+
37
+ **Early testing release (`0.1.0a1`).** Not an official xAI package. The API may change; expect rough edges. A non-alpha release will follow after more testing.
38
+
39
+ Extractable **xAI transport + living model catalog + connect + usage metering** as a library-first Python kit.
40
+
41
+ Requires **Python 3.10+**, the same floor as the official [xAI Python SDK](https://github.com/xai-org/xai-sdk-python).
42
+
43
+ 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.
44
+
45
+ ## What it does
46
+
47
+ 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.
48
+
49
+ | You want to… | xAIkit gives you |
50
+ | --- | --- |
51
+ | **Call Grok from Python** (chat, stream, tools, vision, structured JSON) | `XaiClient` and `AsyncXaiClient` — same method names; JSON dicts, not protobuf |
52
+ | **Generate or edit images**, make video, or do speech on xAI | Imagine generate/edit, video + extend, REST + streaming STT/TTS, realtime speech-to-speech |
53
+ | **Pick a model** without hardcoding IDs that churn | `resolve_model("cheapest" \| "economy" \| "best")` per role (`chat` / `image` / `video` / `voice`); `pin=` still wins |
54
+ | **See what a feature costs** (tokens, estimated USD, OpenTelemetry) | Purpose-tagged `UsageMeter` — `purpose=` is required when a meter is attached |
55
+ | **Test without an API key** or live spend | `MockChatProvider` + `inject_catalog` — CI stays offline |
56
+ | **Keep keys out of the browser** for realtime voice | Server-side `create_realtime_client_secret` (and the `sec-websocket-protocol` helper) |
57
+ | **Bring your own credentials / IdP** | `api_key=` or a `CredentialStore`; OAuth URLs are caller-supplied, never hardcoded |
58
+ | **Use the rest of the xAI surface** | Files, embeddings, tokenize, batch, collections, Responses, priority and deferred chat |
59
+
60
+ Not an official xAI package. Domain schemas and the tool loop stay in your app.
61
+
62
+ ## Install
63
+
64
+ This is a **pre-release**. A plain `pip install xaikit-py` / `uv add xaikit-py` will not pick it up until a non-alpha version exists. The import stays `xaikit` (the PyPI name is `xaikit-py` because `xaikit` was too close to an existing explainable-AI project).
65
+
66
+ ```bash
67
+ # PyPI alpha (opt in)
68
+ uv add xaikit-py --prerelease allow
69
+ # or: pip install --pre xaikit-py
70
+
71
+ # From a git tag
72
+ uv add "xaikit-py @ git+https://github.com/BrianCLowe/xAIkit@v0.1.0a1"
73
+
74
+ # Editable neighbor checkout
75
+ uv add --editable ../xAIkit
76
+ ```
77
+
78
+ ## Quick usage
79
+
80
+ ```python
81
+ from xaikit import (
82
+ InMemoryUsageSink,
83
+ MockChatProvider,
84
+ UsageMeter,
85
+ XaiClient,
86
+ )
87
+
88
+ meter = UsageMeter(sink=InMemoryUsageSink())
89
+ client = XaiClient(
90
+ provider=MockChatProvider(replies="hi"),
91
+ model="grok-4.5",
92
+ usage_meter=meter,
93
+ )
94
+ resp = client.chat([{"role": "user", "content": "hello"}], purpose="demo.chat")
95
+ ```
96
+
97
+ Async twin (`AsyncXaiClient`) uses the same method names; REST uses `httpx.AsyncClient` and live chat uses `xai_sdk.AsyncClient`. `MockChatProvider` works with both clients:
98
+
99
+ ```python
100
+ import asyncio
101
+ from xaikit import AsyncXaiClient, MockChatProvider
102
+
103
+ async def main() -> None:
104
+ client = AsyncXaiClient(provider=MockChatProvider(replies="hi"), model="grok-4.5")
105
+ resp = await client.chat([{"role": "user", "content": "hello"}])
106
+ print(resp.content)
107
+
108
+ asyncio.run(main())
109
+ ```
110
+
111
+ When `usage_meter` is attached, **purpose is required**. Without a meter, purpose is optional.
112
+
113
+ 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.
114
+
115
+ ## Credentials and OAuth
116
+
117
+ 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`.
118
+
119
+ ```python
120
+ from xaikit import (
121
+ build_oauth_authorize_url,
122
+ exchange_oauth_code,
123
+ oauth_is_configured,
124
+ )
125
+
126
+ # Your app supplies the IdP endpoints — not the kit.
127
+ authorize = "https://idp.example.com/authorize"
128
+ token = "https://idp.example.com/token"
129
+
130
+ assert oauth_is_configured(client_id="app-id", client_secret="app-secret")
131
+ url = build_oauth_authorize_url(
132
+ client_id="app-id",
133
+ redirect_uri="https://app.example.com/callback",
134
+ state="nonce-1",
135
+ authorize_url=authorize,
136
+ )
137
+ # tokens = exchange_oauth_code(
138
+ # code,
139
+ # client_id="app-id",
140
+ # client_secret="app-secret",
141
+ # redirect_uri="https://app.example.com/callback",
142
+ # token_url=token,
143
+ # )
144
+ ```
145
+
146
+ ## Catalog resolve
147
+
148
+ Callers pass `cheapest` / `economy` / `best` (and optional `role=`). Chat is the default pool. Pin still wins when `pin=` is set.
149
+
150
+ ```python
151
+ from xaikit import ModelInfo, inject_catalog, resolve_model, resolve_model_selection
152
+
153
+ inject_catalog(
154
+ [
155
+ ModelInfo(id="grok-4.6", capabilities=["chat"], input_per_million=20.0, created=2),
156
+ ModelInfo(id="grok-imagine-image", capabilities=["image"], created=1),
157
+ ModelInfo(id="grok-imagine-image-quality", capabilities=["image"], created=2),
158
+ ]
159
+ )
160
+
161
+ chat_id = resolve_model(intent="economy") # role="chat" default
162
+ image = resolve_model_selection(intent="best", role="image")
163
+ video_id = resolve_model(intent="cheapest", role="video")
164
+ voice_id = resolve_model(intent="economy", role="voice")
165
+ ```
166
+
167
+ `role` is `chat` | `image` | `video` | `voice`. Offline tests inject fixtures with `inject_catalog` — do not hit the network.
168
+
169
+ When `model` is omitted, chat resolve falls back to `BOOTSTRAP_MODEL` (`grok-4.6`). Offline with no API key or fixture, `list_models` injects `grok-4.6` 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.
170
+
171
+ ## Image generation and edit
172
+
173
+ 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). Source image is a public URL, data URI, or a `file_id` from `upload_file`.
174
+
175
+ ```python
176
+ from xaikit import MockChatProvider, XaiClient
177
+
178
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
179
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
180
+
181
+ out = client.generate_image("A red cube on a table", aspect_ratio="1:1")
182
+ edited = client.edit_image(
183
+ "Make it a pencil sketch",
184
+ image_url=out["url"], # or image_file_id="file-..."
185
+ )
186
+ # edited["url"] / edited["b64_json"] / edited["file_id"]
187
+ ```
188
+
189
+ Default model is `grok-imagine-image-quality`. When Imagine returns `file_output.file_id`, both methods surface it as `file_id`.
190
+
191
+ ## Files
192
+
193
+ 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}`.
194
+
195
+ ```python
196
+ from xaikit import MockChatProvider, XaiClient
197
+
198
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
199
+ meta = client.upload_file(b"hello", "note.txt", content_type="text/plain")
200
+ # meta["id"] is the opaque file_id
201
+ # client.get_file(meta["id"])
202
+ # client.delete_file(meta["id"])
203
+ ```
204
+
205
+ Uploads larger than 50 MB are rejected before HTTP.
206
+
207
+ ## Embeddings
208
+
209
+ 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}, …]`. Pin `model=` (OpenAPI examples use `v1`; there is no documented grok-embedding default). Empty input is rejected before HTTP.
210
+
211
+ ```python
212
+ from xaikit import MockChatProvider, XaiClient
213
+
214
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
215
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
216
+
217
+ out = client.embed(["query: hello", "passage: world"], model="v1")
218
+ vectors = [row["embedding"] for row in out["data"]]
219
+ ```
220
+
221
+ 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.
222
+
223
+ ## Tokenizer
224
+
225
+ 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.
226
+
227
+ ```python
228
+ from xaikit import MockChatProvider, XaiClient
229
+
230
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
231
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
232
+
233
+ out = client.tokenize("Hello world")
234
+ n = out["count"]
235
+ pieces = [row["string"] for row in out["tokens"]]
236
+ ```
237
+
238
+ 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.
239
+
240
+ ## Batch
241
+
242
+ 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). Empty name / batch id / requests are rejected before the RPC.
243
+
244
+ ```python
245
+ from xaikit import MockChatProvider, XaiClient
246
+
247
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
248
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
249
+
250
+ job = client.create_batch("nightly-capitals")
251
+ client.add_batch_requests(
252
+ job["id"],
253
+ [
254
+ {
255
+ "messages": [{"role": "user", "content": "Capital of France?"}],
256
+ "batch_request_id": "fr",
257
+ }
258
+ ],
259
+ )
260
+ status = client.get_batch(job["id"])
261
+ # status["state"]["num_pending"] / num_success / …
262
+ # results = client.list_batch_results(job["id"])
263
+ ```
264
+
265
+ 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.
266
+
267
+ ## Collections
268
+
269
+ 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.
270
+
271
+ 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.
272
+
273
+ ```python
274
+ from xaikit import MockChatProvider, XaiClient
275
+
276
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
277
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"]) # plus XAI_MANAGEMENT_KEY in env
278
+
279
+ coll = client.create_collection("docs")
280
+ client.upload_document(coll["id"], "note.txt", b"hello world")
281
+ hits = client.search_collections("hello", coll["id"])
282
+ # hits["matches"][0]["chunk_content"] / file_id / score
283
+ ```
284
+
285
+ 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.
286
+
287
+ ## Video generation
288
+
289
+ REST Imagine video on `XaiClient` (mocked HTTP in tests; live calls need `XAI_API_KEY`). Default `wait=True` polls until the clip is ready; `wait=False` returns `request_id` for `poll_video`.
290
+
291
+ ```python
292
+ from xaikit import MockChatProvider, XaiClient
293
+
294
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
295
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
296
+
297
+ started = client.generate_video(
298
+ "A red cube rotating on a table",
299
+ duration=8,
300
+ aspect_ratio="16:9",
301
+ resolution="480p",
302
+ wait=False,
303
+ )
304
+ status = client.poll_video(started["request_id"])
305
+ # bytes = client.download_video(status["url"]) # when status == "done"
306
+ ```
307
+
308
+ `extend_video(prompt, video_url=...)` continues a clip. Default model is `grok-imagine-video-1.5`.
309
+
310
+ ## Realtime voice
311
+
312
+ 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.
313
+
314
+ ```python
315
+ from xaikit import MockChatProvider, XaiClient, decode_realtime_audio
316
+
317
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
318
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
319
+
320
+ with client.open_realtime_session(
321
+ voice="eve",
322
+ instructions="You are a helpful assistant.",
323
+ ) as session:
324
+ session.send_text("Hello!")
325
+ event = session.recv(timeout=30)
326
+ # audio bytes: decode_realtime_audio(event) # when type is response.output_audio.delta
327
+ # session.send_audio(pcm16_bytes)
328
+ ```
329
+
330
+ 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).
331
+
332
+ 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`).
333
+
334
+ ```python
335
+ from xaikit import MockChatProvider, XaiClient, realtime_client_secret_protocol
336
+
337
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
338
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"]) # server-side only
339
+
340
+ secret = client.create_realtime_client_secret(expires_after=300)
341
+ token = secret["value"]
342
+ # Client WS: Authorization: Bearer {token}
343
+ protocol = realtime_client_secret_protocol(token) # "xai-client-secret.{token}"
344
+ ```
345
+
346
+ ## Streaming speech-to-text
347
+
348
+ Unary-transcribe over `wss://api.x.ai/v1/stt`. Send raw PCM bytes (not base64). This is not the realtime voice (STS) socket.
349
+
350
+ ```python
351
+ from xaikit import MockChatProvider, XaiClient
352
+
353
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
354
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
355
+
356
+ pcm16_bytes = bytes(3200) # 100 ms of 16 kHz s16le PCM — apps own capture
357
+ with client.open_stt_session(language="en", interim_results=True) as session:
358
+ session.send_audio(pcm16_bytes)
359
+ session.audio_done()
360
+ for event in session.events():
361
+ if event.get("type") == "transcript.partial":
362
+ print(event.get("text"))
363
+ elif event.get("type") == "transcript.done":
364
+ break
365
+ ```
366
+
367
+ REST file transcription stays on `transcribe`. Offline tests mock the socket.
368
+
369
+ ## Streaming text-to-speech
370
+
371
+ 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.
372
+
373
+ ```python
374
+ from xaikit import MockChatProvider, XaiClient, decode_tts_audio
375
+
376
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
377
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
378
+
379
+ with client.open_tts_session(language="en", voice="eve", codec="mp3") as session:
380
+ session.send_text("Hello from streaming TTS.")
381
+ session.text_done()
382
+ for event in session.events():
383
+ chunk = decode_tts_audio(event)
384
+ if chunk:
385
+ pass # apps own playback
386
+ elif event.get("type") == "audio.done":
387
+ break
388
+ ```
389
+
390
+ REST unary synthesis stays on `synthesize_speech`. Offline tests mock the socket.
391
+
392
+ List built-in TTS voices (not team-scoped custom clones):
393
+
394
+ ```python
395
+ voices = client.list_tts_voices()
396
+ # voices[0]["voice_id"] / ["name"] / ["language"]
397
+ # client.get_tts_voice("eve")
398
+ ```
399
+
400
+ ## Streaming
401
+
402
+ ```python
403
+ for chunk in client.chat_stream(
404
+ [{"role": "user", "content": "hello"}],
405
+ purpose="demo.stream",
406
+ ):
407
+ print(chunk.delta, end="", flush=True)
408
+ ```
409
+
410
+ ## Tools, vision, and structured JSON
411
+
412
+ The kit wraps xAI chat extras as JSON dicts. It does **not** run tools — the app owns the loop.
413
+
414
+ ```python
415
+ from xaikit import MockChatProvider, XaiClient
416
+
417
+ weather_tool = {
418
+ "name": "get_weather",
419
+ "description": "Get the weather for a city.",
420
+ "parameters": {
421
+ "type": "object",
422
+ "properties": {"city": {"type": "string"}},
423
+ "required": ["city"],
424
+ },
425
+ }
426
+ client = XaiClient(
427
+ provider=MockChatProvider(
428
+ replies=[
429
+ "a cube",
430
+ {
431
+ "tool_calls": [
432
+ {"id": "call_1", "name": "get_weather", "arguments": {"city": "NYC"}},
433
+ ],
434
+ },
435
+ {"title": "blue"},
436
+ ]
437
+ ),
438
+ model="grok-4.5",
439
+ )
440
+
441
+ # Vision: content may be a string or a list of parts
442
+ client.chat(
443
+ [
444
+ {
445
+ "role": "user",
446
+ "content": [
447
+ {"type": "text", "text": "What is in this image?"},
448
+ {"type": "image_url", "url": "https://example.com/cube.png"},
449
+ ],
450
+ }
451
+ ]
452
+ )
453
+
454
+ # Tools: defs in, tool_calls out (arguments are parsed JSON, typically a dict)
455
+ resp = client.chat(
456
+ [{"role": "user", "content": "Weather in NYC?"}],
457
+ tools=[weather_tool],
458
+ tool_choice="auto",
459
+ )
460
+ # resp.tool_calls → [{"id": "call_1", "name": "get_weather", "arguments": {"city": "NYC"}}]
461
+ # App runs the function, then sends the assistant turn + tool result:
462
+ # client.chat([
463
+ # {"role": "user", "content": "Weather in NYC?"},
464
+ # {"role": "assistant", "content": "", "tool_calls": resp.tool_calls},
465
+ # {"role": "tool", "content": "72F", "tool_call_id": resp.tool_calls[0]["id"]},
466
+ # ], tools=[weather_tool])
467
+
468
+ # Native structured outputs (fence-stripping remains the fallback)
469
+ schema = {
470
+ "type": "object",
471
+ "properties": {"title": {"type": "string"}},
472
+ "required": ["title"],
473
+ }
474
+ data = client.chat_json("Name a color", schema=schema)
475
+ ```
476
+
477
+ ## Responses API (built-in tools)
478
+
479
+ 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=`.
480
+
481
+ ```python
482
+ from xaikit import MockChatProvider, XaiClient
483
+
484
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
485
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
486
+
487
+ out = client.create_response(
488
+ "What is 101*3?",
489
+ tools=[{"type": "code_interpreter"}], # omit tools= for text-only
490
+ )
491
+ # out["id"] / out["output"] / out["usage"]
492
+ # client.get_response(out["id"])
493
+ ```
494
+
495
+ 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.
496
+
497
+ ## Priority processing and deferred chat
498
+
499
+ 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.
500
+
501
+ Deferred completions are a separate REST pair — not a second return type on `chat`:
502
+
503
+ ```python
504
+ from xaikit import MockChatProvider, XaiClient
505
+
506
+ client = XaiClient(provider=MockChatProvider(), api_key="test-key")
507
+ # Live: XaiClient(api_key=os.environ["XAI_API_KEY"])
508
+
509
+ ticket = client.create_deferred_chat([{"role": "user", "content": "126/3=?"}])
510
+ # ticket["request_id"]
511
+ # result = client.get_deferred_chat(ticket["request_id"])
512
+ # result["status"] is "pending" (HTTP 202) or "complete" (HTTP 200 + completion fields)
513
+ ```
514
+
515
+ Create and pending get meter `modality="chat"` without tokens. A complete get may record `usage` tokens. No invented USD.
516
+
517
+ ## Opt-in dev completion traces *(default off)*
518
+
519
+ ```python
520
+ from xaikit import CompletionTracer, InMemoryTraceSink, MockChatProvider, XaiClient
521
+
522
+ tracer = CompletionTracer(sink=InMemoryTraceSink())
523
+ client = XaiClient(
524
+ provider=MockChatProvider(replies="hi"),
525
+ model="grok-4.5",
526
+ completion_tracer=tracer,
527
+ )
528
+ client.chat([{"role": "user", "content": "hello"}])
529
+ ```
530
+
531
+ ## Optional gap log *(companion — default off)*
532
+
533
+ ```bash
534
+ uv run python -m xaikit.gaps --path ./gaps.jsonl
535
+ # or: xaikit-gaps --path ./gaps.jsonl --kind capability_gap
536
+ ```
537
+
538
+ ## HTTP mounts *(examples/docs only)*
539
+
540
+ Thin FastAPI samples under [`examples/`](examples/) — not required package surface.
541
+
542
+ ## License
543
+
544
+ MIT
545
+
546
+ Contributor / agent docs (not part of the installed package): [`docs/Master_Index.md`](docs/Master_Index.md).