Skip to content

Provider-Registry mit Extension Point, OpenAI-kompatibler Provider, Modellvorschläge aus Symfony AI - #14

Merged
dergel merged 6 commits into
mainfrom
feature/provider-registry
Sep 4, 2026
Merged

Provider-Registry mit Extension Point, OpenAI-kompatibler Provider, Modellvorschläge aus Symfony AI#14
dergel merged 6 commits into
mainfrom
feature/provider-registry

Conversation

@dergel

@dergel dergel commented Sep 3, 2026

Copy link
Copy Markdown
Member

Jeder Provider steht jetzt in genau einem Eintrag in ProviderRegistry — Label, benötigte Formularfelder, Default-Modelle, Modellkatalog und Platform-Factory. Service::getProviders(), getModelSuggestions() und getPlatform() delegieren dorthin, und über den Extension Point AI_PLATFORM_PROVIDERS ergänzt ein anderes AddOn einen Provider ohne Änderung an diesem AddOn. assets/profiles.js kennt dadurch keine Provider-Namen mehr: Feldsichtbarkeit, Default-Modell und Modellvorschläge kommen als JSON aus ProviderRegistry::formConfig() — die doppelte Pflege war die Ursache dafür, dass das API-Key-Feld für Ollama ausgeblendet blieb, obwohl PHP den Key längst annahm.

Neu ist der Provider OpenAI-kompatibel über symfony/ai-generic-platform (v0.6, passend zum installierten symfony/ai-platform): Chat-Completions gegen eine frei eingetragene Basis-URL, also Open WebUI, LiteLLM, vLLM, LM Studio, OpenRouter. Damit erledigt sich #9 ohne eigene Bridge-Klassen — und mit Tool-Calls, Streaming, Fehler-Mapping und Token-Usage, die der Upstream-Converter mitbringt.

Die Modellvorschläge kommen aus dem Katalog der jeweiligen Bridge, gefiltert nach den Fähigkeiten des Profiltyps (78 gepflegte Modelle statt ~16 handgeschriebener Namen). Das Modellfeld bleibt Freitext mit Datalist, weil die Kataloge Lücken haben: llama3.2-vision fehlt ganz, llava trägt kein INPUT_IMAGE.

Verifiziert: PHPStan Level 6 ohne Fehler; Registry gegen einen Header-protokollierenden Server (/v1, ohne /v1 und /v1/ landen alle auf /v1/chat/completions, Bearer gesetzt bzw. bei leerem Key keiner, Ollama unverändert auf /api/chat); Extension Point mit einem Fremd-Provider; formConfig() im echten REDAXO; die JS-Logik in sieben Fällen gegen ein nachgebautes Formular.


Dieser Text wurde durch eine KI erstellt.

Jeder Provider steht jetzt mit Label, benötigten Formularfeldern, Default-Modellen,
Modellkatalog und Platform-Factory in genau einem Eintrag in ProviderRegistry.
Service::getProviders(), getModelSuggestions() und getPlatform() delegieren dorthin und
tragen kein Provider-Wissen mehr. Über den Extension Point AI_PLATFORM_PROVIDERS kann ein
anderes AddOn einen Provider ergänzen, ohne dieses AddOn zu ändern.

assets/profiles.js kennt keine Provider-Namen mehr: Feldsichtbarkeit, Default-Modell und
Modellvorschläge kommen als JSON aus ProviderRegistry::formConfig(), das
pages/profiles.php neben dem Formular ausgibt. Genau diese doppelte Pflege war die Ursache
dafür, dass das API-Key-Feld für Ollama ausgeblendet blieb, obwohl der PHP-Teil den Key
längst annahm.

