Ihr API-Key landete beim falschen Host — Hermes korrigiert das base_url-Routing


Endlich ist der interne Relay genehmigt. Eine Zeile in ~/.hermes/config.yaml — model.base_url — zeigt Hermes auf das Gateway Ihrer Firma, der Security-Review winkt durch, und wochenlang sieht alles gut aus: Antworten kommen zurück, Tools laufen, die Rechnung landet auf der richtigen Kostenstelle. Dann fragt jemand aus dem Netzwerk-Team eine kleine Frage: Warum hängt die Credential Ihres Relays an Traffic, der an api.vendor.example geht? Sie schauen die Config noch einmal an. Sie zeigt weiterhin Ihren Relay. Und genau das ist das Problem: Auf vier Codepfaden wurde diese Zeile nie gelesen.

Was model.base_url verspricht

Jeder Provider in Hermes hat einen kanonischen Host — https://api.x.ai/v1, den OpenRouter-Endpunkt, chatgpt.com/backend-api/codex. Ein konfiguriertes model.base_url ist die Überschreibung: „sprich nicht mit dem kanonischen Host, sprich stattdessen mit diesem“. Leute setzen das aus Gründen, bei denen es immer um Kontrolle geht:

  • ein Firmen- oder selbst gehosteter Relay, der intern loggt, Rate-Limits setzt und abrechnet;
  • ein regionaler Endpunkt, wenn die Daten in einer Jurisdiktion bleiben müssen;
  • ein Proxy vor einem Anbieter oder ein OpenAI-kompatibles Gateway, das auf mehrere Labs verteilt.

Wenn die Überschreibung verworfen wird, scheitert die Anfrage nicht laut. Sie gelingt — gegen den falschen Host, mit der Credential, die Sie für den anderen ausgestellt haben. Genau das reparieren zwei Fixes, die am 25. September 2026 gemergt wurden.

Fehler eins: vier Pfade, die die Config nie gelesen haben

PR #121614 bündelt vier getrennt gemeldete Bugs mit derselben Form — ein konfiguriertes base_url wird verworfen, und der Code erreicht stattdessen den kanonischen Host des Anbieters, womit die Anfrage und der API-Key irgendwo landen, was der Nutzer nie gewählt hat.

  • Runtime-Auflösung (xAI). Der Runtime-Resolver hat eine Shortcut-Tabelle für „einfache“ Provider. Der xAI-Eintrag in dieser Tabelle trug eine leere Default-URL, sodass die Regel, die normalerweise greift — wenn die Pool-Zeile noch den Registry-Default enthält, nutze model.base_url — für xAI nie lief, und die aus https://api.x.ai/v1 befüllte Env-Zeile jedes Mal gewann.
  • Fallback-Einträge. Wenn ein primärer Provider ausfällt und Hermes einen Fallback aktiviert, wird das base_url des Eintrags weitergereicht. Zwei Provider mit eigenen Resolver-Zweigen (OpenRouter und der Anthropic-Arm) verwarfen es dann, während alle anderen Provider es respektierten.
  • Model-Picker-Liste. Die von /model angezeigte Liste ging direkt an die kanonischen Fetchers pro Provider und fragte nie, ob Sie diesen Provider auf einen Relay gezeigt hatten, sodass der Katalog Ihres Relays unsichtbar blieb.
  • /model --provider-Validierung. Der Validierungspfad hatte denselben blinden Fleck.

Drei der vier hatten bereits Community-Fixes, die mit erhaltener Urheberschaft in die Konsolidierung übernommen wurden. Die praktische Konsequenz für alle, die einen Relay betreiben: Modell-Discovery und -Validierung prüfen jetzt den Endpunkt, den die Inferenz tatsächlich nutzt, statt eines Hosts, von dem Sie sich bewusst entfernt haben.

Fehler zwei: ein eigenes Codex-Gateway rief weiterhin chatgpt.com an

Der zweite Fix, PR #121751, ist der, der sich wie ein Security-Vorfall liest. Wenn Sie Codex über ein eigenes Gateway betreiben — HERMES_CODEX_BASE_URL oder ein Gateway, das nur über model.base_url deklariert ist —, verdrahteten mehrere Nebenpfade chatgpt.com/backend-api/codex fest und hängten dann die aufgelöste (Gateway-)Credential an die Anfrage.

Die Liste ist länger, als man erwarten würde, weil das meiste im Normalbetrieb unsichtbar ist:

  • die Context-Length-Prüfung, die bei jedem Chat-Turn läuft;
  • die Codex-Liste des /model-Pickers;
  • der Codex-Flow von hermes model;
  • der Codex-Client des Image-Plugins;
  • der auxiliäre Codex-Client;
  • die Quota-Prüfung der gepoolten Credentials;
  • /usage.

