Architectural Overview#
This page describes the internal structure of the package and the design rationale behind the dual API (object-oriented and functional).
Repository Layout#
saia-python/
├── saia_python/ # Main package
│ ├── __init__.py # Public API, version, functional wrappers
│ ├── client.py # SAIAClient — composes all services
│ ├── chat.py # ChatService — completions + streaming
│ ├── voice.py # VoiceService — transcribe + translate
│ ├── arcana.py # ArcanaService — RAG / knowledge bases
│ ├── models.py # ModelsService — list available models
│ ├── documents.py # DocumentService — Docling conversion
│ ├── openai_compat.py # OpenAI SDK compatibility layer
│ ├── auth.py # Credential and config discovery
│ ├── rate_limits.py # RateLimitInfo dataclass + parser
│ ├── exceptions.py # SAIAError hierarchy + raise_for_status
│ └── _streaming.py # Shared SSE iterator
├── tests/ # Unit tests
├── docs/ # Sphinx documentation (PyData theme)
├── examples/
│ ├── saia_python_demo.ipynb # Interactive demo
│ └── config.toml.example # Template for structured config
├── .github/workflows/ # CI/CD (tests + docs deployment)
├── pyproject.toml # Package metadata + dependencies
└── README.md
Module Layers#
The package follows a four-layer architecture:
Infrastructure — shared utilities with no service-specific logic:
auth.py— credential and configuration discovery across environment variables,.env,.saia_api, andconfig.toml.exceptions.py— exception hierarchy and the singleraise_for_status()implementation used by all service modules._streaming.py— SSE (Server-Sent Events) line parser, shared byChatServiceandArcanaService.rate_limits.py— parsesx-ratelimit-*response headers into aRateLimitInfodataclass.
Service classes — each module encapsulates one SAIA API surface:
chat.py—ChatService: chat completions with streaming support.voice.py—VoiceService: audio transcription and translation.arcana.py—ArcanaService: RAG knowledge base management and chat.models.py—ModelsService: model listing.documents.py—DocumentService: document conversion via Docling.
Each service receives a shared requests.Session and base URL from the
client. Services do not manage authentication or HTTP sessions.
Composition — wires infrastructure and services together:
client.py—SAIAClientcreates the authenticated session and lazily instantiates service classes as properties (.chat,.voice,.models,.arcana,.documents).
Public surface — the user-facing API:
__init__.py— re-exportsSAIAClient, exception types, and configuration utilities. Defines the functional API as thin wrappers.
Dual API: OOP and Functional#
The library exposes two equivalent calling styles:
# Object-oriented
client = SAIAClient()
client.models.list_ids()
client.chat.completions(model="...", messages=[...])
# Functional
from saia_python import list_model_ids, chat_completion
list_model_ids()
chat_completion(model="...", messages=[...])
Avoiding Code Duplication#
All logic resides exclusively in the service classes. The functional API
consists of one-line wrappers that instantiate a temporary SAIAClient
and delegate:
# __init__.py (simplified)
def _make_client(api_key=None, base_url=None):
return SAIAClient(api_key=api_key, base_url=base_url or DEFAULT_BASE_URL)
def list_model_ids(*, api_key=None, base_url=None):
return _make_client(api_key, base_url).models.list_ids()
The resulting call chain:
Functional wrapper (__init__.py)
→ SAIAClient (client.py)
→ ServiceClass (e.g. ChatService)
→ HTTP request via shared Session
→ raise_for_status() (exceptions.py)
→ iter_sse() for streaming (_streaming.py)
No HTTP logic, request construction, or error handling is duplicated.
Performance Tradeoff#
Each functional call creates a new SAIAClient and requests.Session,
incurring a TCP handshake and TLS negotiation per call. The OOP style
reuses a single session with HTTP keep-alive:
# New session per call — acceptable for exploratory use
for prompt in prompts:
chat_completion(model="...", messages=[{"role": "user", "content": prompt}])
# Shared session — recommended for batch processing
client = SAIAClient()
for prompt in prompts:
client.chat.completions(model="...", messages=[{"role": "user", "content": prompt}])
This follows the same pattern as the requests library, where
requests.get() creates an internal session while
requests.Session().get() reuses one.
Use the functional API for interactive exploration and single calls.
Use SAIAClient when issuing multiple requests in sequence.