把推論端點接上去,表面上只是填一組金鑰跟一個模型名稱。實際會出問題的是端點選哪一個:Google 同時提供原生 API 與 OpenAI 相容層,兩者填進去都連得上,差別要到多輪工具呼叫才會顯現。以下拆開組態內容、轉譯路徑,以及一個把金鑰填成「看起來像隨便填」而失敗的案例。
金鑰放 .env,其餘放 config.yaml:
model:
provider: gemini
default: gemini-3.5-flash-lite
base_url: https://generativelanguage.googleapis.com/v1beta
# .env
GOOGLE_API_KEY=<在 AI Studio 產生,由使用者自行填入>
金鑰由本人到 AI Studio 產生後自行寫入,全程在本機完成。hermes config set 會判定鍵名,屬於密鑰的寫進 .env,其餘寫進 config.yaml。
base_url 結尾的 /v1beta 與 /v1beta/openai/ 各自對應一條程式碼路徑。
Hermes 對 Gemini 有一支專用的轉譯層 agent/gemini_native_adapter.py,它的檔頭直接寫了理由:
Google 的 OpenAI 相容端點對 Hermes 的多輪 agent 與工具迴圈一直不穩定(認證反覆、tool call 重放的異常、thought signature 的要求)。原生 Gemini API 是正規路徑,可以完全避開相容層
判定方式很直接,base_url 含有 generativelanguage.googleapis.com 且結尾不是 /openai,就視為原生端點。
v0.21.0 對兩種填錯做了防禦:
base_url 結尾是 /openai 時,會把這一段切掉google/ 或 gemini/ 前綴時,會把前綴剝掉這兩個值目前只剩閱讀上的意義,填對仍然比較好,因為組態檔是人在讀的。
provider: gemini 在 Hermes 內部仍維持 api_mode='chat_completions',主迴圈照舊用 OpenAI 形狀的訊息流,轉譯只發生在傳輸層。
agent 的核心邏輯只看到統一的介面,下面接哪一家由 provider 層決定
| OpenAI 形狀 | Gemini 原生 |
|---|---|
messages[] |
contents[] |
| system 角色的訊息 | systemInstruction |
tools[].function |
tools[].functionDeclarations[] |
tool_choice: "auto" |
functionCallingConfig.mode: "AUTO" |
tool_choice: "required" |
functionCallingConfig.mode: "ANY" |
tool_choice 指定單一函式 |
mode: "ANY" 加 allowedFunctionNames |

