Files
hermes-agent/tests/agent/test_per_model_provider_routing.py
Teknium 12871bd01e feat(openrouter): per-model provider_routing.models.<id> overrides
`provider_routing.models.<model-id>` now takes the same only/ignore/order/sort/
require_parameters/data_collection keys and overlays the flat provider_routing
values whenever the agent is on that model. Resolution lives in the one
chokepoint every request path already uses (_provider_preferences_for_agent),
so CLI, gateway, TUI/Desktop, cron, /model switches, fallback activation and
delegated children on another model all honour it with no per-surface plumbing.
Matching is spelling-tolerant, sharing _canonical_model_variants with
agent.reasoning_overrides.

The OpenRouter profile's speed-tier pin no longer overwrites an explicit user
`only` on the BASE gpt-6-astra slug: the pin exists to keep default routing off
flex/fast, and a user pin is the stronger intent (only: [openai] stays [openai]
instead of becoming [openai, azure, azure/us]). Tier slugs (-fast/-flex) keep
owning `only`.

Live A/B (config only: {gpt-6-astra: [openai], claude-fable-5.1: [anthropic]}):
main sent {"sort":"price"} for fable and OpenRouter served it from Azure; with
this change it sends {"only":["anthropic"],"sort":"price"} and Anthropic serves it.

Schema proposed in #24495 (samplesabotage) and #100711 (Artemonim); this is a
slim chokepoint implementation of that design.

Co-authored-by: samplesabotage <samplesabotage@users.noreply.github.com>
2026-09-06 02:18:14 -07:00

41 lines
1.8 KiB
Python

"""``provider_routing.models.<id>`` overlays the flat OpenRouter routing for the CURRENT agent.model."""
from types import SimpleNamespace
import pytest
from agent import chat_completion_helpers as cch
def _agent(model, **flat):
base = dict(providers_allowed=None, providers_ignored=None, providers_order=None, provider_sort="price",
provider_require_parameters=False, provider_data_collection=None)
base.update(flat)
return SimpleNamespace(model=model, **base)
@pytest.fixture
def routing_cfg(monkeypatch):
cfg = {"provider_routing": {"sort": "price", "models": {
"openai/gpt-6-astra": {"only": ["openai"]},
"anthropic/claude-fable-5.1": {"only": ["anthropic"], "sort": "throughput"},
}}}
import hermes_cli.config as config_mod
monkeypatch.setattr(config_mod, "load_config_readonly", lambda: cfg)
return cfg
def test_per_model_entry_overlays_flat_routing_for_that_model_only(routing_cfg):
assert cch._provider_preferences_for_agent(_agent("openai/gpt-6-astra")) == {"only": ["openai"], "sort": "price"}
# A per-model key wins over the flat one; unset keys fall through.
assert cch._provider_preferences_for_agent(_agent("anthropic/claude-fable-5.1")) == {
"only": ["anthropic"], "sort": "throughput"}
# Unlisted model keeps the flat behaviour; no pin leaks across models.
assert cch._provider_preferences_for_agent(_agent("moonshotai/kimi-k2.6")) == {"sort": "price"}
def test_per_model_match_is_spelling_tolerant_and_follows_model_switch(routing_cfg):
agent = _agent("openrouter/openai/gpt-6-astra", providers_allowed=["together"])
assert cch._provider_preferences_for_agent(agent)["only"] == ["openai"]
agent.model = "claude-fable-5-1"
assert cch._provider_preferences_for_agent(agent)["only"] == ["anthropic"]