Files
hermes-agent/tests/gateway/test_turn_request_overrides.py
T
Teknium c7e2e0b779 feat(fast): bounded /fast auto|cold windows behind one route-aware gate
Adds two bounded fast modes on top of the static /fast toggle, default OFF:

- `auto`: every user turn opens a `agent.fast_auto_seconds` (default 60s)
  window; requests inside it carry the provider fast param, later tool-loop
  requests fall back to standard pricing.
- `cold`: the same window, but only on the first turn of a session (no prior
  user/assistant/tool history).

agent/fast_mode.py holds the whole policy: `begin_turn()` at the
run_conversation ingress arms `agent._fast_until`; `effective_request_overrides()`
is consumed in the ONE place request_overrides feed the transports
(build_api_kwargs), so the fast param is a per-request kwarg only. System
prompt, tools and messages are untouched — the prompt cache is preserved.

resolve_fast_mode_overrides() is now the single gate for static and bounded
modes and accepts provider/base_url: OpenRouter, Nous, Copilot, Azure,
Bedrock and custom base_urls never receive service_tier/speed (#34308's
route gating). Both existing callers (CLI turn route, gateway turn route)
and the TUI config.set path pass the route.

Surfaces: config `agent.service_tier: auto|cold` + `agent.fast_auto_seconds`,
`/fast auto|cold` in CLI, gateway (picker gains both entries), TUI/desktop
config.set; status shows the mode; web dashboard select lists the real
values. Docs: configuration.md Fast Mode section with mode table + cost note,
slash-commands, cli-config.yaml.example, locale strings for the two picker
entries.

Salvages #89991 (bounded fast modes) and #34308 (route gating).
Fixes #64785, #74730.

Co-authored-by: Eva <239388517+100yenadmin@users.noreply.github.com>
Co-authored-by: kbaicai <kbaicai@qq.com>
2026-09-02 05:33:13 -07:00

141 lines
5.2 KiB
Python

"""Regression tests: the gateway must preserve a custom provider's
``request_overrides`` on per-turn agent config.
A ``custom_providers`` entry can carry an ``extra_body`` (e.g.
``chat_template_kwargs`` to toggle a local model's thinking).
``resolve_runtime_provider`` surfaces it as ``request_overrides`` on the
resolved runtime dict, but the gateway used to rebuild the runtime from a
fixed key whitelist that omitted it -- so the provider's configured
``extra_body`` never reached the model on the gateway path, and only
``/fast`` service-tier overrides survived.
"""
import pytest
from gateway.run import GatewayRunner
PROVIDER_OVERRIDES = {"extra_body": {"chat_template_kwargs": {"enable_thinking": True}}}
def _runtime_kwargs(**extra):
base = {
"api_key": "no-key-required",
"base_url": "http://10.0.0.1:8000/v1",
"provider": "custom",
"api_mode": "chat_completions",
"command": None,
"args": [],
"credential_pool": None,
"max_tokens": None,
}
base.update(extra)
return base
def _runner(service_tier=None):
runner = object.__new__(GatewayRunner)
runner._service_tier = service_tier
return runner
def test_provider_request_overrides_preserved_without_service_tier():
"""No /fast: the provider's extra_body must pass straight through."""
runner = _runner(service_tier=None)
rk = _runtime_kwargs(request_overrides=PROVIDER_OVERRIDES)
route = runner._resolve_turn_agent_config("hi", "main", rk)
assert route["request_overrides"] == PROVIDER_OVERRIDES
# A copy, not an alias into runtime_kwargs.
assert route["request_overrides"] is not rk["request_overrides"]
def test_provider_request_overrides_merged_under_fast_mode(monkeypatch):
"""/fast active: provider extra_body AND the service-tier marker both survive."""
monkeypatch.setattr(
"hermes_cli.models.resolve_fast_mode_overrides",
lambda model_id, **_route: {"service_tier": "priority"},
)
runner = _runner(service_tier="priority")
rk = _runtime_kwargs(request_overrides=PROVIDER_OVERRIDES)
route = runner._resolve_turn_agent_config("hi", "main", rk)
assert route["request_overrides"]["extra_body"] == PROVIDER_OVERRIDES["extra_body"]
assert route["request_overrides"]["service_tier"] == "priority"
def test_no_provider_overrides_yields_empty():
"""Regression: absent provider overrides, behaviour is unchanged ({})."""
runner = _runner(service_tier=None)
route = runner._resolve_turn_agent_config("hi", "main", _runtime_kwargs())
assert route["request_overrides"] == {}
def test_resolve_runtime_agent_kwargs_carries_request_overrides(monkeypatch):
"""The module-level runtime resolver must not drop request_overrides."""
import gateway.run as gateway_run
fake_runtime = {
"api_key": "k",
"base_url": "http://10.0.0.1:8000/v1",
"provider": "custom",
"api_mode": "chat_completions",
"request_overrides": PROVIDER_OVERRIDES,
}
monkeypatch.setattr(
"hermes_cli.runtime_provider.resolve_runtime_provider",
lambda *a, **k: dict(fake_runtime),
)
monkeypatch.setattr(
"hermes_cli.runtime_provider._get_model_config", lambda: {}
)
rk = gateway_run._resolve_runtime_agent_kwargs()
assert rk["request_overrides"] == PROVIDER_OVERRIDES
# --- /model session-override follow-up: request_overrides must survive a switch ---
def test_session_override_applies_request_overrides():
"""A /model switch to a custom provider carries its extra_body into runtime."""
runner = object.__new__(GatewayRunner)
runner._session_model_overrides = {
"sess1": {
"model": "thinkmodel",
"provider": "custom",
"api_key": "k",
"base_url": "http://10.0.0.1:8000/v1",
"api_mode": "chat_completions",
"request_overrides": PROVIDER_OVERRIDES,
}
}
rk = _runtime_kwargs() # default resolution carried no overrides
model, out = runner._apply_session_model_override("sess1", "oldmodel", rk)
assert model == "thinkmodel"
assert out["request_overrides"] == PROVIDER_OVERRIDES
def test_session_override_clears_stale_request_overrides():
"""Switching to a provider with no overrides clears a stale value."""
runner = object.__new__(GatewayRunner)
runner._session_model_overrides = {
"sess1": {
"model": "plain",
"provider": "openrouter",
"api_key": "k",
"base_url": "https://openrouter.ai/api/v1",
"api_mode": "chat_completions",
"request_overrides": None,
}
}
rk = _runtime_kwargs(request_overrides=PROVIDER_OVERRIDES) # stale, from default
_, out = runner._apply_session_model_override("sess1", "old", rk)
assert out.get("request_overrides") is None
def test_session_override_absent_is_noop():
"""No override for the session leaves runtime_kwargs untouched."""
runner = object.__new__(GatewayRunner)
runner._session_model_overrides = {}
rk = _runtime_kwargs(request_overrides=PROVIDER_OVERRIDES)
model, out = runner._apply_session_model_override("nope", "keepme", rk)
assert model == "keepme"
assert out["request_overrides"] == PROVIDER_OVERRIDES