Neu ist der Provider "OpenAI-kompatibel" auf Basis von symfony/ai-generic-platform:
klassische Chat-Completions gegen eine frei eingetragene Basis-URL, wie sie Open WebUI,
LiteLLM, vLLM, LM Studio oder OpenRouter anbieten. Ein angehängtes /v1 wird abgeschnitten,
weil die Bridge ihren versionierten Pfad selbst setzt -- beide Schreibweisen landen damit
auf derselben URL.

Die Modellvorschläge stammen aus dem Katalog der jeweiligen Symfony-AI-Bridge, gefiltert
nach den Fähigkeiten, die der Profiltyp braucht; getModelSuggestions() wurde bisher von
nichts aufgerufen und pflegte eine zweite Namensliste neben der im JavaScript. Das
Modellfeld bleibt Freitext mit Datalist: die Kataloge sind nicht vollständig -- bei Ollama
fehlt llama3.2-vision ganz und llava trägt kein INPUT_IMAGE --, ein Auswahlfeld würde
funktionierende Modellnamen verbieten.
Das Modellfeld war ein Textfeld mit Datalist, die Vorschläge blieben dadurch unsichtbar,
bis man zu tippen anfing. Stattdessen steht jetzt ein Select vor dem Feld, gefüllt aus dem
Katalog des Providers für den gewählten Typ.

Ein reines Auswahlfeld ginge nicht: die Kataloge haben Lücken -- bei Ollama fehlt
llama3.2-vision ganz und llava trägt kein INPUT_IMAGE --, und der Provider
"OpenAI-kompatibel" hat gar keinen Katalog. Deshalb trägt die Auswahl als letzten Eintrag
"eigener Modellname", der das Textfeld freischaltet, und bei leerem Katalog entfällt die
Auswahl ganz, sodass nur das Textfeld bleibt.

Der Select ist das Prefix des Modellfelds -- der Core rendert es in dasselbe <dd> --, hat
kein name-Attribut und schreibt nur in das Input, das damit weiterhin das einzige an die
Spalte gebundene Feld ist. syncModelSelect() leitet den Select aus dem Input ab und nie
umgekehrt: ein gespeicherter Name, den der Katalog nicht kennt, überlebt so das Öffnen und
Speichern eines Profils, statt still durch die erste Option ersetzt zu werden.
Die Auswahlbox sah anders aus als die Boxen für Typ und Provider, weil ihr die Klasse
selectpicker fehlte. Mit ihr kommen zwei Pflichten: be_style initialisiert jedes
.selectpicker bei rex:ready und das Plugin rendert sein Markup danach nur einmal, also
muss jede Änderung an Optionen oder Wert per selectpicker('refresh') angekündigt werden.
Und das Plugin versteckt das ursprüngliche <select> selbst und wickelt es in ein
div.bootstrap-select -- die Sichtbarkeit gehört deshalb an diesen Wrapper, sonst schaltet
man etwas um, das ohnehin unsichtbar ist. Beides fällt sauber zurück, wenn das Plugin
fehlt; genau das macht die Auswahl außerhalb eines Browsers testbar.

Beim Wechsel von Typ oder Provider wird das Modell jetzt mitgezogen: ein Name aus der
vorigen Auswahl ist mit Sicherheit falsch -- gpt-4o-mini wird durch die Wahl des Typs
Embeddings kein Embedding-Modell. Hat die neue Kombination einen Katalog, tritt dessen
Default an die Stelle des alten Werts. Ein leerer Katalog bleibt unangetastet, weil der
eingetippte Name dort die einzige Quelle ist.

Der Typ "Text / Code" heißt jetzt "Text / Code / Completion" -- so nennen die
Symfony-AI-Kataloge diese Modalität.

Zwei Testskripte im Stil der übrigen unter .claude/tests/:

- provider-registry-test.php (100 Asserts) prüft die Registry gegen den echten
  REDAXO-Boot, damit ein Label als "[translate:…]" auffällt, und schickt die Bridges
  durch einen MockHttpClient. Damit sind Basis-URL-Normalisierung, Bearer-Header und
  Ollamas /api/chat ohne Netz belegt.
