fix(gateway): propagate trusted proxy capabilities

Forward normalized custom-provider capabilities on the default gateway path so native compaction does not depend on session rehydration. Document the content trust boundary and cover both lookup and gateway resolution.
This commit is contained in:
Stephen Chin
2026-08-29 05:54:41 -07:00
committed by Teknium
parent c9b9b5e6c7
commit 80044bf385
5 changed files with 102 additions and 18 deletions
+12
View File
@@ -2960,6 +2960,17 @@ def _resolve_runtime_agent_kwargs() -> dict:
if isinstance(_runtime_mot, int) and _runtime_mot > 0:
max_tokens = _runtime_mot
capabilities = runtime.get("capabilities")
capabilities = (
{
key: value
for key, value in capabilities.items()
if isinstance(key, str) and isinstance(value, bool)
}
if isinstance(capabilities, dict)
else {}
)
return {
"api_key": runtime.get("api_key"),
"base_url": runtime.get("base_url"),
@@ -2976,6 +2987,7 @@ def _resolve_runtime_agent_kwargs() -> dict:
# Must flow through to the per-turn route or the provider's configured
# request body never reaches the model on the gateway path.
"request_overrides": runtime.get("request_overrides"),
"capabilities": capabilities,
}
+20 -17
View File
@@ -707,25 +707,26 @@ def _try_resolve_from_custom_pool(
return None
def _filter_capabilities(value: Any) -> Dict[str, bool]:
"""Return the string-keyed boolean capabilities accepted at runtime."""
if not isinstance(value, dict):
return {}
return {
key: enabled
for key, enabled in value.items()
if isinstance(key, str) and isinstance(enabled, bool)
}
def _lift_model_capabilities(
entry: Dict[str, Any], model: Optional[str], result: Dict[str, Any]
) -> None:
"""Copy explicit boolean per-model capabilities into the runtime."""
capabilities = {
key: value
for key, value in (entry.get("capabilities") or {}).items()
if isinstance(key, str) and isinstance(value, bool)
}
capabilities = _filter_capabilities(entry.get("capabilities"))
models = entry.get("models")
model_config = models.get(model) if isinstance(models, dict) and model else None
if isinstance(model_config, dict):
capabilities.update(
{
key: value
for key, value in model_config.items()
if isinstance(key, str) and isinstance(value, bool)
}
)
capabilities.update(_filter_capabilities(model_config))
if capabilities:
result["capabilities"] = capabilities
@@ -827,7 +828,7 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
# Found match by provider key
base_url = entry.get("api") or entry.get("url") or entry.get("base_url") or ""
if base_url:
result = {
result: Dict[str, Any] = {
"name": entry.get("name", ep_name),
"base_url": base_url.strip(),
"api_key": resolved_api_key,
@@ -855,8 +856,9 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
if api_mode:
result["api_mode"] = api_mode
_lift_max_output_tokens(entry, result)
if isinstance(entry.get("capabilities"), dict):
result["capabilities"] = dict(entry["capabilities"])
capabilities = _filter_capabilities(entry.get("capabilities"))
if capabilities:
result["capabilities"] = capabilities
return result
# Fall back to custom_providers: list (legacy format)
@@ -904,8 +906,9 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
if model_name:
result["model"] = model_name
_lift_max_output_tokens(entry, result)
if isinstance(entry.get("capabilities"), dict):
result["capabilities"] = dict(entry["capabilities"])
capabilities = _filter_capabilities(entry.get("capabilities"))
if capabilities:
result["capabilities"] = capabilities
return result
return None
+26
View File
@@ -80,6 +80,32 @@ class TestAgentConfigSignature:
)
def test_default_gateway_runtime_forwards_filtered_capabilities(self, monkeypatch):
"""Configured provider capabilities must reach a newly created gateway agent."""
from gateway.run import _resolve_runtime_agent_kwargs
from hermes_cli import runtime_provider
monkeypatch.setattr(
runtime_provider,
"resolve_runtime_provider",
lambda: {
"api_key": "test-key",
"base_url": "https://trusted-proxy.example/v1",
"provider": "custom",
"requested_provider": "custom:trusted-proxy",
"api_mode": "responses",
"capabilities": {
"openai_native_compaction": True,
"ignore-me": "not-a-bool",
},
},
)
monkeypatch.setattr(runtime_provider, "_get_model_config", lambda: {})
runtime = _resolve_runtime_agent_kwargs()
assert runtime["capabilities"] == {"openai_native_compaction": True}
# ---------------------------------------------------------------
# cache_keys (compression/context config cache-busting)
# ---------------------------------------------------------------
@@ -659,6 +659,39 @@ def test_named_custom_provider_uses_saved_credentials(monkeypatch):
assert resolved["source"] == "custom_provider:Local"
def test_named_custom_provider_filters_capabilities_at_lookup_boundary(monkeypatch):
monkeypatch.setattr(
rp,
"load_config",
lambda: {
"providers": {
"local": {
"name": "Local",
"base_url": "http://1.2.3.4:1234/v1",
"capabilities": {
"openai_native_compaction": True,
"invalid-value": "yes",
42: True,
},
}
}
},
)
monkeypatch.setattr(
rp,
"resolve_provider",
lambda *a, **k: (_ for _ in ()).throw(
AssertionError(
"resolve_provider should not be called for named custom providers"
)
),
)
provider = rp._get_named_custom_provider("local")
assert provider["capabilities"] == {"openai_native_compaction": True}
def test_bare_custom_resolves_providers_dict_entry_named_custom(monkeypatch):
"""A request for bare ``provider="custom"`` must resolve a literal
``providers.custom`` entry (e.g. a cliproxy endpoint) instead of falling
+11 -1
View File
@@ -167,7 +167,7 @@ When `fallback_chain` is absent, `auto` uses the top-level `fallback_providers`
## Per-provider request options
Provider entries (`providers.<name>` in the `providers:` dict, or items in the legacy `custom_providers` list) accept two knobs that shape how Hermes talks to the endpoint:
Provider entries (`providers.<name>` in the `providers:` dict, or items in the legacy `custom_providers` list) accept knobs that shape how Hermes talks to the endpoint:
**`extra_headers`** — a mapping of extra HTTP headers attached to every LLM request routed to that provider's base URL. They are applied last, after URL/profile defaults and user header overrides, so they survive credential swaps and client rebuilds. Useful for Cloudflare Access service tokens, proxy auth, or custom bearer schemes:
@@ -197,6 +197,16 @@ providers:
With discovery off, the model picker (`hermes model`, `/model`) shows the configured list instead of a live probe.
**`openai_native_compaction`** — set this capability to `true` only for an OpenAI-compatible endpoint that you trust with conversation content. Native compaction sends its payload to that provider's configured `base_url`:
```yaml
providers:
trusted-proxy:
api: https://llm.internal.example.com/v1
capabilities:
openai_native_compaction: true
```
For a gateway that resolves a bare model alias only after receiving the
request, opt the alias into prompt-cache markers with the per-model
`prompt_caching` capability: