Spaces:
Sleeping
Sleeping
| # `backend/providers/` β STT / TTS / LLM / embedding clients | |
| Every external model is fronted by a small typed client here. The orchestrator and helper modules **only** ever import provider symbols from this folder β single import surface so a provider swap is local. | |
| ## Files | |
| | File | Provider | Role | Notes | | |
| | --- | --- | --- | --- | | |
| | `base.py` | β | Abstract `LLM`, `STT`, `TTS`, `Embeddings` Protocols. Every concrete client conforms. | β | | |
| | `nvidia_nim_llm.py` | NVIDIA NIM | Core chain runner β `NimChainLLM(chain=[...])` uses probe-driven primary election (KI-080): calls the elected PRIMARY once per turn, falls to elected BACKUP on real-time failure. Exposes `get_brain_llm()` (the separate `get_fast_brain_llm()` / `get_judge_llm()` accessors were collapsed into it in the 2026-05-15 three-chain consolidation). Legacy `_balanced_brain_chain()` (50/50 NIM β Groq rotator) retained as a bypassed feature-flag branch for one-release rollback. | [ADR-019](../../70-docs/60-decisions/ADR-019-nim-single-provider-consolidation.md), [ADR-031](../../70-docs/60-decisions/ADR-031-sticky-primary-election.md) (supersedes [ADR-026](../../70-docs/60-decisions/ADR-026-provider-load-balancing.md)) | | |
| | `groq_llm.py` | Groq | Single-call Llama-3.3-70B client. Used as cross-provider backup election candidate (KI-080) for both brain + fast-brain chains. | [ADR-031](../../70-docs/60-decisions/ADR-031-sticky-primary-election.md) | | |
| | `openrouter_llm.py` | OpenRouter | Multi-model fallback rung (DeepSeek-V3 etc.) for chains; rarely the primary in production. | β | | |
| | `sarvam_llm.py` | Sarvam-M | Indic-aware LLM. Used directly by `backend/single_brain.py` for Indic outputs β Sarvam-M routing is conditional on `_detect_language(user_text)` returning `'indic'`. | [ADR-006](../../70-docs/60-decisions/ADR-006-sarvam-first-stack.md) | | |
| | `sarvam_stt.py` | Sarvam Saarika v2.5 | Speech-to-text (10 Indic languages + English). | ADR-006 | | |
| | `sarvam_tts.py` | Sarvam Bulbul v2 | Text-to-speech; returns base64 WAV the frontend mounts in the in-DOM `<audio>` element. | ADR-006 | | |
| | `voyage_embeddings.py` | Voyage AI | Original ingest-time embedder. **Not on the hot path** β query-time uses Chroma vectors directly. Configured in `.env` for occasional re-ingest. | [ADR-011](../../70-docs/60-decisions/ADR-011-bge-local-embeddings.md) | | |
| | `local_embeddings.py` | BGE-small-en-v1.5 (local) | The actual production embedder. 384-dim, free, no rate cap. | ADR-011 | | |
| | `_smoke_test.py` | β | Stand-alone connectivity probe β hits every provider, prints latency + first 80 chars. Run before a long audit. | β | | |
| ## Invariants | |
| - **Never instantiate `NvidiaNimLLM(model=...)` directly.** Always go through `NimChainLLM(chain=...)` so the call survives single-pool rate limits. KI-033 migrated the last two stragglers (`profile_extractor`, `fact_find_normalizer`). | |
| - **Brain chains preserve family diversity.** Qwen brain β Mistral judge β failovers can't accidentally collapse to a single family and produce circular grading. | |
| - **Per-call random for load-balance, not a shared counter.** `random.random()` is evaluated at chain-construction time; a shared `itertools.cycle` breaks under async concurrency (see ADR-026 "Why per-call random"). | |
| ## Chain budgets | |
| | Chain | Per-link timeout (s) | Total budget (s) | Where set | | |
| | --- | --- | --- | --- | | |
| | Brain | 20 | 35 | `nvidia_nim_llm.py::get_brain_llm` | | |
| Per-link timeout is dynamically clipped to remaining budget. KI-084 explicit per-phase `httpx.Timeout(connect=2, read=<per-link>, write=2, pool=2)` is nested inside. | |
| ## Credit-aware election (KI-085) | |
| Beyond liveness, election in `backend/llm_health.py` is gated on `credits_remaining > credits_low_water` per candidate so quota-exhausted providers are excluded BEFORE producing a user-facing 429. KI-087 further prefers NIM as primary; Groq/OpenRouter serve as emergency fallback. | |
| ### Per-provider signal sources | |
| | Provider | Producer | Signal | Unit | Low-water | | |
| |---|---|---|---|---| | |
| | **Groq** | `update_credits_from_groq` from `groq_llm.py::chat` | `x-ratelimit-remaining-tokens-day` header + `x-ratelimit-reset-tokens-day` for reset | `tokens_day` | **5,000** (one fact-find round-trip ~2.4K + margin) | | |
| | **OpenRouter** | `poll_openrouter_credits` (every 10 min) + `update_credits_from_openrouter_headers` per-call fallback | `GET /api/v1/credits` β `{total_credits, total_usage}` | `usd_balance` | **$0.05** USD | | |
| | **NIM** | `record_nim_call` from `NimChainLLM._try` | Local 60s deque of monotonic timestamps per model | `requests_min` (remaining in current 60s window) | **5.0** slots (gate at 35/40 req/min) | | |
| ### Election predicate | |
| In `_is_election_eligible(h, now_mono)`: | |
| ```python | |
| if h.credits_reset_at is not None and now_mono >= h.credits_reset_at: | |
| return True # quota already reset | |
| if h.credits_remaining is None: # cold-start permissive | |
| return True | |
| return h.credits_remaining > h.credits_low_water | |
| ``` | |
| ### Cold-start | |
| `credits_remaining = None` is permissive β fresh process restarts don't grind to a halt before the first call has stamped credit state. OpenRouter poll fires on startup so the USD balance is non-None within seconds. | |
| See [ADR-032](../../70-docs/60-decisions/ADR-032-llm-chain-architecture.md) Β§6 for the full table and Β§12 for the operational runbook. | |
| ## Related | |
| - [ADR-006](../../70-docs/60-decisions/ADR-006-sarvam-first-stack.md), [ADR-011](../../70-docs/60-decisions/ADR-011-bge-local-embeddings.md), [ADR-019](../../70-docs/60-decisions/ADR-019-nim-single-provider-consolidation.md), [ADR-026](../../70-docs/60-decisions/ADR-026-provider-load-balancing.md) | |
| - `tests/test_routing_regression.py::TestProviderLoadBalancing` β pins the legacy 50/50 split (kept as a bypassed-by-default invariant; KI-080's probe-driven election supersedes it for live traffic) | |
| - `40-data/llm_health.json` β last health-probe snapshot surfaced in the admin tab | |