欄位改個名字是容易的部分。實際的麻煩在三個地方:
一、角色必須嚴格交替。 Gemini 對連續兩則同角色的 contents 回傳 HTTP 400,所以轉譯時得把相鄰的同角色內容合併。串流中斷或配額耗盡導致模型那一輪中斷時,歷史裡會出現使用者訊息緊接在工具結果後面,轉譯層在中間插一則佔位文字「先前的回應在完成前被中斷」,讓請求維持交替合法。
二、工具 schema 走白名單。 Gemini 的 FunctionDeclaration.parameters 只接受 OpenAPI 3.0 的一個子集,Hermes 只保留清單內的鍵:
type、format、enum、properties、required、items、minimum、maximum、pattern、anyOf 等二十餘個$schema、additionalProperties、oneOf、allOf 這些清單外的寫法,在送出前靜默消失實際生效的是轉譯後送出的那一份 schema
三、thought signature 是必填欄位。 Gemini 3 的思考模型在重放歷史時要求每個 functionCall 帶 thoughtSignature,缺少時回傳 400 INVALID_ARGUMENT。跨 provider 的 fallback 產生的工具呼叫紀錄缺少這個欄位,Hermes 的處理是填入字串 skip_thought_signature_validator 當佔位值。
還有一個預設值的差異會直接影響輸出品質。省略 max_tokens 時:
Hermes 因此在呼叫端省略該欄位時主動填入 65535,也就是目前各 Gemini 文字模型共通的輸出上限。
hermes model 對免費層金鑰的處置hermes model 是互動式的 provider 設定流程。選 Gemini 並填入金鑰後,它會先發一個 maxOutputTokens: 1 的最小請求探測金鑰等級,再決定是否把 Gemini 存成預設 provider。
| 探測結果 | 判定 | 設定流程 |
|---|---|---|
回應標頭 x-ratelimit-limit-requests-per-day 小於等於 1000 |
free | 中止,組態維持原狀 |
| 該標頭大於 1000 | paid | 通過 |
無該標頭,HTTP 429 且內容含 free_tier |
free | 中止,組態維持原狀 |
| 無該標頭,HTTP 2xx | paid | 通過 |
| 連線失敗或其餘狀態碼 | unknown | 通過 |
判定為 free 時印出的理由是 Hermes 每個使用者回合通常發出 3 到 10 次 API 呼叫(工具迭代加上輔助任務),免費層的每日上限在幾則訊息內就會用完,一個 agent session 的用量高於這個上限。
以 gemini-3.5-flash-lite 送出最小請求,Google 回 HTTP 200 而且完全不帶 x-ratelimit-* 標頭,落在「無標頭且 2xx」這一條,判定為 paid。探測函式的預設模型是 gemini-3.7-flash,該模型當下回 503 表示負載過高,落到 unknown,同樣放行。
標頭缺席時,一次成功的請求就會被判定為付費層,這道閘門實際攔下的是已經回 429 的金鑰
配額還有剩的金鑰會在設定這一關通過。 實際會遇到的是執行期的 429,錯誤處理會在訊息後面附上同一段免費層說明。
金鑰格式另有一條時程,Google 官方論壇的公告分兩階段:
| 日期 | 範圍 |
|---|---|
| 2026-06-19 | 拒絕未設限的 Standard 金鑰,已加上限制的仍可使用 |
| 2026 年 9 月 | 拒絕所有 Standard 金鑰,改用綁定 Google Cloud 服務帳戶的 auth key |
AI Studio 目前簽發的就是新的 auth key,字首從 AIza 換成 AQ.,實測可正常呼叫。這一側遇到的錯誤是 401 並要求改用 OAuth 2 存取權杖,訊息本身會把人導向 OAuth 方向。
| 段落 | 內容 |
|---|---|
| 現象 | 接一個地端的 OpenAI 相容端點,api_key 已填值,執行時回報 No usable credentials found for custom |
| 根本原因 | 該端點接受任何字串當金鑰,於是填的是 dummy。Hermes 這一側 hermes_cli/auth.py 的 has_usable_secret() 有兩道檢查,長度至少 4 個字元,以及不在佔位字串清單內。清單含 dummy、none、null、placeholder、changeme、example 等,dummy 被判定為未填 |
| 修正方式 | 改填一串隨機字元 |
| 迴歸測試 | 換過金鑰後執行 hermes doctor,看該 provider 的憑證狀態是否由缺少憑證轉為可用 |
同一個區域還有一個更早的缺陷:Desktop 的設定流程在 api_key 留空時,會在憑證池留下 api_key: "none" 的殘留項並卡在最高優先序,之後從介面填入的正確值排在它後面(NousResearch/hermes-agent #38799)。當時的處理是改用 hermes model 從 CLI 重設。
v0.21.0 已對這一類情況加上本機回送端點的豁免,127.0.0.1 之類的本機服務即使金鑰過短也會放行(迴歸測試 #86864)。長度下限與佔位字串清單仍然生效。
接受任何字串的端點,配上會擋佔位字串的客戶端,湊在一起會產生一個錯誤訊息指向別處的失敗。
地端跑起來的 OpenAI 相容端點多半接受任意 api_key,於是「隨便填一個」是常見做法。Hermes 這一側則假設能填進去的都是真憑證,會把明顯是佔位符的值視為留空。兩邊各自都合理。
結果是「隨便填一個值」跟「填一個看起來像隨便填的值」行為不同。填 dummy 得到的錯誤訊息是找不到可用憑證,排查方向因此歪到金鑰是否寫進檔案、路徑是否正確這些地方,而檔案一直是對的。
錯誤訊息指向的層級決定排查方向,指錯層級時會在對的檔案上繞很久
更換推論端點,確認其餘職責維持原狀,以及一個把 agent 當成模型端點呼叫的反例。