ADR-007: Austauschbarer LLM-Provider

Warum Owlat das Vercel AI SDK mit einer Provider-Abstraktionsschicht nutzt, statt einen einzelnen LLM-Anbieter fest zu verdrahten.

  • Status: Angenommen
  • Datum: 2026-03-24

Kontext

Agent Pipeline, Knowledge Graph und das semantische Dateisystem benötigen allesamt LLM-Fähigkeiten — Textgenerierung, strukturierte Ausgaben, Embeddings und Tool Calling. Owlat muss mehrere Deployment-Szenarien unterstützen:

  1. Cloud-Nutzer, die über API-Schlüssel die besten verfügbaren Modelle (GPT-4o, Claude usw.) einsetzen möchten
  2. Selbst-Hoster, die einen vollständig offlinefähigen Betrieb mit lokalen Modellen (über Ollama, vLLM oder Ähnliches) benötigen
  3. Enterprise-Nutzer, die über interne API-Gateways mit eigenen Endpunkten routen

Die geprüften Optionen:

  1. OpenAI fest verdrahten — am einfachsten, schließt aber Selbst-Hoster aus, die Cloud-APIs nicht nutzen können oder wollen
  2. LangChain/LlamaIndex — schwergewichtige Frameworks mit großen Abhängigkeitsbäumen, komplexen Abstraktionen und Funktionen, die Owlat nicht braucht (Chains, Memory-Verwaltung, Vector-Store-Adapter)
  3. Vercel AI SDK mit Provider-Abstraktion — leichtgewichtig, bereits im Abhängigkeitsbaum vorhanden (@ai-sdk/openai), anbieterunabhängig, unterstützt strukturierte Ausgaben und Tool Calling nativ

Entscheidung

Das Vercel AI SDK als LLM-Orchestrierungsschicht einsetzen, gekapselt in einer dünnen, über Umgebungsvariablen konfigurierten Provider-Abstraktion.

LLM_PROVIDER=openai             # or: openrouter, ollama
LLM_BASE_URL=                    # for ollama: http://ollama:11434/v1 (localhost outside Docker)
LLM_API_KEY=                     # not needed for ollama; openrouter resolves its own base URL
LLM_MODEL_FAST=gpt-4o-mini       # classify / extract / guard / summarize
LLM_MODEL_CAPABLE=gpt-4o         # draft / plan
LLM_MODEL=gpt-4o                 # shared fallback when a tier var is unset
LLM_EMBEDDING_MODEL=             # optional, defaults to text-embedding-3-small

Modelle werden pro Task-Stufe ausgewählt statt als ein einzelnes Modell — die Routing-Regeln beschreibt ADR-009: Aufgabenbasiertes Model-Routing. LLM_MODEL bleibt ein gemeinsamer Fallback, wenn die stufenspezifischen Variablen nicht gesetzt sind.

Die Factory createOpenAI() des AI SDK akzeptiert einen baseURL-Parameter, wodurch jede OpenAI-kompatible API (Ollama, vLLM, LiteLLM, Azure OpenAI) ohne zusätzlichen Provider-Code funktioniert. openrouter und ollama werden vom selben OpenAI-kompatiblen Provider mit einer vorab aufgelösten Basis-URL bedient.

Kein nativer Anthropic-Provider

Natives Anthropic über @ai-sdk/anthropic ist nicht implementiert und nicht installiert — nur @ai-sdk/openai ist eine Abhängigkeit in apps/api/package.json. Der Wert anthropic für LLM_PROVIDER wird nicht gesondert behandelt: Er landet im Default-Zweig von resolveBaseURL() (apps/api/convex/lib/llmProvider.ts:56-57), der undefined zurückgibt, sodass er sich wie einfaches OpenAI verhält. Um heute Claude-Modelle zu betreiben, nutzen Sie den OpenAI-kompatiblen Provider mit Anthropics OpenAI-kompatiblem Endpunkt:

LLM_PROVIDER=openai
LLM_BASE_URL=https://api.anthropic.com/v1/
LLM_MODEL_CAPABLE=claude-sonnet-4-6
LLM_MODEL_FAST=claude-haiku-4-5-20251001

Für Ollama setzen Sie ein lokales Modell, etwa LLM_MODEL_FAST=llama3.1:8b.

Die LLM-Auflösung liegt in einem einzigen Modul, apps/api/convex/lib/llmProvider.ts, das getLLMProvider(task), getLLMProviderForUserText(task, userText), getEmbeddingModel(), getLLMConfig() und assertEmbeddingDimension() exportiert. Es liest diese Umgebungsvariablen und ruft die Factory createOpenAI() des AI SDK direkt auf, mit einer je nach LLM_PROVIDER durch resolveBaseURL() (llmProvider.ts:48-59) aufgelösten baseURL. Jeder Consumer importiert und ruft diese Funktionen direkt auf.

Konsequenzen

Ermöglicht:

  • Selbst-Hoster betreiben Ollama lokal für vollständig offlinefähige, kostenlose KI-Funktionen
  • Cloud-Nutzer wählen ihren bevorzugten Anbieter (OpenAI, OpenRouter, Claude über den OpenAI-kompatiblen Endpunkt oder jede kompatible API)
  • Enterprise-Nutzer zeigen über LLM_BASE_URL auf interne Gateways oder Proxy-Endpunkte
  • Eine einzige Konfigurationsfläche — eine Handvoll Umgebungsvariablen (LLM_PROVIDER, LLM_BASE_URL, LLM_API_KEY, die Modellstufen-Variablen und LLM_EMBEDDING_MODEL) steuert das gesamte LLM-Verhalten
  • Das AI SDK ist bereits eine Abhängigkeit (@ai-sdk/openai sowohl in apps/api als auch in apps/web)

Abwägungen:

  • Die Qualität unterscheidet sich zwischen Anbietern erheblich — lokale Modelle liefern womöglich schlechtere Klassifikationen und Entwürfe als GPT-4o oder Claude
  • Embedding-Dimensionen unterscheiden sich zwischen Modellen — Vektorindizes müssen auf die Dimensionen des gewählten Embedding-Modells konfiguriert werden
  • Keine eingebauten RAG-Chains — Retrieval-Augmented Generation ist als explizite Convex-Funktionsschritte umgesetzt (Vektorindex abfragen, Ergebnisse an den Prompt übergeben), was ausführlicher, aber besser debugbar ist
  • AI-SDK-Updates können Breaking Changes einführen, auch wenn die Abstraktionsschicht den Anwendungscode isoliert