# Infrastruktur: AI Gateway (LiteLLM) **Filsti:** `docs/infra/ai_gateway.md` ## 1. Konsept Sidelinja bruker en sentralisert AI Gateway (LiteLLM) som eneste kontaktpunkt for alle AI-kall i systemet. All kode — Rust-workers, SvelteKit server-side — snakker med `http://ai-gateway:4000/v1`. Aldri direkte til leverandør-APIer. Fordeler: * **BYOK (Bring Your Own Key):** Direkte API-nøkler til Anthropic, Google, xAI — ingen markup * **OpenRouter som fallback:** Tilgang til alle modeller vi ikke har direkte nøkler til, og sikkerhetsventil ved nedetid * **Kostnadskontroll:** Rutineoppgaver rutes til gratisnivå (Gemini), dyre modeller kun når det trengs * **Sentralisert logging:** Token-bruk per funksjon (Podcastfabrikken, Editor AI-behandling, Live-assistent) på ett sted * **Redundans:** Automatisk failover mellom leverandører — redaksjonen merker ikke nedetid ## 2. Leverandører og bruksmønster | Leverandør | Nøkkeltype | Primært bruksområde | |---|---|---| | Google Gemini | BYOK (gratisnivå) | Rutineoppgaver: transkripsjonsvasking, research-oppsummering, metadata-uttrekk | | Anthropic (Claude) | BYOK | Oppgaver som krever høy resonneringsevne: live-assistent faktoid-vurdering, kompleks analyse | | xAI (Grok) | BYOK | Alternativ for analyse, sanntidssøk (når tilgjengelig) | | OpenRouter | BYOK | Fallback for alle modeller, sikkerhetsventil ved leverandør-nedetid | **Merk:** Kvaliteten på norsk tekst varierer mellom modeller. Test alltid med norsk innhold før en modell tildeles en produksjonsoppgave. ## 3. Modellruting ### 3.1 Arkitekturprinsipp: PG eier config, LiteLLM er stateløs PostgreSQL er single source of truth for all modellkonfigurasjon. LiteLLM er en stateløs proxy som får generert `config.yaml` fra PG-data. Dette gir: * **Ingen avhengighet til LiteLLM sitt admin API** — de endrer API mellom versjoner * **All konfig i samme backup/migrasjon** som resten av systemet * **Enkel bytte** — hvis LiteLLM erstattes, er all konfig intakt i PG * **Admin-UI i SvelteKit** — gjenbruker eksisterende `/admin/`-mønster ### 3.2 Datamodell ```sql -- Globale modellaliaser (server-nivå, ikke per workspace) CREATE TABLE ai_model_aliases ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), alias TEXT NOT NULL, -- 'sidelinja/rutine', 'sidelinja/resonering' description TEXT, -- 'Billig, høyt volum' is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(alias) ); -- Leverandør-modeller med prioritert fallback per alias CREATE TABLE ai_model_providers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), alias_id UUID NOT NULL REFERENCES ai_model_aliases(id) ON DELETE CASCADE, provider TEXT NOT NULL, -- 'gemini', 'openrouter', 'anthropic' model TEXT NOT NULL, -- 'gemini/gemini-2.5-flash', 'openrouter/anthropic/claude-sonnet-4' api_key_env TEXT NOT NULL, -- 'GEMINI_API_KEY', 'OPENROUTER_API_KEY' priority SMALLINT NOT NULL, -- lavere = prøves først is_active BOOLEAN NOT NULL DEFAULT true, UNIQUE(alias_id, model) ); -- Jobbtype → modellalias mapping CREATE TABLE ai_job_routing ( job_type TEXT PRIMARY KEY, -- 'ai_text_process', 'whisper_postprocess', etc. alias TEXT NOT NULL, -- 'sidelinja/rutine' description TEXT ); ``` ### 3.3 Config-generering SvelteKit-serveren genererer `config.yaml` fra PG ved oppstart og ved endringer i admin-panelet: 1. Les aktive aliaser og deres providers (sortert etter priority) 2. Skriv `config.yaml` til volum delt med LiteLLM-containeren 3. Restart LiteLLM (`docker restart ai-gateway`) eller send `SIGHUP` Generert config inkluderer alltid `router_settings` og `general_settings` fra faste verdier — kun `model_list` er dynamisk. ### 3.4 Jobbkø-styrt modellvalg Jobbkøen bruker `ai_job_routing` for å bestemme modellalias per jobbtype: | Jobbtype | Standard alias | Begrunnelse | |---|---|---| | `ai_text_process` (✨-behandling) | `sidelinja/rutine` | Tekstvasking, høyt volum | | `whisper_postprocess` | `sidelinja/rutine` | Transkripsjonsvasking, høyt volum | | `research_clip` | `sidelinja/rutine` | Research-oppsummering, høyt volum | | `live_factoid_eval` | `sidelinja/resonering` | Krever presis vurdering under tidspress | Modellalias lagres som felt på jobben i PG — kan overstyres manuelt per jobb ved behov. ### 3.5 Admin-panel (`/admin/ai`) Admin-panelet lar administrator: * Se og redigere modellaliaser og deres fallback-liste (drag-and-drop prioritering) * Aktivere/deaktivere individuelle leverandør-modeller * Endre jobbtype → alias mapping * Se live-status: hvilke leverandører som svarer, responstider * Trigge config-regenerering og LiteLLM-restart ## 4. Docker-oppsett ```yaml # docker-compose.dev.yml / docker-compose.yml ai-gateway: image: ghcr.io/berriai/litellm:main-stable restart: unless-stopped command: --config /etc/litellm/config.yaml environment: LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY} GEMINI_API_KEY: ${GEMINI_API_KEY} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} XAI_API_KEY: ${XAI_API_KEY} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} volumes: - ./config/litellm/config.yaml:/etc/litellm/config.yaml:ro ports: - "127.0.0.1:4000:4000" # kun localhost (dev), ingen port i prod networks: - sidelinja-dev # eller sidelinja-net i prod ``` ## 5. Prompt-kvalitetssikring (Promptfoo) Alle LLM-prompts i Sidelinja testes systematisk med [Promptfoo](https://promptfoo.dev) før de brukes i produksjon. Dette er spesielt viktig fordi vi jobber med norsk tekst, der modellkvaliteten varierer kraftig mellom leverandører. ### 5.1 Hva vi tester Hver jobbtype som bruker LLM har et tilhørende testsett: | Jobbtype | Testsett | Eksempler på assertions | |---|---|---| | `whisper_postprocess` | Norske transkripsjoner med kjente feil | Egennavn korrigert, setningsflyt bevart | | `openrouter_analyze` | Episoder med kjent metadata | Riktig tittel, kapitler matcher innhold | | `research_clip` | Nyhetsartikler med kjente aktører/fakta | Aktører identifisert, faktoider korrekte | | `live_factoid_eval` | Transkripsjons-chunks med kjente entiteter | Riktig entity-match, lav falsk-positiv-rate | ### 5.2 Hva vi sammenligner Promptfoo kjøres mot alle kandidatmodeller via AI Gateway: ```yaml # promptfoo-config.yaml providers: - id: "openai:chat:sidelinja/rutine" config: apiBaseUrl: "http://localhost:4000/v1" apiKey: "${LITELLM_MASTER_KEY}" - id: "openai:chat:sidelinja/resonering" config: apiBaseUrl: "http://localhost:4000/v1" apiKey: "${LITELLM_MASTER_KEY}" ``` Dette lar oss svare på: * Klarer Gemini (gratis) denne oppgaven like bra som Claude (betalt)? * Fungerer prompten på norsk, eller trenger vi en annen formulering? * Har en modelloppgradering hos leverandøren degradert kvaliteten? ### 5.3 Når vi kjører tester * **Ved ny prompt:** Før den tas i bruk i produksjon * **Ved modellbytte:** Før en leverandør/modell settes som primær for en jobbtype * **Periodisk (CI):** Månedlig cron-jobb i Forgejo Actions kjører `promptfoo eval` mot alle testsett. Resultater postes som issue ved regresjoner. Leverandører oppdaterer modeller uten varsel — automatisk regresjonssjekk fanger dette opp. * **Ved kvalitetsklager:** Når redaksjonen rapporterer dårlig output ### 5.4 Lagring av testsett Testsett og promptfoo-config versjonskontrolleres i Git under `tests/prompts/`. Testdata er norske eksempler fra faktiske episoder og artikler. ``` tests/prompts/ ├── promptfooconfig.yaml ├── whisper_postprocess/ │ ├── prompt.txt │ └── dataset.json ├── metadata_extract/ │ ├── prompt.txt │ └── dataset.json └── research_clip/ ├── prompt.txt └── dataset.json ``` ## 6. Tokenregnskap og kostnadskontroll ### 6.1 Token-logging per workspace Rust-workeren logger tokenforbruk etter hvert AI-kall. Dataen lagres i PG: ```sql CREATE TABLE ai_usage_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, job_id UUID REFERENCES job_queue(id) ON DELETE SET NULL, model_alias TEXT NOT NULL, -- 'sidelinja/rutine' model_actual TEXT, -- 'gemini/gemini-2.5-flash' (fra LiteLLM-respons) prompt_tokens INT NOT NULL, completion_tokens INT NOT NULL, total_tokens INT NOT NULL, estimated_cost NUMERIC(10, 6), -- USD, beregnet fra kjente priser job_type TEXT, -- 'ai_text_process', etc. created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_ai_usage_workspace_month ON ai_usage_log (workspace_id, created_at); ``` **Flyten:** 1. Rust-worker sender AI-kall via gateway, får tilbake `usage` i responsen 2. Worker skriver rad til `ai_usage_log` med workspace_id, tokens og modellinfo 3. Estimert kostnad beregnes fra en enkel prisliste i config (oppdateres manuelt) ### 6.2 Visning — to nivåer **Admin (`/admin/ai`):** Aggregert oversikt over alle workspaces. Tabell med totaler per workspace/modell/periode. Identifiserer kostnadsdrivere. **Workspace (sidebar-widget):** Enkel tekst-indikator i workspace-sidebar: `✨ 12.4k tokens denne uken`. Klikk åpner detaljert visning med fordeling per jobbtype og modell. Ingen speedometer — det krever et definert budsjett for å gi mening, og det er overkill for MVP. ### 6.3 Workspace-budsjett (fase 2) Når token-logging er på plass, kan budsjett-tak legges til: - Budsjett lagres i `workspaces.settings` (JSONB): `{ "ai_budget": { "monthly_limit_usd": 50 } }` - Rust-worker sjekker aggregert forbruk før AI-kall - Ved budsjett nær: fall tilbake til `sidelinja/rutine` (billigste) - Ved budsjett nådd: sett jobb i `paused` med varsel i workspace-chat ### 6.4 Per-episode maks-kostnad Podcastfabrikken-jobber (whisper + metadata + oppsummering) kan estimere totalkostnad basert på lydlengde. Jobben avbrytes med varsel hvis estimert kostnad overstiger `max_cost_per_episode` (default: $5). ## 7. Dataklassifisering (ref. docs/arkitektur.md 2.2) | Data | Kategori | Detaljer | |---|---|---| | LiteLLM config.yaml | Gjenskapbar (Git) | Versjonskontrollert | | API-nøkler | Kritisk (.env) | Aldri i Git | | Token-bruk-logger | Flyktig (TTL 90 dager) | For kostnadsoversikt, ryddes automatisk | | Promptfoo testsett | Gjenskapbar (Git) | `tests/prompts/` — versjonskontrollert | | Promptfoo testresultater | Flyktig (lokal) | Kjøres on-demand, ikke lagret permanent | ## 8. Kildevern-modus (proposal) For sensitive redaksjonelle diskusjoner kan en lokal LLM-leverandør (Ollama/vLLM) registreres som `sidelinja/lokal` i config. Channels/møter med `kildevern: true` ruter all AI-prosessering til denne modellen — data forlater aldri serveren. Se `docs/proposals/kildevern_modus.md`. ## 9. Instruks for Claude Code * All AI-kode skal peke på `http://ai-gateway:4000/v1` — aldri direkte til leverandør * Bruk modellaliaser (`sidelinja/rutine`, `sidelinja/resonering`) — aldri hardkod leverandør-spesifikke modellnavn i applikasjonskode * API-nøkler i `.env`, aldri i config-filer eller kode * Test alltid med norsk innhold før en ny modell/leverandør tas i bruk for en produksjonsoppgave * Kjør `promptfoo eval` før du endrer prompts eller bytter modell for en jobbtype * Nye jobbtyper som bruker LLM skal ha et tilhørende testsett i `tests/prompts/` før de merges