- model-picker-test.mjs (19 Asserts, reines node) fährt assets/profiles.js gegen ein
  handgeschriebenes Minimal-DOM.

Der Registry-Test hat gleich zwei veraltete Defaults gefunden: gemini-2.0-flash-exp und
text-embedding-004 stehen nicht mehr im Katalog der Gemini-Bridge und hätten das Formular
sofort auf "eigener Modellname" gestellt. Sie sind auf gemini-2.5-flash-image und
gemini-embedding-001 korrigiert.

Außerdem stellt der Test klar, was ein selbst eingetragener Modellname wirklich bewirkt:
AbstractModelCatalog::getModel() wirft ModelNotFoundException für einen unbekannten Namen,
bevor irgendein Request entsteht. Das Freitextfeld trägt also für den generischen Provider
mit seinem FallbackModelCatalog -- bei Ollama weist Symfony AI llama3.2-vision ab. Die
Dokumentation behauptete dort bisher das Gegenteil.

createPlatform() nimmt einen optionalen HttpClient und reicht ihn an die Bridge-Factory
durch. Das ist die Naht für eigene Header, Timeouts oder einen Proxy -- und sie ist es,
die den MockHttpClient im Test möglich macht.
Beide Bridges gibt es bei Symfony AI in 0.6, passend zum installierten Stand:
symfony/ai-open-router-platform und symfony/ai-replicate-platform (letztere zieht
symfony/ai-meta-platform mit).

Sie sind aber nicht gleichwertig. OpenRouters PlatformFactory ist nur ein dünner Aufsatz
auf der generischen Bridge mit baseUrl https://openrouter.ai/api -- dasselbe
Chat-Completions-Protokoll, dazu ein Katalog von rund 360 Modellen vieler Anbieter hinter
einem Schlüssel. Replicate ist bei Symfony AI dagegen ein Llama-Client: LlamaModelClient,
LlamaResultConverter, LlamaMessageBagNormalizer und 15 llama-*-Einträge im Katalog. Für
Bildgenerierung, wofür Replicate ansonsten bekannt ist, gibt es dort nichts. Das Label
sagt das ("nur Llama-Modelle, nur Text"), sonst wirkt die leere Modellauswahl bei den
anderen Typen wie ein Fehler.

TYPE_CAPABILITIES verlangt jetzt alle Fähigkeiten aus 'all' und mindestens eine aus 'any'.
Grund: OpenRouter beschreibt seine Katalogeinträge mit INPUT_TEXT statt INPUT_MESSAGES, und
die bisherige Regel ließ von 362 Modellen genau 2 übrig. Beide Eingabearten zu akzeptieren
hält whisper-1 weiterhin aus der Textliste heraus und ändert bei OpenAI, Anthropic, Gemini
und Ollama nichts -- 19/14/9/19 Textmodelle vor und nach der Änderung. array_intersect()
ist für diese Prüfung übrigens unbrauchbar: es vergleicht per String-Cast und wirft bei
Enum-Instanzen, was der Test sofort gezeigt hat.

Die Modellauswahl bekommt data-live-search, weil eine Liste mit 341 Einträgen nicht
scrollbar bedient wird. Die Nutzlast des Formulars wächst dadurch von 3 KB auf 17 KB.

OpenRouters @preset-Eintrag bleibt in der Liste -- ihn zu filtern wäre providerspezifische
Logik in der Registry, also genau das, was dieser Umbau beseitigt hat. Er ist ein
Platzhalter für gespeicherte Presets und kein aufrufbares Modell, deshalb sorgen die
Defaults dafür, dass ein neues Profil nie darauf landet.

