Python
The codai-sdk package for Python 3.9+ — quickstart, every method, codai extensions and error handling.
codai-sdk is the official Python client. Zero dependencies (stdlib urllib), Python 3.9+, synchronous. The import name is codai.
pip install codai-sdkQuickstart
import os
from codai import Codai
client = Codai(api_key=os.environ["CODAI_API_KEY"], session_id="my-project")
result = client.chat([{"role": "user", "content": "Explain asyncio.gather in one line."}])
print(result.content)
print(result.routed_to) # upstream model that actually served
for delta in client.chat_stream([{"role": "user", "content": "hi"}]):
print(delta, end="", flush=True)
run = client.agents_run("Find and summarize the TODOs in this codebase")
print(run.result)
if result.request_id:
client.feedback(result.request_id, 1)Configure
client = Codai(
api_key=os.environ["CODAI_API_KEY"], # required — raises ValueError if empty
base_url="https://ai.codai.ro", # default
session_id="my-project", # optional — session memory + sticky routing
timeout=120.0, # seconds, default
max_retries=2, # retries on 429 / 5xx with exponential backoff
)Resource groups
Since 0.2.0 every gateway operation is reachable as a method on the client, grouped by tag exactly like the TypeScript SDK (camelCase becomes snake_case). The 0.1.x top-level methods below still work unchanged; chat, embeddings, models and feedback are callable resource groups, so client.chat([...]) and client.chat.completions.create({...}) are the same call.
| Attribute | Methods | Gateway |
|---|---|---|
chat.completions | create, stream | POST /v1/chat/completions |
messages | create, stream | POST /v1/messages — Anthropic Messages wire |
responses | create, stream | POST /v1/responses — OpenAI Responses wire |
embeddings | create | POST /v1/embeddings |
audio | transcribe, transcribe_detailed, speech, speech_detailed | /v1/audio/* |
tokens | create | POST /v1/tokens |
models | list | GET /v1/models |
health | get, ready, status | /health, /health/ready, /status |
agents | run; runs.create, runs.get, runs.steps, runs.stats, runs.cancel, runs.stream | /v1/agents/* |
tools | search, fetch | /v1/tools/* |
tasks | list, pending, stats, get, confirm | /v1/tasks/* |
sessions | create, list, get, update, delete, dispatch, stream; events.*, controls.*, lease.*, shares.* | /v1/sessions/* |
devices | list, update, delete, dispatch_inbox | /v1/devices/* |
hosts | list, exec, post_result, stream | /v1/hosts/* |
orgs | create, list; members.list, members.add, members.remove | /v1/orgs/* |
account | get, update | /v1/account |
receipt | get | GET /v1/receipt |
feedback | submit | POST /v1/feedback |
phone_models | list | GET /v1/phone/models |
Every method takes an optional ext={...} bag covering all documented X-Codai-* request headers (effort, thinking, thinking_budget, cache, no_task, task_id, incognito, mode, best_of, compact, device, share_token, … plus raw headers) — see headers. Request and response shapes are available as TypedDicts in codai._types, generated from the OpenAPI spec.
client = Codai(api_key=os.environ["CODAI_API_KEY"], device="5dc0de00-0000-4000-8000-00000000c0da")
r = client.chat.completions.create(
{"messages": messages, "model": "codai"},
ext={"effort": "high", "thinking": True, "thinking_budget": 8192},
)
stream = client.chat.completions.stream({"messages": messages})
for delta in stream: # text deltas; stream.chunks() yields raw chunk dicts
print(delta, end="")
print(stream.final.usage, stream.final.tool_calls)
# codai-native SSE streams are generators of {"event", "data"} frames
run = client.agents.runs.create({"task": "Summarise the repo README."})
for ev in client.agents.runs.stream(run["id"]):
if ev["event"] == "done":
print(ev["data"]["status"], ev["data"]["result"])
# shared sessions need a device id on the client
s = client.sessions.create({"title": "pairing"})
for ev in client.sessions.stream(s["id"], after=0):
print(ev["event"], ev["data"].get("seq"))Chat
result = client.chat(
messages=[{"role": "user", "content": "hello"}],
model="codai", # default
temperature=None,
max_tokens=None,
tools=None, # OpenAI function tools — you own the tool loop
agent_mode=False, # X-Codai-Mode: agent (Pro+)
compact=None, # "auto" → X-Codai-Compact: auto
best_of=None, # 3 forces best-of-N, 0 disables
session_id=None, # per-call override
)ChatResult is a dataclass:
| Field | Type | Source |
|---|---|---|
content | str | First choice's assistant text. |
raw | dict | The full OpenAI-shaped response. |
request_id | str | None | x-request-id — pass to feedback(). |
routed_to | str | None | x-codai-routed-to. |
exec_verify | str | None | x-codai-exec-verify on codai-labs. |
usage | dict | None | {"prompt_tokens", "completion_tokens"}. |
Tool calls
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
},
}]
first = client.chat([{"role": "user", "content": "Weather in Cluj?"}], tools=tools)
message = first.raw["choices"][0]["message"]
for call in message.get("tool_calls") or []:
args = json.loads(call["function"]["arguments"])
output = get_weather(args["city"])
final = client.chat([
{"role": "user", "content": "Weather in Cluj?"},
message,
{"role": "tool", "tool_call_id": call["id"], "content": json.dumps(output)},
], tools=tools)
print(final.content)Streaming
chat_stream() is a generator of text deltas. It parses data: frames, ignores keep-alives and stops on [DONE].
for delta in client.chat_stream(
[{"role": "user", "content": "Write a haiku about Python."}],
model="codai",
temperature=None,
max_tokens=None,
agent_mode=False,
session_id=None,
):
print(delta, end="", flush=True)chat_stream() yields text only. For the assembled tool calls, the final usage frame, the finish reason and the routing headers of a streamed reply use client.chat.completions.stream() and read .final after iterating (see resource groups); .chunks() yields the raw chat.completion.chunk dicts. An HTTP error before the first byte raises CodaiError.
Server-side agent
run = client.agents_run(
task="Summarize the key points of the provided text.", # required, ≤ 64 000 chars
context="…your input…", # optional, ≤ 256 000 chars
system="Answer in Romanian.", # optional, ≤ 32 000 chars
model=None, # optional, default codai
)
run.result # str
run.model # upstream that served
run.event_id # usage row id — pass to feedback()
run.usage # dict | NoneFeedback
client.feedback(request_id, 1) # 👍
client.feedback(request_id, -1, comment="wrong") # 👎, comment ≤ 500 charsThe SDK sends request_id as event_id. Ratings feed the routing priors for everyone.
Embeddings
vectors = client.embeddings(["hello", "world"], model="codai-embed", dimensions=None)
vectors[0] # list[float]Returns a list of vectors directly (not the raw envelope).
Audio
# Speech-to-text — multipart upload, max 25 MB. Returns the transcript.
with open("clip.webm", "rb") as f:
text = client.transcribe(f.read(), filename="clip.webm", model="codai-transcribe")
# Text-to-speech — returns audio bytes (mp3 by default).
audio = client.speech("Hello from codai.", model="codai-tts", voice="alloy")
with open("hello.mp3", "wb") as f:
f.write(audio)transcribe() does not retry — a multipart body cannot be replayed safely.
Ephemeral tokens
minted = client.mint_token("realtime", ttl_seconds=600)
minted["token"] # "codai_eph_v1.…" — hand to a browser client
minted["expires_at"] # ISO timestamp
minted["scope"] # "realtime"Scopes: "realtime", "audio", "embeddings". TTL 60–3 600 s. See ephemeral tokens.
Models
for m in client.models():
print(m["id"], m["codai"]["kind"]) # e.g. "codai alias"Returns the data list of GET /v1/models.
codai extensions
| Option | Header sent | Effect |
|---|---|---|
session_id | X-Codai-Session-Id | Session memory, sticky routing, per-session receipts. Client-wide or per call. |
agent_mode=True | X-Codai-Mode: agent | Plan-and-execute loop on the gateway (Pro+). |
compact="auto" | X-Codai-Compact: auto | Deterministic server-side context compaction. chat() only. |
best_of=3 / best_of=0 | X-Codai-Best-Of | Force or disable best-of-N sampling. chat() only. |
Everything else — effort tiers, thinking pins, task ids, device, share tokens — goes through the ext={...} bag accepted by every method (also by chat() and chat_stream()), with snake_case keys named after the header (effort, thinking_pin, task_id, …) and a raw headers dict as escape hatch; see resource groups and the headers page.
Errors
from codai import Codai, CodaiError
try:
client.chat([{"role": "user", "content": "hi"}])
except CodaiError as e:
print(e.status, e) # HTTP status (0 for network failures after retries)
code = (e.body or {}).get("error", {}).get("code") if isinstance(e.body, dict) else None
if code == "quota_exceeded":
... # plan task allowance used up — back offCodaiError.body is the parsed JSON error envelope when the gateway returned one, otherwise None or the raw bytes. Stable code values are listed under limits & pricing.
Async
There is no asyncio client in 0.1.1. The methods are blocking; in an async application wrap them with asyncio.to_thread, or call the gateway with an async HTTP client of your choice — the wire formats are plain HTTP.
result = await asyncio.to_thread(client.chat, [{"role": "user", "content": "hello"}])TypeScript
The codai-sdk package for Node and edge runtimes — configuration, resource groups with full OpenAPI parity, chat, streaming, agents, sessions, errors and migrating from the OpenAI SDK.
Shared sessions
One conversation, many devices — the gateway keeps the ordered event log; the device with the tools executes; everyone else follows or steers.