fix: align cache-policy pre-gate identity with the capability matcher

Follow-ups on top of the salvaged #92785 commit:

- Pre-gate now matches base URLs via normalize_route_base_url and
  provider ids via custom_provider_aliases, mirroring the semantics of
  get_custom_provider_model_capability. The raw string comparison
  silently dropped declarations whose config spelling differed only by
  host case or trailing slash (proven empirically: …/v1/ vs …/v1 with a
  non-matching provider name returned (False, False) despite an explicit
  prompt_caching: true).
- get_provider(..., allow_network=False) in the early-init/stub branch:
  the policy runs per request destination (MoA aggregator, auxiliary
  replans via blank_cache_policy_stub, early agent init) and a cold
  models.dev cache triggered a measured ~450 ms foreground registry
  fetch from the send path. A catalog miss degrades to the conservative
  side.
- Debug-log the previously silent provider-lookup exception fallback.
- Tests: _make_agent defaults _custom_providers=[] (post-init reality;
  keeps built-in-route tests off the catalog/config fallback), the two
  early-init tests delete the attr explicitly, and three regression
  tests pin the URL-drift, spaced-legacy-name, and no-network contracts
  (all three fail on the unfixed commit).
This commit is contained in:
kshitijk4poor
2026-08-24 05:02:46 +05:30
committed by kshitij
parent 0204e4898e
commit f93b350711
2 changed files with 109 additions and 15 deletions
+37 -15
View File
@@ -2322,46 +2322,68 @@ def anthropic_prompt_cache_policy(
)
_custom_providers = getattr(agent, "_custom_providers", None)
_route_may_be_custom = False
if _custom_providers:
if not _supports_anthropic_cache_markers:
# Responses/Bedrock never consume the declaration — skip the
# identity probe entirely for those transports.
pass
elif _custom_providers:
# The normalized list is already attached after agent initialization.
# Use cheap runtime identity signals before calling the capability
# helper so an unrelated configured provider does not put every
# built-in chat-completions request on the route-normalization path.
#
# Identity must match the authoritative helper's semantics:
# get_custom_provider_model_capability compares base URLs via
# normalize_route_base_url, and runtime provider ids go through
# custom_provider_aliases (space→hyphen, custom: prefix variants).
# A raw-string gate here would silently drop declarations whose
# config spelling differs only in host case / trailing slash.
from hermes_cli.providers import custom_provider_aliases
from hermes_cli.route_identity import normalize_route_base_url
_provider_ids = {provider_lower}
if provider_lower.startswith("custom:"):
_provider_ids.add(provider_lower.removeprefix("custom:"))
from hermes_cli.route_identity import normalize_route_base_url
_runtime_route_url = normalize_route_base_url(eff_base_url)
_eff_url_normalized = normalize_route_base_url(eff_base_url)
for _entry in _custom_providers:
if not isinstance(_entry, dict):
continue
_entry_ids = {
str(_entry.get("name") or "").strip().lower(),
str(_entry.get("provider_key") or "").strip().lower(),
}
if _provider_ids & (_entry_ids - {""}) or (
_runtime_route_url
_entry_ids = custom_provider_aliases(
str(_entry.get("name") or ""),
str(_entry.get("provider_key") or ""),
)
if _provider_ids & _entry_ids or (
_eff_url_normalized
and normalize_route_base_url(_entry.get("base_url"))
== _runtime_route_url
== _eff_url_normalized
):
_route_may_be_custom = True
break
elif _custom_providers is None:
# During early agent initialization the normalized custom-provider list
# is not attached yet. Avoid rebuilding it for ordinary built-in routes,
# None = the list is not attached yet (early agent initialization or
# a blank_cache_policy_stub destination); an attached empty list means
# the agent initialized with no custom providers and correctly never
# matches. Avoid rebuilding the list for ordinary built-in routes,
# while still recognizing arbitrary config keys and built-in-name
# overrides that point at a different endpoint.
try:
from hermes_cli.providers import get_provider
_provider_def = get_provider(eff_provider)
# allow_network=False: this runs per request destination; a cold
# models.dev cache must not trigger a foreground registry fetch
# from the send path. A catalog miss (None) degrades to the
# conservative side (route may be custom → capability lookup).
_provider_def = get_provider(eff_provider, allow_network=False)
_route_may_be_custom = _provider_def is None or (
bool(_provider_def.base_url)
and base_url_hostname(_provider_def.base_url)
!= base_url_hostname(eff_base_url)
)
except Exception:
except Exception as _pd_exc:
logger.debug(
"provider lookup failed during cache-policy pre-gate: %s",
_pd_exc,
)
_route_may_be_custom = provider_lower.startswith("custom:")
if _supports_anthropic_cache_markers and (
@@ -28,6 +28,11 @@ def _make_agent(
agent.api_mode = api_mode
agent.model = model
agent._base_url_lower = (base_url or "").lower()
# Post-init reality for agents without custom providers: an attached
# empty list. Keeps built-in-route tests hermetic — the policy's
# None-branch would otherwise consult the models.dev catalog and, on a
# miss, fall back to reading the developer's real config.yaml.
agent._custom_providers = []
agent.client = MagicMock()
agent.quiet_mode = True
return agent
@@ -288,6 +293,7 @@ class TestThirdPartyAnthropicGateway:
)
# No agent._custom_providers — exercises the config fallback the
# init-time call (agent_init before the snapshot assignment) hits.
del agent._custom_providers
assert agent._anthropic_prompt_cache_policy() == (True, True)
agent.model = "opus"
@@ -363,6 +369,69 @@ class TestCustomProviderOpenAIWireCapability:
assert agent._anthropic_prompt_cache_policy() == (False, False)
def test_gate_matches_declaration_despite_url_spelling_drift(self):
"""The pre-gate must use the same URL identity semantics as the
authoritative capability matcher (normalize_route_base_url): a
declaration whose config spelling differs only by host case or
trailing slash must still be honored, even when the provider name
gives the gate no help."""
agent = self._configured_agent(enabled=True)
agent.provider = "some-unrelated-alias"
agent.base_url = "https://Models.Example.NET/v1/"
agent._base_url_lower = agent.base_url.lower()
assert agent._anthropic_prompt_cache_policy() == (True, False)
def test_gate_matches_declaration_via_spaced_legacy_name(self):
"""Legacy entries with spaced display names ('My Gateway') must match
the runtime 'custom:my-gateway' identity (custom_provider_aliases
semantics). Combined with URL spelling drift, the raw pre-gate
dropped this declaration on both legs."""
agent = _make_agent(
provider="custom:my-gateway",
base_url="https://GW.example.net/v1/",
api_mode="chat_completions",
model="alias-model",
)
agent._custom_providers = [
{
"name": "My Gateway",
"base_url": "https://gw.example.net/v1",
"models": {"alias-model": {"prompt_caching": True}},
}
]
assert agent._anthropic_prompt_cache_policy() == (True, False)
def test_early_init_probe_never_fetches_catalog_from_network(
self, monkeypatch
):
"""The None-branch provider probe runs per request destination and
must stay off the network: get_provider must be called with
allow_network=False so a cold models.dev cache cannot trigger a
foreground registry download from the send path."""
import hermes_cli.providers as _providers
seen: list = []
real_get_provider = _providers.get_provider
def recording_get_provider(name, **kwargs):
seen.append(kwargs.get("allow_network"))
return real_get_provider(name, **kwargs)
monkeypatch.setattr(_providers, "get_provider", recording_get_provider)
agent = _make_agent(
provider="openrouter",
base_url="https://openrouter.ai/api/v1",
api_mode="chat_completions",
model="anthropic/claude-sonnet-4.6",
)
del agent._custom_providers # early-init shape: attr not attached yet
assert agent._anthropic_prompt_cache_policy() == (True, False)
assert seen, "None-branch did not consult the provider catalog"
assert all(v is False for v in seen)
def test_modern_providers_yaml_is_honored_during_early_init(
self, tmp_path, monkeypatch
):
@@ -390,6 +459,9 @@ class TestCustomProviderOpenAIWireCapability:
api_mode="chat_completions",
model="vendor-agnostic-model",
)
# Early init: the normalized custom-provider list is not attached
# yet, so the policy must recognize the route from config itself.
del agent._custom_providers
assert agent._anthropic_prompt_cache_policy() == (True, False)