AIR

Architecture

This describes how the pieces of AIR fit together, based on the current source tree.

text
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):

  1. If request is a plain string, it becomes a single ChatRequest with one user Message.
  2. 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.
  3. 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.
  4. Otherwise AIR builds a priority ordered list of registered providers. If routing="intelligent", it first tries to pick a better starting provider using classify_request and select_provider, then still falls back through the remaining providers in priority order if that pick fails.
  5. 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).
  6. 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.
  7. 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.
  8. If every provider in the list fails or is skipped, AIR raises ProvidersExhaustedError with 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.