Python SDK
Official Python client for ScaiGrid. Sync and async clients, first-class
streaming for chat completions, typed errors, and dedicated sub-clients for
ScaiBot, ScaiSpeak, and ScaiDial.
Install
| pip install scailabs-scaigrid
|
The top-level import name is scaigrid (the distribution name carries the
ScaiLabs brand prefix, but the package itself stays short).
Python ≥ 3.10 supported. The only required dependencies are httpx and
pydantic.
Quickstart
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | from scaigrid import ScaiGrid
with ScaiGrid(api_key="sgk_...") as client:
# List models
for m in client.models.list(modality="chat"):
print(m["slug"])
# Streaming chat
for chunk in client.inference.chat_stream(
model="anthropic/claude-haiku-4-5",
messages=[{"role": "user", "content": "Hello!"}],
metadata={"agent_id": "..."}, # flows into accounting
):
print(chunk["delta"].get("content", ""), end="", flush=True)
|
Async variant — same API, different prefix:
| from scaigrid import AsyncScaiGrid
async with AsyncScaiGrid(api_key="sgk_...") as client:
resp = await client.inference.chat(
model="anthropic/claude-haiku-4-5",
messages=[{"role": "user", "content": "Hi"}],
)
|
Authentication
Pick exactly one of three modes:
1
2
3
4
5
6
7
8
9
10
11
12 | # 1. Static API key (most common)
client = ScaiGrid(api_key="sgk_...")
# 2. Pre-minted JWT
client = ScaiGrid(access_token=jwt)
# 3. OAuth client_credentials against ScaiKey — cached + auto-refresh
client = ScaiGrid.from_client_credentials(
client_id="...",
client_secret="...",
token_url="https://scaikey.scailabs.ai/api/v1/platform/oauth/token",
)
|
For full control (custom auth, refresh, token caching) implement the
Auth protocol and pass it as auth=:
| from scaigrid import Auth, ScaiGrid
class MyAuth:
def header(self) -> str: ...
async def header_async(self) -> str: ...
client = ScaiGrid(auth=MyAuth())
|
Resources
1
2
3
4
5
6
7
8
9
10
11
12 | # Models — list, get
client.models.list(modality="chat")
client.models.get("anthropic/claude-haiku-4-5")
# Inference — chat (sync), chat_stream (Iterator), embed
client.inference.chat(model=..., messages=...)
client.inference.chat_stream(model=..., messages=...)
client.inference.embed(model="text-embedding-3-small", input=["..."])
# Modules — list/get/enable/disable, generic proxy
client.modules.list()
client.modules.proxy("scaimatrix", "POST", "/collections", json_body={...})
|
Module sub-clients
First-class wrappers for the most-used module routes:
ScaiBot
| bot = client.scaibot.bots.create(
name="support",
model="anthropic/claude-haiku-4-5",
display_name="Support Bot",
)
calls = client.scaibot.voice.list_calls(limit=20)
transcript = client.scaibot.voice.get_transcript(call_id)
cost = client.scaibot.voice.get_cost(call_id)
|
ScaiSpeak
| audio = client.scaispeak.synthesize(
voice_id="voice_...",
text="Hello, world!",
)
# audio["audio"] is decoded bytes; audio["audio_base64"] is the raw payload.
voices = client.scaispeak.voices.list(language="nl")
|
ScaiDial
| extensions = client.scaidial.extensions.list()
client.scaidial.extensions.create(number="1042", type="bot", target_ref=bot.id)
matches = client.scaidial.directory.lookup("billing")
client.scaidial.dialplans.add_rule(
dialplan_id, action_type="goto_extension",
action_params={"extension_number": "1042"},
)
|
For modules without a dedicated wrapper (ScaiMatrix, ScaiMind, ScaiPersona,
etc.), reach for client.modules.proxy(module_id, method, path, ...).
Streaming
chat_stream returns an iterator of typed chunk dicts. Each chunk is the
native ScaiGrid shape — delta, finish_reason, usage, model — not
the OpenAI-compat envelope.
| buffer = []
for chunk in client.inference.chat_stream(model=..., messages=...):
delta = chunk.get("delta", {}).get("content")
if delta:
buffer.append(delta)
if chunk.get("finish_reason"):
print(f"finished: {chunk['finish_reason']}")
print("".join(buffer))
|
The async client yields the same shape via async for.
Errors
Every exception is a subclass of ScaiGridError with the same code
string as the server. Catch the specific subclass you care about:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | from scaigrid import (
ScaiGridError, NotFoundError, RateLimitedError,
BudgetExceededError, ValidationError,
)
try:
client.inference.chat(...)
except RateLimitedError as exc:
print(f"retry after {exc.retry_after}s")
except BudgetExceededError:
# Tenant or user budget cap hit
...
except ScaiGridError as exc:
# Catch-all — exc.code, exc.http_status, exc.request_id all populated.
...
|
Full taxonomy: AuthError, AuthTokenInvalidError,
AuthTokenExpiredError, PermissionDeniedError, NotFoundError,
ConflictError, ValidationError, RateLimitedError,
BudgetExceededError, BackendError, BackendTimeoutError,
BackendRateLimitedError, InternalServerError, TimeoutError,
TransportError.
Paginated endpoints expose a cursor in the response. The SDK lets you
iterate manually:
| cursor = None
while True:
page = client.scaibot.voice.list_calls(limit=100, cursor=cursor)
for call in page["items"]:
process(call)
cursor = page.get("pagination", {}).get("next_cursor")
if not cursor:
break
|
Request IDs + observability
Every method accepts an optional request_id= kwarg. The SDK forwards it
as X-Request-ID and ScaiGrid propagates it through logs, audit records,
and downstream module calls.
| client.inference.chat(
model=..., messages=...,
request_id="req-batch-job-7",
)
|
Server-side exceptions surface the request_id on the raised exception
so you can join client- and server-side traces.
Retries
The HTTP layer retries 429 and idempotent 5xx up to three times
(exponential backoff with jitter, honours Retry-After). Tune via
the constructor:
| client = ScaiGrid(api_key="sgk_...", max_retries=5, timeout=120)
|
POST requests (the inference path) are not retried by default; the
user-side retry policy lives in the caller.
Download
The current release is available on
/downloads under ScaiGrid →
Python, with SHA-256 verification.
See also