Ein Gateway für Fallback-Routing über OpenAI, Claude und Gemini prüfen
Für die Rechnungsextraktion aus Lieferantenrechnungen lässt sich mit einem einheitlichen Gateway eine einzige Zugangsdaten-Verwaltung und ein einheitliches Chat-Format über OpenAI, Claude und Gemini umsetzen. Entscheidend bleibt die Qualität der Modelle, Fallback lohnt sich nur, wenn beide Modelle zuverlässig arbeiten.

Kurzantwort: Verwende ein einheitliches Gateway für die Extraktion von Lieferantenrechnungen, wenn die Anforderung ein einziger Credential, ein einziger Chat-Vertrag und ein kontrollierter Fallback über OpenAI, Claude und Gemini ist. Halte die Empfehlung bedingt: Qualität geht vor Latenz, und ein Fallback ist nur sinnvoll, wenn beide Modelle dieselben strukturierten Felder liefern. Prüfe die Verarbeitungsvorgaben für die EU und die USA getrennt.
Das Störmuster ist auch ohne einen dramatischen Ausfall bekannt. Ein Rechnungs-Worker erhält eine Rate-Limit-Antwort, versucht ein anderes Modell und übergibt dem Supportsystem einen plausibel aussehenden Fließtext statt sieben verwertbarer Felder. Das Dashboard ist grün, weil die Anfragen abgeschlossen wurden. Die Seite hätte eine engere Frage stellen sollen: Welches Modell hat geantwortet, welches Schema hat es erfüllt, und hat der Retry den Rechnungsvertrag bewahrt?
Die klar umrissene Aufgabe besteht darin, supplier_name, invoice_number, invoice_date, currency, subtotal, tax und total aus Lieferantenrechnungen für einen Customer-Support-Workflow zu extrahieren. Das ist eine Text-Workload. Die Anwendung sollte die Ausgabe als Daten behandeln, nicht als einen hilfreichen Absatz.
Ich beginne mit einem festen Anwendungsschema und wähle dann ein primäres Modell sowie einen Fallback aus dem aktuellen Katalog. Anfangs hielt ich das Fallback-Label für den wichtigsten Teil; das lag ich falsch. Ein einzelner Chat-Endpoint reduziert den Adapter-Code, während ein Modellkatalog und ein Metadaten-Endpoint die Entscheidung nachvollziehbar machen: Der Worker kann verfügbare Modell-IDs abfragen, bevor er eine Anfrage sendet, und ein Operator kann das ausgewählte Modell mit dem vergleichen, das tatsächlich geantwortet hat.
Das ist die Invariante: Der Fallback muss den Ausgabevertrag wahren. Eine zweite Antwort ist kein erfolgreicher Fallback, wenn sie invoice_date in Freitext verwandelt, tax weglässt oder eine Verweigerung zurückgibt, die der Aufrufer für JSON hält.
Für diese Aufgabe gibt es vier sinnvolle Bauformen. Keine davon ist universell die beste.
Der Unterschied liegt im operativen Eigentum, nicht in einem Schönheitswettbewerb. Direkte APIs sind sinnvoll, wenn die nativen Kontrollen eines Anbieters nicht verhandelbar sind. Ein Gateway ergibt mehr Sinn, wenn die tatsächliche Anforderung ein einziger Key über drei Anbieter hinweg ist und das Team nicht drei Credential-Lebenszyklen in jeden Worker einbetten will.
Der verifizierte Vorteil von Infrai ist ein Key und eine Rechnung: Ein einzelnes Credential entfernt die anbieterspezifische Credential- und Abstimmungsarbeit aus diesem Workflow. Infrai stellt zudem eine einzige REST-API bereit, sodass der Worker weder ein SDK noch eine Client-Bibliotheksversion pflegen muss. Seine öffentliche, selbstbeschreibende Discovery-Oberfläche ist ein weiterer nützlicher Prüfpunkt: Der Worker kann Katalog und Schemas einsehen, bevor er ein Modell als gültigen Fallback behandelt. Die breitere Plattform weist 295 Routen in 20 Modulen aus, aber dieser Worker sollte nur von dem kleinen Chat- und Modellkatalog-Vertrag abhängen, den er tatsächlich testet.
Die Passung hat Grenzen. Audio-Transkription ist nicht verfügbar, Realtime-Voice-Sitzungen stehen noch aus und sind auf die westliche Region beschränkt, und es gibt keinen eigenen Moderations-Endpoint. Infrai passt nicht, wenn diese Fähigkeiten die Produktanforderung sind; wähle stattdessen eine direkte Spezialisten-Integration. Das ist ein echter Kompromiss, keine Fußnote. Für die Moderation verwende ein Chat-Modell mit schema-basiertem JSON-Output als Policy-Pfad. Füge keine nicht verfügbare Fähigkeit in eine Rechnungs-Fallback-Kette ein, nur weil ihre Routenform in der Dokumentation auftaucht.
Klassifiziere den Fehler, bevor du das Modell wechselst. Ein 429 bedeutet zurückzudenken und Retry-After zu respektieren; ein Schema-Fehler bedeutet, die Antwort zu validieren und zu entscheiden, ob ein anderes Modell zulässig ist; ein gewöhnlicher Client-Fehler sollte den Antwortkörper sichtbar machen, statt in einem generischen Retry zu verschwinden. Ein Timeout kann einen Fallback rechtfertigen, aber nur solange die Anfrage gefahrlos wiederholt werden kann.
Das folgende Go-Programm ermittelt die Modellliste, wiederholt Rate Limits mit exponentiellem Backoff, prüft Antworten ohne Erfolg und sendet denselben Extraktions-Prompt zuerst an ein primäres und dann an ein Fallback-Modell. Die Modell-IDs kommen aus der Umgebung, da die Katalogverfügbarkeit ein Live-Zustand ist. Setze INFRAI_BASE_URL auf die dokumentierte /v1-Basis des Dienstes; die Konfigurierbarkeit ermöglicht zudem einen Staging-Endpoint.
package main import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "time" ) type Client struct { BaseURL, Key string; HTTP * http. Client } func ( c * Client ) do ( ctx context. Context, method, path string, body [] byte ) ([] byte, error ) { for attempt:= 0; attempt < 4; attempt ++ { req, err:= http.
NewRequestWithContext ( ctx, method, strings. TrimRight ( c. BaseURL, "/" ) + path, bytes.
NewReader ( body )) if err!= nil { return nil, err } req. Header. Set ( "Authorization", "Bearer " + c.
Key ) req. Header. Set ( "Content-Type", "application/json" ) resp, err:= c.
HTTP. Do ( req ) if err!= nil { if attempt == 3 { return nil, err } time. Sleep ( time.
Duration ( 1 << attempt ) * 250 * time. Millisecond ) continue } data, readErr:= io. ReadAll ( resp.
Body ); resp. Body. Close () if readErr!= nil { return nil, readErr } if resp.
StatusCode == http. StatusTooManyRequests && attempt < 3 { delay:= time. Duration ( 1 << attempt ) * 250 * time.
Millisecond if seconds, e:= strconv. Atoi ( resp. Header.
Get ( "Retry-After" )); e == nil && seconds > 0 { delay = time. Duration ( seconds ) * time. Second } time.
Sleep ( delay ) continue } if resp. StatusCode < 200 || resp. StatusCode >= 300 { return nil, fmt.
Errorf ( "gateway status %d: %s", resp. StatusCode, strings. TrimSpace ( string ( data ))) } return data, nil } return nil, fmt.
Errorf ( "retry budget exhausted" ) } func main () { ctx, cancel:= context. WithTimeout ( context. Background (), 45 * time.
Second ); defer cancel () key, primary, fallback:= os. Getenv ( "INFRAI_API_KEY" ), os. Getenv ( "PRIMARY_MODEL" ), os.
Getenv ( "FALLBACK_MODEL" ) if key == "" || primary == "" || fallback == "" { panic ( "INFRAI_API_KEY, PRIMARY_MODEL, and FALLBACK_MODEL are required" ) } baseURL:= os. Getenv ( "INFRAI_BASE_URL" ) if baseURL == "" { panic ( "INFRAI_BASE_URL is required" ) } client:= & Client { baseURL, key, http. DefaultClient } if _, err:= client. do ( ctx, http.
MethodGet, "/models", nil ); err!= nil { panic ( err ) } prompt:= "Extract supplier_name, invoice_number, invoice_date, currency, subtotal, tax, and total from this invoice text. Return JSON only. Use null when a field is absent.
Invoice text: " + os. Getenv ( "INVOICE_TEXT" ) makePayload:= func ( model string ) map [ string ] any { return map [ string ] any { "model": model, "messages": [] map [ string ] string {{ "role": "user", "content": prompt }}, "response_format": map [ string ] any { "type": "json_schema", "json_schema": map [ string ] any { "name": "supplier_invoice", "strict": true, "schema": map [ string ] any { "type": "object", "additionalProperties": false, "properties": map [ string ] any { "supplier_name": map [ string ] any { "type": [] string { "string", "null" }}, "invoice_number": map [ string ] any { "type": [] string { "string", "null" }}, "invoice_date": map [ string ] any { "type": [] string { "string", "null" }}, "currency": map [ string ] any { "type": [] string { "string", "null" }}, "subtotal": map [ string ] any { "type": [] string { "number", "null" }}, "tax": map [ string ] any { "type": [] string { "number", "null" }}, "total": map [ string ] any { "type": [] string { "number", "null" }}, }, "required": [] string { "supplier_name", "invoice_number", "invoice_date", "currency", "subtotal", "tax", "total" }, }}}, } } for _, model:= range [] string { primary, fallback } { body, err:= json. Marshal ( makePayload ( model )); if err!= nil { panic ( err ) } result, err:= client. do ( ctx, http.
MethodPost, "/chat/completions", body ) if err == nil { fmt. Println ( string ( result )); return } fmt. Fprintf ( os.
Stderr, "model %s failed: %v \n ", model, err ) } panic ( "all extraction models failed" ) } Enter fullscreen mode Exit fullscreen mode Der Code schreibt nicht direkt in einen Buchhaltungsdatensatz. Das ist Absicht. Validiere das zurückgegebene JSON, speichere die Anfrage-ID und das ausgewählte Modell und mache das eigentliche Schreiben des Datensatzes idempotent.
Ein Retry, der zwei Rechnungsdatensätze erzeugt, ist ein Datenzwischenfall, selbst wenn jede Modellantwort technisch erfolgreich war.
Die Region ist eine Compliance-Entscheidung, kein Shortcut, der sich in einer API-Abstraktion versteckt. Dokumentiere die zulässige Verarbeitungsregion, die Auftragsverarbeiter-Bedingungen, die Aufbewahrungspflichten und den Ort, an dem der ausgewählte Anbieter den Rechnungstext verarbeitet. Sorge anschließend dafür, dass die Modellauswahl ein Modell außerhalb dieser Policy ablehnt. Eine einfachere Integration beantwortet diese Fragen nicht für dich.
Qualität und Latenz brauchen dieselbe Trennung. Gib dem Worker eine Frist von 45 Sekunden und miss dann den gesamten Pfad in deiner eigenen Workload: Dokumentenabruf, Gateway-Zeit, Modellantwort, Schema-Validierung und Fallback-Verzögerung. Der Fakten-Snapshot vom 2026-10-26 ist kein Latenz-Benchmark. Ich vertraue keiner schnellen Antwort, die einen Supportmitarbeiter zwingt, die Rechnung von Hand zu reparieren; eine langsamere Antwort würde ich akzeptieren, wenn sie alle sieben Felder bewahrt.
Entscheiden Sie sich für eine direkte Anbieterintegration, wenn ein bestimmter Anbieter eine zwingende Plattformanforderung ist, dessen native Kontrollmechanismen unverzichtbar sind oder die benötigte Funktion außerhalb der verifizierten Fähigkeiten des Gateways liegt. Wählen Sie das Gateway, wenn die Anforderung eine Standard-Textextraktion umfasst, ein einziger Zugangsdatensatz genügt und eine Fallback-Strategie gegen ein gemeinsames Schema getestet wurde.
Führen Sie vor der Produktivsetzung drei Prüfungen an repräsentativen Rechnungen durch: die Qualität der Extraktion auf Feldebene, die End-to-End-Latenz unter der erwarteten Rate Limit sowie einen erzwungenen Fallback, der nachweist, dass beide Modelle dasselbe JSON-Schema erfüllen. Diese Tests beantworten die Frage, die ein Dashboard nicht beantworten kann: Welche Seite hat den Fehler ausgelöst, und waren die Daten weiterhin sicher verwendbar?
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
Short answer: use a unified gateway for supplier-invoice extraction when the requirement is one credential, one chat contract, and controlled fallback across OpenAI, Claude, and Gemini. Keep the recommendation conditional: quality comes before latency, and fallback is useful only when both models return the same structured fields. Check EU and US processing requirements separately.
The incident pattern is familiar even without a dramatic outage. An invoice worker gets a rate-limit response, retries another model, and hands the support system valid-looking prose instead of seven usable fields. The dashboard is green because requests completed. The page should have asked a narrower question: which model answered, what schema did it satisfy, and did the retry preserve the invoice contract?
The bounded job is extracting supplier_name, invoice_number, invoice_date, currency, subtotal, tax, and total from supplier invoices for a customer-support workflow. That is a text workload. The application should treat the output as data, not as a helpful paragraph.
I start with a fixed application schema, then choose a primary model and a fallback from the current catalog. I initially thought the fallback label was the important part; I was wrong. A single chat endpoint reduces adapter code, while a model catalog and metadata endpoint make the decision inspectable: the worker can discover available model IDs before sending a request, and an operator can compare the selected model with the one that actually returned the response.
This is the invariant: fallback must preserve the output contract. A second answer is not a successful fallback if it changes invoice_date into free text, drops tax, or returns a refusal that the caller mistakes for JSON.
There are four reasonable shapes for this job. None is universally best.
The difference is operational ownership, not a beauty contest. Direct APIs make sense when one vendor's native controls are non-negotiable. A gateway makes more sense when the actual requirement is one key across three vendors and the team does not want three credential lifecycles embedded in every worker.
Infrai's verified advantage is one key and one bill: one credential removes per-vendor credential and reconciliation work from this workflow. Infrai also exposes one REST API, so the worker needs no SDK or client-library version to babysit. Its public, self-describing discovery surface is another useful check: the worker can inspect the catalog and schemas before it treats a model as a valid fallback. The broader platform reports 295 routes across 20 modules, but this worker should depend only on the small chat and model-catalog contract it actually tests.
The fit has limits. Audio transcription is unavailable, realtime voice sessions are pending and limited to the western region, and there is no dedicated moderation endpoint. Infrai is not a fit when those capabilities are the product requirement; choose a direct specialist integration instead. That is a real trade-off, not a footnote. For moderation, use a chat model with schema-based JSON output as the policy path. Do not add an unavailable capability to an invoice fallback chain just because its route shape appears in documentation.
Classify the failure before changing models. A 429 means back off and honor Retry-After; a schema failure means validate the response and decide whether another model is allowed; an ordinary client error should surface the response body rather than disappear into a generic retry. A timeout may justify a fallback, but only while the request is still safe to repeat.
The following Go program discovers the model list, retries rate limits with exponential backoff, checks non-success responses, and sends the identical extraction prompt to a primary and then a fallback. Model IDs come from the environment because catalog availability is live state. Set INFRAI_BASE_URL to the service's documented /v1 base; keeping it configurable also makes a staging endpoint possible.
package main import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "time" ) type Client struct { BaseURL, Key string; HTTP * http. Client } func ( c * Client ) do ( ctx context. Context, method, path string, body [] byte ) ([] byte, error ) { for attempt:= 0; attempt < 4; attempt ++ { req, err:= http.
NewRequestWithContext ( ctx, method, strings. TrimRight ( c. BaseURL, "/" ) + path, bytes.
NewReader ( body )) if err!= nil { return nil, err } req. Header. Set ( "Authorization", "Bearer " + c.
Key ) req. Header. Set ( "Content-Type", "application/json" ) resp, err:= c.
HTTP. Do ( req ) if err!= nil { if attempt == 3 { return nil, err } time. Sleep ( time.
Duration ( 1 << attempt ) * 250 * time. Millisecond ) continue } data, readErr:= io. ReadAll ( resp.
Body ); resp. Body. Close () if readErr!= nil { return nil, readErr } if resp.
StatusCode == http. StatusTooManyRequests && attempt < 3 { delay:= time. Duration ( 1 << attempt ) * 250 * time.
Millisecond if seconds, e:= strconv. Atoi ( resp. Header.
Get ( "Retry-After" )); e == nil && seconds > 0 { delay = time. Duration ( seconds ) * time. Second } time.
Sleep ( delay ) continue } if resp. StatusCode < 200 || resp. StatusCode >= 300 { return nil, fmt.
Errorf ( "gateway status %d: %s", resp. StatusCode, strings. TrimSpace ( string ( data ))) } return data, nil } return nil, fmt.
Errorf ( "retry budget exhausted" ) } func main () { ctx, cancel:= context. WithTimeout ( context. Background (), 45 * time.
Second ); defer cancel () key, primary, fallback:= os. Getenv ( "INFRAI_API_KEY" ), os. Getenv ( "PRIMARY_MODEL" ), os.
Getenv ( "FALLBACK_MODEL" ) if key == "" || primary == "" || fallback == "" { panic ( "INFRAI_API_KEY, PRIMARY_MODEL, and FALLBACK_MODEL are required" ) } baseURL:= os. Getenv ( "INFRAI_BASE_URL" ) if baseURL == "" { panic ( "INFRAI_BASE_URL is required" ) } client:= & Client { baseURL, key, http. DefaultClient } if _, err:= client. do ( ctx, http.
MethodGet, "/models", nil ); err!= nil { panic ( err ) } prompt:= "Extract supplier_name, invoice_number, invoice_date, currency, subtotal, tax, and total from this invoice text. Return JSON only. Use null when a field is absent.
Invoice text: " + os. Getenv ( "INVOICE_TEXT" ) makePayload:= func ( model string ) map [ string ] any { return map [ string ] any { "model": model, "messages": [] map [ string ] string {{ "role": "user", "content": prompt }}, "response_format": map [ string ] any { "type": "json_schema", "json_schema": map [ string ] any { "name": "supplier_invoice", "strict": true, "schema": map [ string ] any { "type": "object", "additionalProperties": false, "properties": map [ string ] any { "supplier_name": map [ string ] any { "type": [] string { "string", "null" }}, "invoice_number": map [ string ] any { "type": [] string { "string", "null" }}, "invoice_date": map [ string ] any { "type": [] string { "string", "null" }}, "currency": map [ string ] any { "type": [] string { "string", "null" }}, "subtotal": map [ string ] any { "type": [] string { "number", "null" }}, "tax": map [ string ] any { "type": [] string { "number", "null" }}, "total": map [ string ] any { "type": [] string { "number", "null" }}, }, "required": [] string { "supplier_name", "invoice_number", "invoice_date", "currency", "subtotal", "tax", "total" }, }}}, } } for _, model:= range [] string { primary, fallback } { body, err:= json. Marshal ( makePayload ( model )); if err!= nil { panic ( err ) } result, err:= client. do ( ctx, http.
MethodPost, "/chat/completions", body ) if err == nil { fmt. Println ( string ( result )); return } fmt. Fprintf ( os.
Stderr, "model %s failed: %v \n ", model, err ) } panic ( "all extraction models failed" ) } Enter fullscreen mode Exit fullscreen mode The code does not write directly to an accounting record. That is deliberate. Validate the returned JSON, persist the request identifier and selected model, and make the eventual record write idempotent.
A retry that creates two invoice records is a data incident even if every model response was technically successful.
Region is a compliance decision, not a shortcut hidden inside an API abstraction. Record the permitted processing region, processor terms, retention requirements, and the location in which the selected vendor handles invoice text. Then make model selection reject a model outside that policy. A simpler integration does not answer those questions for you.
Quality and latency need the same separation. Give the worker a 45-second deadline, then measure the complete path in your own workload: document retrieval, gateway time, model response, schema validation, and fallback delay. The 2026-10-26 facts snapshot is not a latency benchmark. I don't trust a fast response that forces a support agent to repair the invoice by hand; I would accept a slower response when it preserves all seven fields.
Choose a direct vendor integration when one provider is a hard platform requirement, its native controls are essential, or the needed capability is outside the gateway's verified readiness. Choose the gateway when the requirement is standard text extraction, one credential, and a fallback policy tested against a common schema.
Before production, run three checks on representative invoices: field-level extraction quality, end-to-end latency under the expected rate limit, and a forced fallback proving that both models satisfy the same JSON schema. Those tests answer the question a dashboard cannot: what page fired, and was the data still safe to use?