codai docs
SDKs

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-sdk

Quickstart

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.

AttributeMethodsGateway
chat.completionscreate, streamPOST /v1/chat/completions
messagescreate, streamPOST /v1/messages — Anthropic Messages wire
responsescreate, streamPOST /v1/responses — OpenAI Responses wire
embeddingscreatePOST /v1/embeddings
audiotranscribe, transcribe_detailed, speech, speech_detailed/v1/audio/*
tokenscreatePOST /v1/tokens
modelslistGET /v1/models
healthget, ready, status/health, /health/ready, /status
agentsrun; runs.create, runs.get, runs.steps, runs.stats, runs.cancel, runs.stream/v1/agents/*
toolssearch, fetch/v1/tools/*
taskslist, pending, stats, get, confirm/v1/tasks/*
sessionscreate, list, get, update, delete, dispatch, stream; events.*, controls.*, lease.*, shares.*/v1/sessions/*
deviceslist, update, delete, dispatch_inbox/v1/devices/*
hostslist, exec, post_result, stream/v1/hosts/*
orgscreate, list; members.list, members.add, members.remove/v1/orgs/*
accountget, update/v1/account
receiptgetGET /v1/receipt
feedbacksubmitPOST /v1/feedback
phone_modelslistGET /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:

FieldTypeSource
contentstrFirst choice's assistant text.
rawdictThe full OpenAI-shaped response.
request_idstr | Nonex-request-id — pass to feedback().
routed_tostr | Nonex-codai-routed-to.
exec_verifystr | Nonex-codai-exec-verify on codai-labs.
usagedict | 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 | None

Feedback

client.feedback(request_id, 1)                    # 👍
client.feedback(request_id, -1, comment="wrong")  # 👎, comment ≤ 500 chars

The 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

OptionHeader sentEffect
session_idX-Codai-Session-IdSession memory, sticky routing, per-session receipts. Client-wide or per call.
agent_mode=TrueX-Codai-Mode: agentPlan-and-execute loop on the gateway (Pro+).
compact="auto"X-Codai-Compact: autoDeterministic server-side context compaction. chat() only.
best_of=3 / best_of=0X-Codai-Best-OfForce 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 off

CodaiError.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"}])

On this page