Metadata-Version: 2.4
Name: air-router
Version: 0.1.0
Summary: Local, provider agnostic AI routing library
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# AIR (Automated Intelligence Router)

AIR is a local, provider-agnostic AI routing library for Python. Applications call AIR instead of integrating directly with individual AI providers. AIR runs no hosted infrastructure of its own — you supply your own provider API keys, and AIR talks to those providers directly.

## Status

V1. Core feature set is frozen: provider abstraction, priority and intelligent routing with automatic fallback, provider health/usage/capacity tracking, local session storage (in-memory or SQLite), context optimization for long conversations, and an optional structured event/TTS announcer system.

## Install

```bash
pip install air-router
```

## Quick start

```python
import asyncio
from air import AIR
from air.providers.xai import XAIProvider
from air.providers.gemini import GeminiProvider

async def main():
    air = AIR()
    air.add_provider(XAIProvider(api_key="...", model="grok-4-0709"), priority=1)
    air.add_provider(GeminiProvider(api_key="...", model="gemini-2.5-flash"), priority=2)

    response = await air.chat("Hello")
    print(response.content, response.provider)

asyncio.run(main())
```

If the first provider fails (rate limit, quota exhaustion, timeout, or API error), AIR automatically falls back to the next one in priority order. Authentication and malformed-response errors are not retried automatically, since they usually indicate a configuration problem rather than a transient outage.

Built-in providers: `air.providers.xai.XAIProvider`, `air.providers.gemini.GeminiProvider`, `air.providers.groq.GroqProvider`, `air.providers.openrouter.OpenRouterProvider`. Add your own by implementing `air.providers.base.Provider`.

## Sessions and context

```python
session = air.session("user-123")
await session.chat("My name is Alex")
await session.chat("What is my name?")          # automatic routing
await session.chat("Continue this", provider="groq")  # manual override for one turn
```

Sessions persist conversation history independently of any provider, so switching providers mid-conversation (automatically via fallback, or manually) never loses context. By default history lives in memory for the life of the process. Pass `persist="path/to.db"` to `AIR()` to store sessions in a local SQLite file instead — no server, no cloud database. AIR never stores API keys or provider-specific message formats, only role, content, provider name, model, and timestamp.

Long conversations are trimmed to a configurable token budget before each request (`AIRConfig.context_budget`), keeping the most recent messages intact and summarizing older ones locally by default. The full original history is always preserved in the session store regardless of what was sent to the provider. AI-assisted summarization is available but off by default (`summarize_with_ai=True`) so AIR never spends provider quota on your behalf without being asked.

## Routing modes

- `routing="priority"` (default): try providers in the configured priority order, falling back on transient failures.
- `routing="intelligent"`: also considers simple deterministic request signals (coding, reasoning, required capabilities, estimated size) and each provider's declared capabilities/capacity/health to pick a starting provider. Fallback still applies if it fails.

Explicit provider selection (`provider="groq"` on a request or a session call) always bypasses routing and uses that provider directly.

## Provider limits and capacity

```python
air.add_provider(gemini, priority=1, limits={"tokens": 100_000, "reserve": 5_000})
```

AIR tracks usage reported by providers (when they report it) against this configured limit and skips a provider before sending a request if it confidently estimates insufficient remaining capacity — never guessing at quota a provider doesn't expose.

## Events and TTS

```python
def handler(event):
    print(event.type, event.provider, event.reason)

air.on_event(handler)  # structured events: provider_switch, provider_exhausted,
                       # provider_recovered, usage_threshold, all_exhausted, capacity_restored
```

Structured events require no console or audio output and are suitable for headless/server use (forward them over your own WebSocket/SSE/logging layer). Optional text-to-speech announcements are available and off by default:

```python
air.announcer.enable(events=["provider_switch", "all_exhausted"])
```

`SystemTTSBackend` is a free, optional convenience backend (macOS `say`, Linux `espeak`/`espeak-ng`, Windows `System.Speech`) used automatically if you don't supply your own. Any object with a `speak(text: str) -> None` method works as a backend, so applications can plug in Android `TextToSpeech`, iOS `AVSpeechSynthesizer`, Piper, Kokoro, or a cloud TTS service. TTS failures are always isolated and never affect routing, fallback, or the returned response.

## Requirements

Python 3.10+

## Development

```bash
pip install -e ".[dev]"
pytest
```

## Known limitations

- Token counts are conservative character-based estimates, not a real tokenizer.
- Remaining provider quota is only as accurate as usage the provider actually reports; none of the four built-in providers expose a true remaining-quota endpoint.
- `context_optimization`'s default local summarizer is a lightweight compaction, not semantic summarization.
- SQLite session storage uses a single connection per `AIR` instance guarded by an `asyncio.Lock`; it is not designed for multi-process concurrent writers.
- A provider marked `exhausted` (quota/credits) is skipped by automatic priority/intelligent routing indefinitely — AIR has no way to know when a provider's quota resets. Call that provider directly (set `provider=` on a `ChatRequest` passed to `air.chat()`, or pass `provider=` to `session.chat()`) to probe it again; a successful direct call clears its exhausted status for future automatic routing.
