Architecture
This describes how the pieces of AIR fit together, based on the current source tree.
air/
client.py AIR class: the main entry point
config.py AIRConfig dataclass
models.py Message, ChatRequest, ChatResponse, Usage, Role
exceptions.py exception hierarchy
console.py optional plain-text [AIR] formatting for exceptions
announcer.py optional TTS announcer, consumes structured events
providers/
base.py Provider abstract base class
xai.py, gemini.py, groq.py, openrouter.py built in provider adapters
routing/
health.py HealthStatus, ProviderHealth
capacity.py token estimation, ProviderLimits, UsageTotals, CapacityKind
capabilities.py ProviderCapabilities
fallback.py which errors trigger fallback
intelligent.py request classification and capability based provider scoring
context/
base.py ContextStore abstract interface
memory.py InMemoryContextStore (default)
sqlite.py SQLiteContextStore (opt in)
session.py Session wrapper used by air.session(...)
optimize.py context budget, trimming, local summarization
events/
__init__.py Event dataclass and event builder functions
tts/
base.py TTSBackend protocol
system.py SystemTTSBackend (macOS/Linux/Windows)
Request flow
When you call await air.chat(request):
- If
requestis a plain string, it becomes a singleChatRequestwith one userMessage. - Context optimization runs (
AIR._optimize_context). If the conversation is short, this is close to a no-op except for trimming any single oversized message. If it exceeds the configured budget, older messages are replaced with a local (or, if configured, AI assisted) summary while the most recent messages are kept exactly. See features/context-optimization.md. - If the request (or
AIRConfig.default_provider) names an explicit provider, AIR calls that provider directly. No fallback happens in this path; if that provider raises, the exception propagates to the caller. - Otherwise AIR builds a priority ordered list of registered providers. If
routing="intelligent", it first tries to pick a better starting provider usingclassify_requestandselect_provider, then still falls back through the remaining providers in priority order if that pick fails. - For each candidate provider in order: AIR checks whether it should be skipped before ever calling it (marked exhausted, or estimated capacity below the estimated request size). If not skipped, AIR calls
provider.chat(request). - If the call succeeds, AIR records success (health, usage, thresholds), attaches routing/event/optimization metadata to the
ChatResponse, dispatches any events collected during this call, and returns the response. - If the call raises one of the fallback eligible errors (
QuotaExceededError,RateLimitError,ProviderTimeoutError,ProviderAPIError), AIR records the failure, emits a provider switch event, and tries the next provider. Any other exception (AuthenticationError,ProviderResponseError, or anything else) propagates immediately without trying another provider. - If every provider in the list fails or is skipped, AIR raises
ProvidersExhaustedErrorwith the list of attempted providers and their errors.
See core/routing.md and core/fallback.md for the details.
Sessions and context
air.session(session_id) returns a Session bound to that id and to AIR's configured context store (in memory by default, SQLite if persist=... was passed to AIR()). Session.chat(...) loads prior messages from the store, prepends them to the new message(s), calls air.chat(...) with the combined request, then appends the user message(s) and the assistant's reply back to the store. Because the store is provider independent (only role, content, provider name, model, and timestamp are kept), a session's context survives provider switches, whether automatic (fallback) or manual (provider= argument). See core/sessions.md.
Events
Every routing decision that matters (provider switch, exhaustion, recovery, usage threshold crossed, capacity based skip) is represented as a structured Event dataclass, not just a string. These events are dispatched to:
- any callback registered with
air.on_event(callback) - the optional
Announcer(air.announcer), if enabled, which can speak them through a TTS backend
Both of these are entirely optional. AIR routing works the same whether or not anything is subscribed. See features/structured-events.md and features/tts.md.