fix: suppress Authorization: Bearer for Gemini provider to prevent HTTP 400 (#7893)
This commit is contained in:
committed by
Teknium
parent
8a59f8a9ed
commit
994faacce8
@@ -745,6 +745,15 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
|
|||||||
from hermes_cli.models import copilot_default_headers
|
from hermes_cli.models import copilot_default_headers
|
||||||
|
|
||||||
extra["default_headers"] = copilot_default_headers()
|
extra["default_headers"] = copilot_default_headers()
|
||||||
|
elif "generativelanguage.googleapis.com" in base_url.lower():
|
||||||
|
# Google's OpenAI-compatible endpoint only accepts x-goog-api-key.
|
||||||
|
# Passing api_key= causes the SDK to inject Authorization: Bearer,
|
||||||
|
# which Google rejects with HTTP 400 "Multiple authentication
|
||||||
|
# credentials received". Use a placeholder for api_key and pass
|
||||||
|
# the real key via x-goog-api-key header instead.
|
||||||
|
# Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
|
||||||
|
extra["default_headers"] = {"x-goog-api-key": api_key}
|
||||||
|
api_key = "not-used"
|
||||||
return OpenAI(api_key=api_key, base_url=base_url, **extra), model
|
return OpenAI(api_key=api_key, base_url=base_url, **extra), model
|
||||||
|
|
||||||
creds = resolve_api_key_provider_credentials(provider_id)
|
creds = resolve_api_key_provider_credentials(provider_id)
|
||||||
@@ -766,6 +775,15 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
|
|||||||
from hermes_cli.models import copilot_default_headers
|
from hermes_cli.models import copilot_default_headers
|
||||||
|
|
||||||
extra["default_headers"] = copilot_default_headers()
|
extra["default_headers"] = copilot_default_headers()
|
||||||
|
elif "generativelanguage.googleapis.com" in base_url.lower():
|
||||||
|
# Google's OpenAI-compatible endpoint only accepts x-goog-api-key.
|
||||||
|
# Passing api_key= causes the SDK to inject Authorization: Bearer,
|
||||||
|
# which Google rejects with HTTP 400 "Multiple authentication
|
||||||
|
# credentials received". Use a placeholder for api_key and pass
|
||||||
|
# the real key via x-goog-api-key header instead.
|
||||||
|
# Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
|
||||||
|
extra["default_headers"] = {"x-goog-api-key": api_key}
|
||||||
|
api_key = "not-used"
|
||||||
return OpenAI(api_key=api_key, base_url=base_url, **extra), model
|
return OpenAI(api_key=api_key, base_url=base_url, **extra), model
|
||||||
|
|
||||||
return None, None
|
return None, None
|
||||||
@@ -1611,6 +1629,15 @@ def resolve_provider_client(
|
|||||||
from hermes_cli.models import copilot_default_headers
|
from hermes_cli.models import copilot_default_headers
|
||||||
|
|
||||||
headers.update(copilot_default_headers())
|
headers.update(copilot_default_headers())
|
||||||
|
elif "generativelanguage.googleapis.com" in base_url.lower():
|
||||||
|
# Google's OpenAI-compatible endpoint only accepts x-goog-api-key.
|
||||||
|
# Passing api_key= causes the OpenAI SDK to inject Authorization: Bearer,
|
||||||
|
# which Google rejects with HTTP 400 "Multiple authentication credentials
|
||||||
|
# received". Use a placeholder for api_key and pass the real key via
|
||||||
|
# x-goog-api-key header instead.
|
||||||
|
# Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
|
||||||
|
headers["x-goog-api-key"] = api_key
|
||||||
|
api_key = "not-used"
|
||||||
|
|
||||||
client = OpenAI(api_key=api_key, base_url=base_url,
|
client = OpenAI(api_key=api_key, base_url=base_url,
|
||||||
**({"default_headers": headers} if headers else {}))
|
**({"default_headers": headers} if headers else {}))
|
||||||
|
|||||||
21
run_agent.py
21
run_agent.py
@@ -1044,6 +1044,16 @@ class AIAgent:
|
|||||||
}
|
}
|
||||||
elif "portal.qwen.ai" in effective_base.lower():
|
elif "portal.qwen.ai" in effective_base.lower():
|
||||||
client_kwargs["default_headers"] = _qwen_portal_headers()
|
client_kwargs["default_headers"] = _qwen_portal_headers()
|
||||||
|
elif "generativelanguage.googleapis.com" in effective_base.lower():
|
||||||
|
# Google's OpenAI-compatible endpoint only accepts x-goog-api-key.
|
||||||
|
# The OpenAI SDK auto-injects Authorization: Bearer when api_key= is
|
||||||
|
# set to a real value, causing HTTP 400 "Multiple authentication
|
||||||
|
# credentials received". Pass a placeholder so the SDK does not
|
||||||
|
# emit Bearer, and carry the real key via x-goog-api-key instead.
|
||||||
|
# Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
|
||||||
|
real_key = client_kwargs["api_key"]
|
||||||
|
client_kwargs["api_key"] = "not-used"
|
||||||
|
client_kwargs["default_headers"] = {"x-goog-api-key": real_key}
|
||||||
else:
|
else:
|
||||||
# No explicit creds — use the centralized provider router
|
# No explicit creds — use the centralized provider router
|
||||||
from agent.auxiliary_client import resolve_provider_client
|
from agent.auxiliary_client import resolve_provider_client
|
||||||
@@ -5102,6 +5112,17 @@ class AIAgent:
|
|||||||
self._client_kwargs["default_headers"] = {"User-Agent": "KimiCLI/1.30.0"}
|
self._client_kwargs["default_headers"] = {"User-Agent": "KimiCLI/1.30.0"}
|
||||||
elif "portal.qwen.ai" in normalized:
|
elif "portal.qwen.ai" in normalized:
|
||||||
self._client_kwargs["default_headers"] = _qwen_portal_headers()
|
self._client_kwargs["default_headers"] = _qwen_portal_headers()
|
||||||
|
elif "generativelanguage.googleapis.com" in normalized:
|
||||||
|
# Google's endpoint rejects Bearer tokens; use x-goog-api-key instead.
|
||||||
|
# Swap the real key out of api_key and into the header so the OpenAI
|
||||||
|
# SDK does not emit Authorization: Bearer.
|
||||||
|
# Fixes: https://github.com/NousResearch/hermes-agent/issues/7893
|
||||||
|
real_key = self._client_kwargs.get("api_key", "")
|
||||||
|
if real_key and real_key != "not-used":
|
||||||
|
self._client_kwargs["api_key"] = "not-used"
|
||||||
|
self._client_kwargs["default_headers"] = {
|
||||||
|
"x-goog-api-key": real_key or self._client_kwargs.get("api_key", ""),
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
self._client_kwargs.pop("default_headers", None)
|
self._client_kwargs.pop("default_headers", None)
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,58 @@ class TestGeminiAgentInit:
|
|||||||
assert agent.api_mode == "chat_completions"
|
assert agent.api_mode == "chat_completions"
|
||||||
assert agent.provider == "gemini"
|
assert agent.provider == "gemini"
|
||||||
|
|
||||||
|
def test_gemini_uses_x_goog_api_key_not_bearer(self, monkeypatch):
|
||||||
|
"""Regression test for issue #7893.
|
||||||
|
|
||||||
|
When provider=gemini, the OpenAI client must be constructed with
|
||||||
|
api_key='not-used' and default_headers={'x-goog-api-key': real_key}.
|
||||||
|
This prevents the SDK from injecting Authorization: Bearer, which
|
||||||
|
Google's endpoint rejects with HTTP 400.
|
||||||
|
"""
|
||||||
|
monkeypatch.setenv("GOOGLE_API_KEY", "AIzaSy_REAL_KEY")
|
||||||
|
real_key = "AIzaSy_REAL_KEY"
|
||||||
|
with patch("run_agent.OpenAI") as mock_openai:
|
||||||
|
mock_openai.return_value = MagicMock()
|
||||||
|
from run_agent import AIAgent
|
||||||
|
AIAgent(
|
||||||
|
model="gemini-2.5-flash",
|
||||||
|
provider="gemini",
|
||||||
|
api_key=real_key,
|
||||||
|
base_url="https://generativelanguage.googleapis.com/v1beta/openai",
|
||||||
|
)
|
||||||
|
call_kwargs = mock_openai.call_args[1]
|
||||||
|
# The SDK must NOT receive the real key as api_key (which would emit Bearer)
|
||||||
|
assert call_kwargs.get("api_key") == "not-used", (
|
||||||
|
"api_key must be 'not-used' to suppress Authorization: Bearer for Gemini"
|
||||||
|
)
|
||||||
|
# The real key must be in x-goog-api-key header
|
||||||
|
headers = call_kwargs.get("default_headers", {})
|
||||||
|
assert headers.get("x-goog-api-key") == real_key, (
|
||||||
|
"x-goog-api-key header must carry the real Gemini API key"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_gemini_resolve_provider_client_auth(self, monkeypatch):
|
||||||
|
"""Regression test for issue #7893 — resolve_provider_client path.
|
||||||
|
|
||||||
|
When resolve_provider_client('gemini') is called, the returned OpenAI
|
||||||
|
client must use x-goog-api-key header, not Authorization: Bearer.
|
||||||
|
"""
|
||||||
|
monkeypatch.setenv("GEMINI_API_KEY", "AIzaSy_TEST_KEY")
|
||||||
|
real_key = "AIzaSy_TEST_KEY"
|
||||||
|
with patch("agent.auxiliary_client.OpenAI") as mock_openai:
|
||||||
|
mock_openai.return_value = MagicMock()
|
||||||
|
mock_openai.return_value.api_key = "not-used"
|
||||||
|
from agent.auxiliary_client import resolve_provider_client
|
||||||
|
resolve_provider_client("gemini")
|
||||||
|
call_kwargs = mock_openai.call_args[1]
|
||||||
|
assert call_kwargs.get("api_key") == "not-used", (
|
||||||
|
"api_key must be 'not-used' to prevent Bearer injection for Gemini"
|
||||||
|
)
|
||||||
|
headers = call_kwargs.get("default_headers", {})
|
||||||
|
assert headers.get("x-goog-api-key") == real_key, (
|
||||||
|
"x-goog-api-key header must carry the real Gemini API key"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── models.dev Integration ──
|
# ── models.dev Integration ──
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user