Getting Started#
Installation#
pip install saia-python
Or install from source:
git clone https://github.com/fschwar4/saia_python.git
cd saia_python
pip install -e .
Authentication#
The library resolves the API key automatically from the following sources, checked in order:
SAIA_API_KEYenvironment variable.saia_apifile (current directory, then home directory) containing the raw key.envfile (current directory, then home directory) withSAIA_API_KEY=...
API keys are obtained through the KISSKI LLM Service booking page.
Username#
The username corresponds to your Academic Cloud account and serves as the
owner prefix in ARCANA IDs (e.g. saiauser123 in saiauser123/My-Arcana).
It can be set via the SAIA_USERNAME environment variable, .env, or
config.toml:
from saia_python import load_username
username = load_username()
ARCANA IDs#
ARCANA IDs are resolved from .env, config.toml, and environment
variables via load_arcana_ids().
Use config.toml for multiple IDs:
[saia.arcana]
default = "username/My-Default-Arcana"
ids = ["username/First-Arcana", "username/Second-Arcana"]
Or set a single default in .env:
SAIA_ARCANA_ID=username/My-Arcana
from saia_python import load_arcana_ids
arcana_ids = load_arcana_ids()
# {'default': 'username/My-Arcana', '0': 'username/First', ...}
The full priority chain is documented in Implementation Details.
The distinction between .env and config.toml is described in
Configuration.
Note
Management endpoints (get, upload) accept both the full
owner/name format and the plain name. The owner prefix is stripped
automatically. The chat endpoint requires the full owner/name format.
OOP Interface#
from saia_python import SAIAClient
client = SAIAClient()
# Connectivity + auth check (combines /models GET with the ARCANA
# heartbeat). Returns bool by default; verbose=True returns a
# diagnostic dict listing which leg succeeded/failed.
client.health_check()
client.health_check(verbose=True)
# Models
client.models.list_ids()
# Chat completion
client.chat.completions(
model="meta-llama-3.1-8b-instruct",
messages=[{"role": "user", "content": "Hello!"}],
)
# Streaming
for chunk in client.chat.completions(
model="meta-llama-3.1-8b-instruct",
messages=[{"role": "user", "content": "Count to 5."}],
stream=True,
):
choices = chunk.get("choices", [])
if choices:
print(choices[0].get("delta", {}).get("content", ""), end="")
# Voice AI
client.voice.transcribe("audio.wav", language="de")
client.voice.translate("audio.wav")
# Non-blocking: wait=False returns a concurrent.futures.Future, so the
# call doesn't block. Resolve it later with .result() (re-raises errors).
future = client.voice.transcribe("audio.wav", wait=False)
transcript = future.result()
# ARCANA (RAG)
from saia_python import load_arcana_ids
arcana_ids = load_arcana_ids()
client.arcana.list()
print(client.arcana.info(arcana_ids["default"]))
client.arcana.upload(arcana_ids["default"], "document.pdf")
client.arcana.upload_directory(arcana_ids["default"], "path/to/docs/")
client.arcana.upload_directory(
arcana_ids["default"], "path/to/docs/", pattern="*.pdf", recursive=True
)
client.arcana.list_files(arcana_ids["default"])
client.arcana.delete_file(arcana_ids["default"], "document.pdf")
client.arcana.generate_index(arcana_ids["default"])
client.arcana.delete_index(arcana_ids["default"])
# End-to-end: create a new arcana, upload a directory, build the
# index — one call. The UUID-suffixed name from create() flows
# through to upload + index automatically.
result = client.arcana.setup_from_directory(
"MyKB", "./markdown/",
pattern="**/*.md",
update_toml=True, toml_label="my_kb",
)
print(result["arcana"]["id"]) # owner/MyKB-<uuid>
print(len(result["uploads"])) # number of files uploaded
print(result["index"]) # final index status
response = client.arcana.chat(
model="llama-3.3-70b-instruct",
messages=[{"role": "user", "content": "Summarize the document."}],
arcana_id=arcana_ids["default"],
)
# Pull the assistant text out of any chat / arcana response.
# Safe against empty `choices` and missing `content` fields —
# returns "" + logs a warning rather than raising.
from saia_python import text_of
print(text_of(response))
# Rate limits
print(client.get_rate_limits())
Tool Use (Function Calling)#
The SAIA API follows the OpenAI tool-calling protocol. Tools are defined as JSON schemas; the model decides when to invoke them:
import json
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
response = client.chat.completions(
model="llama-3.3-70b-instruct",
messages=[{"role": "user", "content": "Weather in Berlin?"}],
tools=tools,
)
msg = response["choices"][0]["message"]
if msg.get("tool_calls"):
tc = msg["tool_calls"][0]
args = json.loads(tc["function"]["arguments"])
result = {"temp_c": 18, "condition": "partly cloudy"}
final = client.chat.completions(
model="llama-3.3-70b-instruct",
messages=[
{"role": "user", "content": "Weather in Berlin?"},
msg,
{"role": "tool", "tool_call_id": tc["id"], "content": json.dumps(result)},
],
tools=tools,
)
print(final["choices"][0]["message"]["content"])
OpenAI SDK Integration#
The library provides an OpenAI-compatible client for use with tools that
require the openai SDK (RAGAS, LangChain, instructor, etc.).
Requires pip install saia-python[openai].
from saia_python import create_openai_client
# Credentials resolved automatically (same as SAIAClient)
openai_client = create_openai_client()
# Chat completions via OpenAI SDK
response = openai_client.chat.completions.create(
model="llama-3.3-70b-instruct",
messages=[{"role": "user", "content": "Hello!"}],
)
# Embeddings
embedding = openai_client.embeddings.create(
model="e5-mistral-7b-instruct",
input="Text to embed",
)
Or via the SAIAClient property:
client = SAIAClient()
client.openai.chat.completions.create(model="...", messages=[...])
Integration with ecosystem tools:
# instructor
import instructor
patched = instructor.from_openai(create_openai_client())
# RAGAS
from ragas.llms import LangchainLLMWrapper
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="llama-3.3-70b-instruct",
openai_api_key=client._api_key,
openai_api_base=client._base_url,
)
Tokenizers#
Download the open-weight models’ tokenizers and measure prompts offline — token
counts, the special/structural-token overhead a chat template adds, and subword
fertility. Requires pip install saia-python[tokenizer]. Tokenizer files
(never the weights) are cached under ~/saia_python/tokenizers/.
from saia_python import chat_template_tokens, token_distribution
# How many tokens does a system prompt cost on gpt-oss-120b?
r = chat_template_tokens(
"openai-gpt-oss-120b",
system="You are a careful assistant.",
user="Summarise the attached report.",
)
print(r.num_tokens, "tokens;", r.overhead_tokens, "from special tokens")
print(f"overhead is {r.overhead_ratio_total:.0%} of the templated total")
print("subword fertility:", round(r.fertility, 3))
# The system prompt may also be read from a .txt / .md file:
r = chat_template_tokens("openai-gpt-oss-120b", system_file="system_prompt.md")
# Size a RAG corpus against the embedding model ARCANA uses internally:
dist = token_distribution("path/to/markdown", "qwen3-embedding-4b")
print(dist.summary())
See Tokenizers for the full surface (downloading, loading,
client.tokenizers, and the tiktoken path for the external OpenAI models).
Functional Interface#
All services are accessible as standalone functions. The API key is resolved automatically when omitted:
from saia_python import list_model_ids, chat_completion, get_rate_limits
list_model_ids()
chat_completion(
model="meta-llama-3.1-8b-instruct",
messages=[{"role": "user", "content": "Hello!"}],
)
print(get_rate_limits())