Drei Meldungen gingen am selben Morgen ein (24. September 2026) — eine zu den Chat-Pfaden, eine zum Image-Pfad, eine dazu, dass Prüfung und Picker ihre Katalog-URLs aus dem aufgelösten Base bilden sollten — und danach wies ein Reviewer auf das verbleibende Leck hin: Picker- und Image-Pfad bauten das Ziel weiterhin aus Ambient-Environment-Variablen neu auf, sodass ein Gateway, das nur über model.base_url läuft, weiterhin leckte. Direkte ChatGPT-OAuth-Nutzer waren nie betroffen; das war ein Problem selbst gehosteter Gateways.

Der Fix in einem Satz

Credential und Ziel werden jetzt als Paar aufgelöst. Der Resolver meldet den Host, an den eine gepoolte Credential tatsächlich routet, nach einer überall gleichen Precedence-Regel — Environment-Überschreibung, dann model.base_url, dann die URL der Pool-Zeile selbst — und der Guard „verweigere einen Nicht-JWT-Key“ gilt nur noch für Prüfungen gegen chatgpt.com. Ein Gateway-Key darf sein eigenes Gateway abfragen; er darf nicht mehr zum öffentlichen wandern.

So prüfen Sie Ihr eigenes Setup

Sie müssen keinen Quellcode lesen, um herauszufinden, ob Sie betroffen waren. Sie müssen wissen, welchen Host Ihr Traffic tatsächlich erreicht hat.

# was steht wirklich in Ihrer Config?
hermes config get model.base_url

# wenn Sie Codex über ein Gateway betreiben: ist die Überschreibung in dieser Shell gesetzt?
echo "$HERMES_CODEX_BASE_URL"

# und was hatte diese Shell in der Umgebung, als der Agent startete?
grep -n "base_url" ~/.hermes/config.yaml

Dann gehen Sie auf die Log-Seite, denn Config ist nicht Egress:

  1. Das Access-Log Ihres Relays ist die Grundwahrheit. Wenn Ihr Gateway den Traffic protokolliert hat, ist die Audit-Antwort ein grep. Wenn der Host des Anbieters in den Logs eines Tools auftaucht, während Ihr Relay nichts zeigt, waren Sie auf dem falschen Pfad.
  2. Prüfen Sie den Probe-Traffic, nicht nur den Chat-Traffic. Die Context-Length-Prüfung feuert bei jedem Turn, was sie zum lautesten Beweis eines Lecks macht — und zum am leichtesten übersehenen, weil sie nie im Transcript erscheint.
  3. Prüfen Sie nach einem Upgrade erneut. Beide PRs liegen derzeit nur auf main — sie merged am 25. September 2026, einen Tag nachdem der Tag v0.21.5 geschnitten wurde, sind also nicht in diesem Tag enthalten (was er enthält, steht auf der Release-Seite zu v0.21.5). Sie erreichen Installationen über das nächste Release oder ein hermes update, das main trackt.
  4. Behandeln Sie „es hat geantwortet“ als überhaupt keinen Beweis. Beide Bugs erzeugten normale, wohlgeformte Antworten. Die einzige verlässliche Prüfung ist zielseitig.

Wenn Sie model.base_url aus Gründen der Datenresidenz oder Compliance nutzen, ist das eine ausdrückliche Nachprüfung wert statt einer Annahme: Zeigen Sie auf einen Relay, der loggt, schicken Sie eine Nachricht und bestätigen Sie, wohin die Bytes gegangen sind. Wer einen Proxy vor einem Lab betreibt — das Setup aus dem Leitfaden zum Claude-Subscription-Provider —, gehört genau zu der Gruppe, die diese beiden Fixes schützen. Wenn Sie zusätzlich Modell-Discovery oder Hooks über einen eigenen Endpunkt routen, zeigt der Walkthrough zu den Provider-Plugin-Hooks, wie weit diese Überschreibung reichen soll, und die Notizen zum Config-Cache erklären, warum ein geänderter Wert erst beim nächsten Start sichtbar wird.

Was Sie mitnehmen sollten

Ein eigener Endpunkt ist keine Kosmetik-Einstellung; er ist eine Aussage darüber, wohin Ihre Daten und Ihre Credentials gehen dürfen. Das Muster hinter beiden Fixes ist dasselbe, das Sie in Ihre eigene Automatisierung mitnehmen sollten: Eine Anfrage, die erfolgreich ist, ist kein Beweis dafür, dass sie dort angekommen ist, wo Sie sie hingeschickt haben. Immer wenn ein Tool, Provider oder Plugin eine Überschreibung im base_url-Stil akzeptiert, lautet die Audit-Frage nicht „hat es funktioniert?“, sondern „welcher Host hat geantwortet, und welche Credential hat er gesehen?“. In diesem Projekt wird die Antwort jetzt einmal an einer Stelle aufgelöst, mit einer dokumentierten Precedence-Reihenfolge — und der Guard, der sich weigert, einen Gateway-Key an einen öffentlichen Host zu schicken, ist auf den Host beschränkt, für den er geschrieben wurde.