Provider-Registry mit Extension Point, OpenAI-kompatibler Provider, Modellvorschläge aus Symfony AI - #14
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Jeder Provider steht jetzt in genau einem Eintrag in
ProviderRegistry— Label, benötigte Formularfelder, Default-Modelle, Modellkatalog und Platform-Factory.Service::getProviders(),getModelSuggestions()undgetPlatform()delegieren dorthin, und über den Extension PointAI_PLATFORM_PROVIDERSergänzt ein anderes AddOn einen Provider ohne Änderung an diesem AddOn.assets/profiles.jskennt dadurch keine Provider-Namen mehr: Feldsichtbarkeit, Default-Modell und Modellvorschläge kommen als JSON ausProviderRegistry::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 installiertensymfony/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-visionfehlt ganz,llavaträgt keinINPUT_IMAGE.Verifiziert: PHPStan Level 6 ohne Fehler; Registry gegen einen Header-protokollierenden Server (
/v1, ohne/v1und/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.