synops/docs/infra/ai_gateway.md
vegard 0a467066ba Synops v2: arkitektur, retninger og dokumentasjon
Nystart basert på arkitektonisk innsikt fra Sidelinja v1.
Koden er ny, visjon og primitiver er validert gjennom tidligere arbeid.

Inneholder:
- Komplett arkitekturdokumentasjon (docs/arkitektur.md)
- 6 vedtatte retninger (docs/retninger/)
- Alle concepts, features, proposals og erfaringer fra v1
- Server-oppsett og drift (docs/setup/)
- LiteLLM-konfigurasjon (API-nøkler via env)
- Editor.svelte referanse fra v1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 06:43:08 +01:00

12 KiB

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

-- 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

# 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 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:

# 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:

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