Skip to content

fix: sync client.api_key during UnicodeEncodeError ASCII recovery#10090

Merged
teknium1 merged 1 commit into
mainfrom
hermes/hermes-63dc0120
Apr 15, 2026
Merged

fix: sync client.api_key during UnicodeEncodeError ASCII recovery#10090
teknium1 merged 1 commit into
mainfrom
hermes/hermes-63dc0120

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Fixes an incomplete recovery path for non-ASCII characters in API keys. The existing UnicodeEncodeError recovery (added for #6843) sanitizes self.api_key and self._client_kwargs['api_key'] but did not update self.client.api_key. The OpenAI SDK stores its own copy and reads it via the auth_headers property on every request — so the retry after sanitization still sent the corrupted key.

Root cause

API keys containing Unicode lookalike characters (e.g. ʋ U+028B instead of v, from copy-pasting out of PDFs or rich-text editors) cause UnicodeEncodeError: 'ascii' codec can't encode character because httpx hard-encodes all HTTP headers as ASCII. The recovery block stripped the bad chars from the agent's state but not from the live client object.

Changes

  • run_agent.py (+5 lines): After sanitizing self.api_key and _client_kwargs, also set self.client.api_key = _clean_key (with null safety checks)
  • tests/run_agent/test_unicode_ascii_codec.py (+64 lines): TestApiKeyClientSync class with 2 tests verifying all three key locations are synced and that client=None doesn't crash

Reported by

Community user kshitij (@kshitijk4poor) — webhook-triggered routines failed with UnicodeEncodeError at position 153 (exactly in the Authorization header range) while manual runs worked (different env var source).

Test plan

python -m pytest tests/run_agent/test_unicode_ascii_codec.py -q  # 25 passed
python -m pytest tests/hermes_cli/test_non_ascii_credential.py -q  # 9 passed

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
@teknium1
teknium1 merged commit 5d5d215 into main Apr 15, 2026
6 of 7 checks passed
@teknium1
teknium1 deleted the hermes/hermes-63dc0120 branch April 15, 2026 05:37
wpsl5168 added a commit to wpsl5168/hermes-agent that referenced this pull request Apr 15, 2026
…ream

* fix: sync client.api_key during UnicodeEncodeError ASCII recovery (NousResearch#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

* fix: stale agent timeout, uv venv detection, empty response after tools, compression model fallback (NousResearch#9051, NousResearch#8620, NousResearch#9400) (NousResearch#10093)

Four independent fixes:

1. Reset activity timestamp on cached agent reuse (NousResearch#9051)
   When the gateway reuses a cached AIAgent for a new turn, the
   _last_activity_ts from the previous turn (possibly hours ago)
   carried over. The inactivity timeout handler immediately saw
   the agent as idle for hours and killed it.

   Fix: reset _last_activity_ts, _last_activity_desc, and
   _api_call_count when retrieving an agent from the cache.

2. Detect uv-managed virtual environments (NousResearch#8620 sub-issue 1)
   The systemd unit generator fell back to sys.executable (uv's
   standalone Python) when running under 'uv run', because
   sys.prefix == sys.base_prefix. The generated ExecStart pointed
   to a Python binary without site-packages.

   Fix: check VIRTUAL_ENV env var before falling back to
   sys.executable. uv sets VIRTUAL_ENV even when sys.prefix
   doesn't reflect the venv.

3. Nudge model to continue after empty post-tool response (NousResearch#9400)
   Weaker models sometimes return empty after tool calls. The agent
   silently abandoned the remaining work.

   Fix: append assistant('(empty)') + user nudge message and retry
   once. Resets after each successful tool round.

4. Compression model fallback on permanent errors (NousResearch#8620 sub-issue 4)
   When the default summary model (gemini-3-flash) returns 503
   'model_not_found' on custom proxies, the compressor entered a
   600s cooldown, leaving context growing unbounded.

   Fix: detect permanent model-not-found errors (503, 404,
   'model_not_found', 'no available channel') and fall back to
   using the main model for compression instead of entering
   cooldown. One-time fallback with immediate retry.

Test plan: 40 compressor tests + 97 gateway/CLI tests + 9 venv tests pass

* fix(cli): defensive subparser routing for argparse bpo-9338 (NousResearch#10113)

On some Python versions, argparse fails to route subcommand tokens when
the parent parser has nargs='?' optional arguments (--continue).  The
symptom: 'hermes model' produces 'unrecognized arguments: model' even
though 'model' is a registered subcommand.

Fix: when argv contains a token matching a known subcommand, set
subparsers.required=True to force deterministic routing.  If that fails
(e.g. 'hermes -c model' where 'model' is consumed as the session name
for --continue), fall back to the default optional-subparsers behaviour.

Adds 13 tests covering all key argument combinations.

Reported via user screenshot showing the exact error on an installed
version with the model subcommand listed in usage but rejected at parse
time.

* feat(doctor): add Command Installation check for hermes bin symlink

hermes doctor now checks whether the ~/.local/bin/hermes symlink exists
and points to the correct venv entry point. With --fix, it creates or
repairs the symlink automatically.

Covers:
- Missing symlink at ~/.local/bin/hermes (or $PREFIX/bin on Termux)
- Symlink pointing to wrong target
- Missing venv entry point (venv/bin/hermes or .venv/bin/hermes)
- PATH warning when ~/.local/bin is not on PATH
- Skipped on Windows (different mechanism)

Addresses user report: 'python -m hermes_cli.main doesn't have an option
to fix the local bin/install'

10 new tests covering all scenarios.

* fix(cli): restore messaging toolset for gateway platforms

* feat: auto session summaries - generate structured summaries on session end, inject into future sessions

* fix: add summary column to SCHEMA_SQL for fresh DB installs

* feat: auto memory extraction on session end

Adds _spawn_auto_extraction() that runs in a background thread after
session ends. Uses LLM to analyze conversation and extract durable
facts (preferences, corrections, environment details) into memory.

- Dedup against existing memory state
- Structured JSON operations (add/replace/remove)
- Configurable via memory.auto_extraction config
- Minimum 3 meaningful turns required to trigger
- Capped at 10 operations per session

Part of the Memory Architecture Upgrade initiative.

* feat: elastic hot/cold memory with FTS5 search

Phase 3 of memory architecture upgrade:

- DB schema v7→v8: new memory_entries table with FTS5 full-text search
- SessionDB: 7 new cold memory methods (add/get/search/remove/list/touch/count)
- MemoryStore: hot/cold architecture with archive (hot→cold), promote (cold→hot),
  add_to_cold, search_cold methods
- memory_tool: 3 new actions (search, archive, promote) for cold storage
- Auto extraction fallback: when hot memory full, auto-stores to cold
- MEMORY_GUIDANCE updated with cold storage instructions
- All integration tests passing

* fix: always write .clean_shutdown on graceful stop to prevent session loss

Previously the marker was skipped when drain timed out, causing
suspend_recently_active() to mark sessions on next startup, which
led to unwanted auto-resets that lost conversation history.

Drain timeout means agents were slow, not that sessions are stuck.
The stuck-loop detector handles genuinely stuck sessions independently.

* feat: memory system upgrade - sqlite-vec, hybrid search, Auto Dream

P0: Fix session reset on gateway restart (always write .clean_shutdown)
P1a: sqlite-vec v0.1.9 + embedding_client.py (OpenRouter text-embedding-3-small)
P1b: session_search hybrid FTS5+vec, prioritize cached summaries
P1c: cold memory embedding integration
P1d: hot memory expansion (4400/2750 chars)
P2: Auto Dream trigger (OR logic: >=24h or >=5 sessions)
     - _check_auto_dream() in run_agent.py
     - Dream A cron changed from weekly to daily fallback
     - system_meta tracks last_dream_at + sessions_since_dream

Schema: v8 -> v9 (embeddings, system_meta, embeddings_vec tables)

* feat(embedding): migrate to Ollama local embedding with OpenRouter fallback

- Rewrite embedding_client.py for dual-backend architecture:
  Ollama (nomic-embed-text, 768-dim) as primary, OpenRouter as fallback
- Add get_query_embedding() with 'search_query:' prefix for retrieval
- Dynamic EMBEDDING_DIM detection based on active backend
- hermes_state.py: read dimensions from embedding_client instead of hardcoding
- memory_tool.py, session_search_tool.py: use get_query_embedding() for searches
- Add scripts/backfill_all_embeddings.py for historical data migration

Zero-cost local embeddings, ~0.8s per call, no API rate limits.

---------

Co-authored-by: Teknium <[email protected]>
Co-authored-by: Teknium <[email protected]>
Co-authored-by: Ubuntu <davida@boombox.kh1kbv5xmjqelbtffzsvgoyjie.cx.internal.cloudapp.net>
@teknium1 teknium1 mentioned this pull request Apr 15, 2026
1 task
ulasbilgen pushed a commit to ulasbilgen/hermes-adhd-agent that referenced this pull request May 1, 2026
…usResearch#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
aj-nt pushed a commit to aj-nt/hermes-agent that referenced this pull request May 1, 2026
…usResearch#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
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…usResearch#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
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…usResearch#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
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
…usResearch#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
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…usResearch#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
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jul 3, 2026
…usResearch#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
kulikman pushed a commit to kulikman/hermes-agent that referenced this pull request Jul 16, 2026
…usResearch#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
waym0reom3ga added a commit to waym0reom3ga/autolycus-agent that referenced this pull request Jul 21, 2026
…usResearch#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant