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.
- xaikit_py-0.1.0a1/.gitignore +13 -0
- xaikit_py-0.1.0a1/LICENSE +21 -0
- xaikit_py-0.1.0a1/PKG-INFO +546 -0
- xaikit_py-0.1.0a1/README.md +516 -0
- xaikit_py-0.1.0a1/pyproject.toml +68 -0
- xaikit_py-0.1.0a1/src/xaikit/__init__.py +210 -0
- xaikit_py-0.1.0a1/src/xaikit/__main__.py +16 -0
- xaikit_py-0.1.0a1/src/xaikit/async_client.py +2457 -0
- xaikit_py-0.1.0a1/src/xaikit/batch.py +402 -0
- xaikit_py-0.1.0a1/src/xaikit/catalog.py +957 -0
- xaikit_py-0.1.0a1/src/xaikit/client.py +3860 -0
- xaikit_py-0.1.0a1/src/xaikit/collections.py +393 -0
- xaikit_py-0.1.0a1/src/xaikit/connect.py +106 -0
- xaikit_py-0.1.0a1/src/xaikit/credentials.py +50 -0
- xaikit_py-0.1.0a1/src/xaikit/gaps.py +519 -0
- xaikit_py-0.1.0a1/src/xaikit/pricing.py +251 -0
- xaikit_py-0.1.0a1/src/xaikit/provider.py +936 -0
- xaikit_py-0.1.0a1/src/xaikit/py.typed +0 -0
- xaikit_py-0.1.0a1/src/xaikit/realtime.py +557 -0
- xaikit_py-0.1.0a1/src/xaikit/retry.py +218 -0
- xaikit_py-0.1.0a1/src/xaikit/stt_stream.py +613 -0
- xaikit_py-0.1.0a1/src/xaikit/traces.py +172 -0
- xaikit_py-0.1.0a1/src/xaikit/tts_stream.py +570 -0
- xaikit_py-0.1.0a1/src/xaikit/types.py +75 -0
- xaikit_py-0.1.0a1/src/xaikit/usage.py +477 -0
- xaikit_py-0.1.0a1/tests/conftest.py +10 -0
- xaikit_py-0.1.0a1/tests/test_async_client_wiring.py +411 -0
- xaikit_py-0.1.0a1/tests/test_batch_wiring.py +330 -0
- xaikit_py-0.1.0a1/tests/test_catalog.py +743 -0
- xaikit_py-0.1.0a1/tests/test_chat_knobs.py +251 -0
- xaikit_py-0.1.0a1/tests/test_chat_tools.py +322 -0
- xaikit_py-0.1.0a1/tests/test_collections_wiring.py +373 -0
- xaikit_py-0.1.0a1/tests/test_connect_auth.py +324 -0
- xaikit_py-0.1.0a1/tests/test_deferred_chat_wiring.py +299 -0
- xaikit_py-0.1.0a1/tests/test_embed_wiring.py +261 -0
- xaikit_py-0.1.0a1/tests/test_files_wiring.py +406 -0
- xaikit_py-0.1.0a1/tests/test_gaps.py +143 -0
- xaikit_py-0.1.0a1/tests/test_live_smoke.py +315 -0
- xaikit_py-0.1.0a1/tests/test_media_wiring.py +490 -0
- xaikit_py-0.1.0a1/tests/test_meter_mock.py +109 -0
- xaikit_py-0.1.0a1/tests/test_otel_usage_sink.py +182 -0
- xaikit_py-0.1.0a1/tests/test_realtime_client_secrets.py +272 -0
- xaikit_py-0.1.0a1/tests/test_realtime_wiring.py +393 -0
- xaikit_py-0.1.0a1/tests/test_responses_wiring.py +424 -0
- xaikit_py-0.1.0a1/tests/test_stream_traces.py +122 -0
- xaikit_py-0.1.0a1/tests/test_stt_stream_wiring.py +350 -0
- xaikit_py-0.1.0a1/tests/test_tokenize_wiring.py +220 -0
- xaikit_py-0.1.0a1/tests/test_tts_stream_wiring.py +373 -0
- xaikit_py-0.1.0a1/tests/test_tts_voices_wiring.py +292 -0
- xaikit_py-0.1.0a1/tests/test_video_wiring.py +434 -0
|
@@ -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).
|