Skip to content

perf(runtime): read-only config loads + per-message provider-resolution memo - #81853

Open
spfcraze wants to merge 2 commits into
NousResearch:mainfrom
spfcraze:fix/provider-runtime-fastpath
Open

spfcraze wants to merge 2 commits into
NousResearch:mainfrom
spfcraze:fix/provider-runtime-fastpath

Conversation

@spfcraze

@spfcraze spfcraze commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

resolve_runtime_provider() deep-copies the whole config 5-7x per call (measured 74% of its 2.35ms/call cost) and the gateway resolver calls it 5-8x per inbound message (~12-19ms of CPU before every LLM call). The MoA path already caches it behind a 300s TTL (#66793); the gateway path never got that. Fix: (A) convert all 6 read-only config reads in the resolve tree to load_config_readonly() (mtime-cached, no deepcopy), deep-copying only the small model section in _get_model_config to preserve its mutation contract — measured 2.35ms->0.94ms/call; (B) add _memoized_resolve_runtime at the gateway per-message entry, keyed on config+auth mtimes with a 300s TTL backstop, never caching the vertex OAuth path or fallback results — measured 0.94ms->0.149ms on memo hit. Per-message cost drops ~10x. 12 new measured-work/behavior-parity tests; sabotage PASS.

Related Issue

No GitHub issue — discovered via code review and reproduced live (see below). Happy to file one first if preferred.

Changes Made

  • fix/provider-runtime-fastpath — 9 file(s) changed vs base:
    • gateway/run.py
    • hermes_cli/runtime_provider.py
    • tests/agent/test_bedrock_adapter.py
    • tests/agent/test_nous_portal_anthropic_wire.py
    • tests/gateway/test_runtime_resolve_fastpath.py
    • tests/hermes_cli/test_canonical_custom_identity.py
    • tests/hermes_cli/test_custom_provider_identity.py
    • tests/hermes_cli/test_runtime_provider_resolution.py
    • tests/tui_gateway/test_custom_provider_session_persistence.py

How to Test

Validation completed:

  1. Sabotage check: pre-fix code fails the regression tests (8 failed), with the fix all pass (12 passed, 0 failed) — target tests/gateway/test_runtime_resolve_fastpath.py.
  2. Suite <full suite>: branch 27401 passed / 6 failed vs baseline 27387 passed / 8 failed — zero branch-only failures.
  3. The full repo-wide suite was run (item 2); GitHub CI remains the final confirmation environment.

Logs

Sabotage verification output:

# base leg (pre-fix code + branch tests):
#   tests: 4 passed, 8 failed
# head leg (with fix):
#   tests: 12 passed, 0 failed

…on memo

resolve_runtime_provider() calls load_config() (a full defensive deepcopy,
~265us warm) 5-7x per call via the resolve tree — measured 74% of the
~2.35 ms/call cost — and the gateway resolver calls it 5-8x per inbound
message. Two layers fix this:

Layer A (hermes_cli/runtime_provider.py): every config read in the resolve
tree is read-only, so switch all 6 sites to load_config_readonly()
(mtime-cached, no deepcopy). _get_model_config() deep-copies only the small
model section to preserve its mutation-safety contract. Measured:
resolve_runtime_provider 2.35ms -> 0.94ms/call; _get_model_config 0.38ms ->
0.023ms/call.

Layer B (gateway/run.py): the MoA path already caches
resolve_runtime_provider behind a 300s TTL (merged MoA runtime cache,
agent/moa_loop.py); the gateway path now gets the same treatment via
_memoized_resolve_runtime, keyed on the files the resolution reads
(config.yaml + profile/global auth.json) with a TTL backstop for
env-var-only changes. Vertex (per-call OAuth token) and AuthError/fallback
results are never cached. The memo stores a copy and hands out fresh dicts
so caller mutation (pop('model')) can never corrupt it. Per-message:
~12-19ms -> ~1.5-2ms (first call resolves, the rest hit the memo).

Tests: new tests/gateway/test_runtime_resolve_fastpath.py pins the
measured-work (no deepcopy from the resolve tree, one resolve per message),
TTL/config/auth invalidation, vertex bypass, and mutation safety. Existing
tests that patched rp.load_config as their config-injection seam now patch
rp.load_config_readonly (the seam moved with the loader).
@alt-glitch alt-glitch added type/perf Performance improvement or optimization comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have labels Aug 8, 2026
The per-message memo in _memoized_resolve_runtime() keyed on config/auth
file (mtime_ns, size) signatures only. A multiplex gateway resolves
multiple profiles' agents in the same OS process (the desktop tui_gateway
switches profiles per request via set_hermes_home_override), and
'hermes profile create --clone-all' copies the profile tree with
mtime-preserving shutil.copy2 — so a cloned profile's config.yaml/auth.json
carry the source's identical (mtime_ns, size) and compute the same memo
signature, letting one profile receive the other's cached api_key/base_url
for up to the 300s TTL.

Add str(get_hermes_home()) to the memo key, the same profile-boundary fix
open PR NousResearch#78185 applies to agent/moa_loop.py's sibling _runtime_cache.

Tests: new tests/gateway/test_runtime_resolve_fastpath_profile_isolation.py
drives _memoized_resolve_runtime under two profile overrides and asserts
each profile gets its own credentials; both tests fail without the fix and
pass with it. Same-profile memo hit preserved (2 invocations, 1 resolve).
@spfcraze

spfcraze commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

this is the same profile-boundary gap #78185 closes for agent/moa_loop.py's sibling cache, and my gateway memo had the identical flaw. Fixed in 189c7b2.

What changed: _runtime_resolve_memo_signature() now leads the memo key with str(get_hermes_home()), so the key is (hermes_home, config sig, profile auth sig, global auth sig) — mirroring #78185's (hermes_home, provider, model) pattern. A multiplex gateway resolving profiles A and B in the same process can no longer collide on cloned profiles whose config.yaml/auth.json share (mtime_ns, size) (mtime-preserving shutil.copy2 from --clone-all); each profile gets its own memo slot and its own resolved api_key/base_url.

Verification:

  • New tests/gateway/test_runtime_resolve_fastpath_profile_isolation.py drives _memoized_resolve_runtime() under two profile overrides and asserts each profile receives its own credentials. Both tests fail without the fix (profile B receives profile A's cached key) and pass with it — same proof style as fix(moa): scope the per-slot runtime cache to the active profile #78185's test_profile_isolation_runtime.py addition.
  • test_signature_includes_hermes_home asserts the signature differs between profiles even when the config/auth files are byte-identical.
  • No perf regression: the same-profile memo hit is still pinned (2 invocations → 1 resolve call) by test_second_resolve_hits_memo.
  • Full affected surface green: 76 tests across the fastpath, profile-isolation, runtime-resolution, and session-override suites; CI on the fixed head is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have type/perf Performance improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants