mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-18 09:51:59 +00:00
fix: sync client.api_key during UnicodeEncodeError ASCII recovery (#10090)
The existing recovery block sanitized self.api_key and self._client_kwargs['api_key'] but did not update self.client.api_key. The OpenAI SDK stores its own copy of api_key and reads it dynamically via the auth_headers property on every request. Without this fix, the retry after sanitization would still send the corrupted key in the Authorization header, causing the same UnicodeEncodeError. The bug manifests when an API key contains Unicode lookalike characters (e.g. ʋ U+028B instead of v) from copy-pasting out of PDFs, rich-text editors, or web pages with decorative fonts. httpx hard-encodes all HTTP headers as ASCII, so the non-ASCII char in the Authorization header triggers the error. Adds TestApiKeyClientSync with two tests verifying: - All three key locations are synced after sanitization - Recovery handles client=None (pre-init) without crashing
This commit is contained in:
parent
9855190f23
commit
5d5d21556e
2 changed files with 69 additions and 0 deletions
|
|
@ -9037,6 +9037,11 @@ class AIAgent:
|
|||
self.api_key = _clean_key
|
||||
if isinstance(getattr(self, "_client_kwargs", None), dict):
|
||||
self._client_kwargs["api_key"] = _clean_key
|
||||
# Also update the live client — it holds its
|
||||
# own copy of api_key which auth_headers reads
|
||||
# dynamically on every request.
|
||||
if getattr(self, "client", None) is not None and hasattr(self.client, "api_key"):
|
||||
self.client.api_key = _clean_key
|
||||
_credential_sanitized = True
|
||||
self._vprint(
|
||||
f"{self.log_prefix}⚠️ API key contained non-ASCII characters "
|
||||
|
|
|
|||
|
|
@ -230,3 +230,67 @@ class TestSanitizeStructureNonAscii:
|
|||
assert _sanitize_structure_non_ascii(payload) is True
|
||||
assert payload["default_headers"]["X-Title"] == "Hermes Agent"
|
||||
assert payload["default_headers"]["User-Agent"] == "Hermes/1.0 "
|
||||
|
||||
|
||||
class TestApiKeyClientSync:
|
||||
"""Verify that ASCII recovery updates the live OpenAI client's api_key.
|
||||
|
||||
The OpenAI SDK stores its own copy of api_key which auth_headers reads
|
||||
dynamically. If only self.api_key is updated but self.client.api_key
|
||||
is not, the next request still sends the corrupted key in the
|
||||
Authorization header.
|
||||
"""
|
||||
|
||||
def test_client_api_key_updated_on_sanitize(self):
|
||||
"""Simulate the recovery path and verify client.api_key is synced."""
|
||||
from unittest.mock import MagicMock
|
||||
from run_agent import AIAgent
|
||||
|
||||
agent = AIAgent.__new__(AIAgent)
|
||||
bad_key = "sk-proj-abc\u028bdef" # ʋ lookalike at position 11
|
||||
agent.api_key = bad_key
|
||||
agent._client_kwargs = {"api_key": bad_key}
|
||||
agent.quiet_mode = True
|
||||
|
||||
# Mock client with its own api_key attribute (like the real OpenAI client)
|
||||
mock_client = MagicMock()
|
||||
mock_client.api_key = bad_key
|
||||
agent.client = mock_client
|
||||
|
||||
# --- replicate the recovery logic from run_agent.py ---
|
||||
_raw_key = agent.api_key
|
||||
_clean_key = _strip_non_ascii(_raw_key)
|
||||
assert _clean_key != _raw_key, "test precondition: key should have non-ASCII"
|
||||
|
||||
agent.api_key = _clean_key
|
||||
agent._client_kwargs["api_key"] = _clean_key
|
||||
if getattr(agent, "client", None) is not None and hasattr(agent.client, "api_key"):
|
||||
agent.client.api_key = _clean_key
|
||||
|
||||
# All three locations should now hold the clean key
|
||||
assert agent.api_key == "sk-proj-abcdef"
|
||||
assert agent._client_kwargs["api_key"] == "sk-proj-abcdef"
|
||||
assert agent.client.api_key == "sk-proj-abcdef"
|
||||
# The bad char should be gone from all of them
|
||||
assert "\u028b" not in agent.api_key
|
||||
assert "\u028b" not in agent._client_kwargs["api_key"]
|
||||
assert "\u028b" not in agent.client.api_key
|
||||
|
||||
def test_client_none_does_not_crash(self):
|
||||
"""Recovery should not crash when client is None (pre-init)."""
|
||||
from run_agent import AIAgent
|
||||
|
||||
agent = AIAgent.__new__(AIAgent)
|
||||
bad_key = "sk-proj-\u028b"
|
||||
agent.api_key = bad_key
|
||||
agent._client_kwargs = {"api_key": bad_key}
|
||||
agent.client = None
|
||||
|
||||
_clean_key = _strip_non_ascii(bad_key)
|
||||
agent.api_key = _clean_key
|
||||
agent._client_kwargs["api_key"] = _clean_key
|
||||
if getattr(agent, "client", None) is not None and hasattr(agent.client, "api_key"):
|
||||
agent.client.api_key = _clean_key
|
||||
|
||||
assert agent.api_key == "sk-proj-"
|
||||
assert agent.client is None # should not have been touched
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue