5. Oktober 2026
OpenAI-kompatibel ist kein Protokollvertrag
Wer AWS Bedrock Rails über ein OpenAI-kompatibles Gateway anbindet, sieht zunächst eine vertraute Oberfläche: API-Base setzen, Key hinterlegen, Modell auswählen. Das ist praktisch, aber es verleitet zu einer falschen Annahme: OpenAI-kompatibel bedeutet nicht, dass jede Semantik identisch ist.
Bei einem Rails LLM mit ruby_llm achte ich besonders auf drei Stellen: die Rolle der System-Nachricht, die Bedeutung von Timeouts und den Umgang mit Zugangsdaten in aufgezeichneten HTTP-Tests. Dazu kommt eine Architekturfrage: Globale Konfiguration wird problematisch, sobald mehrere Endpoints parallel nötig sind.
Falle 1: Die System-Nachricht wird als developer gesendet
Ohne explizite Konfiguration sendet ruby_llm die System-Message mit der Rolle developer. Ein Bedrock-Gateway versteht diese Rolle nicht zwingend so, wie es für den gewünschten Prompt nötig wäre. Das Problem ist unauffällig: Der Request kann technisch erfolgreich sein, aber die Instruktionen wirken anders als erwartet.
Für einen OpenAI-kompatiblen Bedrock-Endpunkt gehört die Einstellung deshalb in den jeweiligen Context:
bedrock_context = RubyLLM.context do |config|
config.openai_api_base = ENV.fetch('BEDROCK_GATEWAY_API_BASE')
config.openai_api_key = ENV.fetch('BEDROCK_GATEWAY_API_KEY')
config.openai_use_system_role = true
endDamit wird die System-Nachricht als system gesendet. Das ist keine kosmetische Option. System-Prompts enthalten häufig Regeln für Sprache, Ausgabeformat oder den Umgang mit Eingaben. Wenn das Gateway die gesendete Rolle anders behandelt, verändert sich damit die fachliche Wirkung des Prompts.
Wichtig ist auch die Grenze dieses Wegs: Claude auf Bedrock ist nicht über den OpenAI-kompatiblen Weg erreichbar. Die Ähnlichkeit der API-Form ersetzt keine Unterstützung für jedes Modell und jeden Provider.
Falle 2: request_timeout ist nicht der Connect-Timeout
Die zweite Falle steckt im Namen. request_timeout begrenzt den Read-Timeout. Er regelt also, wie lange auf eine Antwort gewartet wird, nachdem die Verbindung besteht. Er regelt nicht, wie lange der Aufbau einer Verbindung dauern darf.
Beides sollte getrennt konfiguriert werden. Ein typisches Muster ist ein kurzer Connect-Timeout und ein bewusst gewählter Read-Timeout:
RubyLLM.configure do |config|
config.request_timeout = 60
end
Faraday.new do |faraday|
faraday.options.open_timeout = 5
faraday.options.timeout = 60
endDer relevante Faraday-Wert für den Verbindungsaufbau heißt open_timeout. Wo die Faraday-Verbindung in einer Anwendung erzeugt oder erweitert wird, muss genau dort dieser Wert gesetzt werden. Nur request_timeout zu erhöhen löst kein Problem beim Connect. Im ungünstigen Fall wartet ein Web-Request dann länger als beabsichtigt, obwohl die Gegenstelle gar nicht erreichbar ist.
Mein Rat: Diese beiden Werte getrennt dokumentieren. Sonst ist bei einem Incident nicht klar, ob gerade der Verbindungsaufbau, das Warten auf Tokens oder die eigene Infrastruktur bremst.
Falle 3: Globale Endpoint-Konfiguration und offene Keys in VCR
Eine globale openai_api_base funktioniert nur, solange es genau einen Endpoint gibt. Sobald beispielsweise ein Gateway und ein weiterer OpenAI-kompatibler Endpoint parallel verwendet werden, wird globale Konfiguration fehleranfällig. Der zuletzt gesetzte Wert gewinnt dann für alle nachfolgenden Aufrufe.
RubyLLM.context schafft hier sauberes Scoping. Jeder Context erhält seine eigene Base-URL und seine eigenen Credentials:
primary_context = RubyLLM.context do |config|
config.openai_api_base = ENV.fetch('PRIMARY_API_BASE')
config.openai_api_key = ENV.fetch('PRIMARY_API_KEY')
config.openai_use_system_role = true
end
secondary_context = RubyLLM.context do |config|
config.openai_api_base = ENV.fetch('SECONDARY_API_BASE')
config.openai_api_key = ENV.fetch('SECONDARY_API_KEY')
endDer zweite Teil betrifft Tests. VCR-Cassettes zeichnen HTTP-Requests auf. Ohne filter_sensitive_data kann der API-Key dabei im Klartext in der Cassette landen und damit im Repository.
VCR.configure do |config|
config.filter_sensitive_data('REDACTED_API_KEY') do
ENV.fetch('BEDROCK_GATEWAY_API_KEY')
end
endDen Filter sollte es für jeden verwendeten Key geben, nicht nur für den primären Endpoint. Prüfe außerdem bestehende Cassettes: Ein später hinzugefügter Filter entfernt keine Secrets aus bereits aufgezeichneten Dateien.
Grept heute einmal eure VCR-Cassettes nach Keys.