Setter opp AI Gateway med LiteLLM som sentralisert proxy for alle AI-kall. PG eier all modellkonfigurasjon — LiteLLM er stateløs. - Migrasjon 008: ai_model_aliases, ai_model_providers, ai_job_routing med seed-data for sidelinja/rutine og sidelinja/resonering - Config-generering fra PG: scripts/generate-litellm-config.sh filtrerer bort providers med tomme API-nøkler - Docker-container kjører på sidelinja-net (intern, ingen eksponert port) - Maskinrommet har AI_GATEWAY_URL via maskinrommet-env.sh - API-nøkkel-placeholders i .env (GEMINI, ANTHROPIC, XAI) - Oppdatert docs/infra/ai_gateway.md med faktisk config Verifisert: container healthy, modellaliaser eksponert, maskinrommet har korrekt gateway-URL. Reelle API-kall krever at Vegard fyller inn leverandør-nøkler i /srv/synops/.env. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
264 lines
12 KiB
Markdown
264 lines
12 KiB
Markdown
# Infrastruktur: AI Gateway (LiteLLM)
|
|
**Filsti:** `docs/infra/ai_gateway.md`
|
|
|
|
## 1. Konsept
|
|
Synops 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å)
|
|
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
|
|
# /srv/synops/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:
|
|
- /srv/synops/config/litellm:/etc/litellm
|
|
networks:
|
|
- sidelinja-net # intern — maskinrommet når gateway via container-IP
|
|
healthcheck:
|
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen(\"http://localhost:4000/health/liveliness\")"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 3
|
|
```
|
|
|
|
### 4.1 Config-generering
|
|
|
|
`config.yaml` genereres fra PG-tabellene med `scripts/generate-litellm-config.sh`.
|
|
Scriptet filtrerer bort providers med tomme API-nøkler i `.env`, slik at bare
|
|
leverandører med gyldige nøkler inkluderes. Kjør med `--restart` for å restarte
|
|
containeren etter generering.
|
|
|
|
```bash
|
|
# Generer og restart:
|
|
scripts/generate-litellm-config.sh --restart
|
|
```
|
|
|
|
## 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 samlings-node
|
|
|
|
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(),
|
|
collection_node_id UUID NOT NULL REFERENCES nodes(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_collection_month ON ai_usage_log (collection_node_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 collection_node_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 samlings-noder. Tabell med totaler per samlings-node/modell/periode. Identifiserer kostnadsdrivere.
|
|
|
|
**Samlings-node (sidebar-widget):**
|
|
Enkel tekst-indikator i 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 Budsjett per samlings-node (fase 2)
|
|
|
|
Når token-logging er på plass, kan budsjett-tak legges til:
|
|
|
|
- Budsjett lagres som JSONB-metadata på samlings-noden: `{ "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 samlings-nodens 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
|