Tests: 131 statt 100 Asserts. Die Modellzahlen werden als Größenordnung geprüft, nicht
exakt -- der OpenRouter-Katalog wird bei jedem Symfony-AI-Update neu generiert. Replicate
wird nicht bis zum Request getrieben: sein Client pollt eine Prediction, ein Mock müsste
diesen Zustandsautomaten nachbauen und der Test würde am Ende den Mock prüfen.
Drei direkte Anbieter, jeweils ein Composer-Paket und ein Eintrag in der Registry: gleiche
Factory-Signatur, OpenAI-förmige Completions-API, ein API-Key-Feld. Was das Formular
anbietet, entscheidet der jeweilige Katalog -- Mistral hat pixtral fürs Bildverständnis und
mistral-embed, Scaleway dasselbe in kleiner, Cerebras ausschließlich Text, weil der
Anbieter offene Modelle für schnelle Inferenz hostet und keines davon multimodal ist.
Bildgenerierung gibt es bei keinem der drei.

Der Testlauf hat dabei etwas gezeigt, das man sonst erst im Support merkt: zwei Bridges
prüfen das Format des Schlüssels im Konstruktor des ModelClient, bevor irgendetwas
gesendet wird. OpenAI verlangt "sk-", Cerebras "csk-". Ein Schlüssel vom falschen Anbieter
scheitert deshalb nicht mit einem 401, sondern mit "The API key must start with …" direkt
aus getPlatform(). Beide Prüfungen sind jetzt als Assertion festgehalten und in README und
CLAUDE.md erklärt.

Im Backend gegengeprüft: alle zehn Provider mit Label, Modellzahl für Text und
vorausgewähltem Default -- 19 OpenAI, 14 Anthropic, 9 Google, 19 Ollama, 15 Mistral, 10
Cerebras, 11 Scaleway, 341 OpenRouter, 15 Replicate, 0 für den generischen Endpunkt, der
konsequent nur das Freitextfeld zeigt. Die Nutzlast des Formulars liegt bei 19 KB.

Tests: 180 Asserts. Modellzahlen werden weiter als Größenordnung geprüft, weil die
Kataloge mit jedem Symfony-AI-Release neu erzeugt werden.
Der Skill ai-platform beschrieb noch vier Provider und nannte als Weg zu
einem neuen die vier Stellen in Service (getProviders, ein case in
getPlatform, profiles.js) -- die es so nicht mehr gibt. Jetzt: zehn
Provider, ein Eintrag in ProviderRegistry bzw. im Extension Point, plus
die Modellauswahl mit ihrem Textfeld und die Luecken der Kataloge.

Ausserdem gegen die Kataloge geprueft und in README korrigiert: o1 und
gpt-image-1 stehen nicht im OpenAI-Katalog, mxbai-embed-large nicht im
Ollama-Katalog, und die Ollama-Auswahl fuer Bildverstaendnis ist leer
(llama3.2-vision fehlt ganz, llava und qwen2.5vl ohne INPUT_IMAGE) --
sie war als verfuegbar ausgewiesen.

Weiter:

- Extension-Point-Tabelle in README: AI_PLATFORM_PROVIDERS und
  AI_PLATFORM_CHANGE_WITHDRAWN fehlten
- Das EP-Beispiel registrierte mistral, was inzwischen eingebaut ist und
  ueberschrieben wuerde -- jetzt Perplexity, samt Hinweis auf das
  Ueberschreiben. Gleiches Beispiel im Docblock von ProviderRegistry
- understandImage im Skill mit vertauschten Argumenten
- REST-Routen: die Zaehlung stand auf sechs bzw. acht, seit
  /withdrawals sind es sieben (CLAUDE.md und Changes-Skill)
- ai-platform-mcp: Verweis auf die entfernten Phase-Abschnitte und ein
  Beispiel-Scope mcp:tools:call, den es seit beta3 nicht mehr gibt
- CLAUDE.md: Bridge-Liste im Intro, provider-registry-test,
  model-picker-test und backend-page-tree-check in der Testuebersicht
@dergel
dergel merged commit 5fa9f61 into main Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant