fix(nous): forward provider routing through Portal

This commit is contained in:
Gille 2026-07-09 20:09:44 -06:00 committed by kshitij
parent 69f1460c3d
commit 3aeaf3755d
6 changed files with 102 additions and 27 deletions

View file

@ -1701,11 +1701,31 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:
_provider_sort = _validated_openrouter_provider_sort(agent.provider_sort)
if _provider_sort:
provider_preferences["sort"] = _provider_sort
if provider_preferences and (
(agent.provider or "").strip().lower() == "openrouter"
or agent._is_openrouter_url()
):
summary_extra_body["provider"] = provider_preferences
if agent.provider_require_parameters:
provider_preferences["require_parameters"] = True
if agent.provider_data_collection:
provider_preferences["data_collection"] = agent.provider_data_collection
if provider_preferences:
profile_provider_preferences = None
try:
from providers import get_provider_profile
provider_profile = get_provider_profile(agent.provider)
if provider_profile is not None:
profile_extra_body = provider_profile.build_extra_body(
provider_preferences=provider_preferences,
)
profile_provider_preferences = profile_extra_body.get("provider")
except Exception:
pass
if profile_provider_preferences:
summary_extra_body["provider"] = profile_provider_preferences
elif (
(agent.provider or "").strip().lower() == "openrouter"
or agent._is_openrouter_url()
):
summary_extra_body["provider"] = provider_preferences
# Pareto Code router plugin — model-gated. Same shape as
# the main-loop emission so summary calls on

View file

@ -13,7 +13,11 @@ class NousProfile(ProviderProfile):
def build_extra_body(
self, *, session_id: str | None = None, **context
) -> dict[str, Any]:
return {"tags": nous_portal_tags()}
body: dict[str, Any] = {"tags": nous_portal_tags()}
provider_preferences = context.get("provider_preferences")
if provider_preferences:
body["provider"] = provider_preferences
return body
def build_api_kwargs_extras(
self,

View file

@ -414,6 +414,18 @@ class TestNousProfile:
body = p.build_extra_body()
assert body["tags"] == nous_portal_tags()
def test_extra_body_with_provider_preferences(self):
from agent.portal_tags import nous_portal_tags
p = get_provider_profile("nous")
preferences = {"only": ["deepseek"], "ignore": ["deepinfra"]}
body = p.build_extra_body(provider_preferences=preferences)
assert body == {
"tags": nous_portal_tags(),
"provider": preferences,
}
def test_auth_type(self):
p = get_provider_profile("nous")
assert p.auth_type == "oauth_device_code"

View file

@ -208,6 +208,21 @@ class TestNousParity:
)
assert kw["extra_body"]["tags"] == nous_portal_tags()
def test_provider_preferences(self, transport):
preferences = {
"only": ["deepseek"],
"ignore": ["deepinfra"],
"sort": "throughput",
}
kw = transport.build_kwargs(
model="deepseek/deepseek-v4-flash",
messages=_simple_messages(),
tools=None,
provider_profile=get_provider_profile("nous"),
provider_preferences=preferences,
)
assert kw["extra_body"]["provider"] == preferences
def test_reasoning_omitted_when_disabled(self, transport):
"""Nous special case: reasoning omitted entirely when disabled."""
kw = transport.build_kwargs(

View file

@ -3788,6 +3788,30 @@ class TestHandleMaxIterations:
kwargs = agent.client.chat.completions.create.call_args.kwargs
assert kwargs["extra_body"]["provider"]["only"] == ["Anthropic"]
def test_summary_keeps_provider_preferences_for_nous(self, agent):
agent.base_url = "https://inference-api.nousresearch.com/v1"
agent._base_url_lower = agent.base_url.lower()
agent.provider = "nous"
agent.providers_allowed = ["deepseek"]
agent.providers_ignored = ["deepinfra"]
agent.provider_sort = "throughput"
agent.provider_require_parameters = True
agent.provider_data_collection = "deny"
agent.client.chat.completions.create.return_value = _mock_response(content="Summary")
agent._cached_system_prompt = "You are helpful."
result = agent._handle_max_iterations([{"role": "user", "content": "do stuff"}], 60)
assert result == "Summary"
kwargs = agent.client.chat.completions.create.call_args.kwargs
assert kwargs["extra_body"]["provider"] == {
"only": ["deepseek"],
"ignore": ["deepinfra"],
"sort": "throughput",
"require_parameters": True,
"data_collection": "deny",
}
def test_summary_drops_invalid_provider_sort(self, agent):
agent.base_url = "https://openrouter.ai/api/v1"
agent._base_url_lower = agent.base_url.lower()

View file

@ -1,18 +1,18 @@
---
title: Provider Routing
description: Configure OpenRouter provider preferences to optimize for cost, speed, or quality.
description: Configure OpenRouter or Nous Portal provider preferences to optimize for cost, speed, or quality.
sidebar_label: Provider Routing
sidebar_position: 7
---
# Provider Routing
When using [OpenRouter](https://openrouter.ai) as your LLM provider, Hermes Agent supports **provider routing** — fine-grained control over which underlying AI providers handle your requests and how they're prioritized.
When using [OpenRouter](https://openrouter.ai) or [Nous Portal](/integrations/nous-portal) as your LLM provider, Hermes Agent supports **provider routing** — fine-grained control over which underlying AI providers handle your requests and how they're prioritized.
OpenRouter routes requests to many providers (e.g., Anthropic, Google, AWS Bedrock, Together AI). Provider routing lets you optimize for cost, speed, quality, or enforce specific provider requirements.
:::tip
Traffic routed through [Nous Portal](/integrations/nous-portal) still respects per-model routing and priority configs — and Portal subscribers get 10% off token-billed providers.
Traffic routed through Nous Portal respects the same provider preferences — and Portal subscribers get 10% off token-billed providers.
:::
## Configuration
@ -30,7 +30,7 @@ provider_routing:
```
:::info
Provider routing only applies when using OpenRouter. It has no effect with direct provider connections (e.g., connecting directly to the Anthropic API).
Provider routing only applies when using OpenRouter or Nous Portal. It has no effect with direct provider connections (e.g., connecting directly to the Anthropic API).
:::
## Options
@ -52,13 +52,13 @@ provider_routing:
### `only`
Whitelist of provider names. When set, **only** these providers will be used. All others are excluded.
Whitelist of provider slugs. When set, **only** these providers will be used. All others are excluded. Use the lowercase slug shown by OpenRouter for each provider.
```yaml
provider_routing:
only:
- "Anthropic"
- "Google"
- "anthropic"
- "google"
```
### `ignore`
@ -68,8 +68,8 @@ Blacklist of provider names. These providers will **never** be used, even if the
```yaml
provider_routing:
ignore:
- "Together"
- "DeepInfra"
- "together"
- "deepinfra"
```
### `order`
@ -79,9 +79,9 @@ Explicit priority order. Providers listed first are preferred. Unlisted provider
```yaml
provider_routing:
order:
- "Anthropic"
- "Google"
- "AWS Bedrock"
- "anthropic"
- "google"
- "amazon-bedrock"
```
### `require_parameters`
@ -138,7 +138,7 @@ Ensure all requests go through a specific provider for consistency:
```yaml
provider_routing:
only:
- "Anthropic"
- "anthropic"
```
### Avoid Specific Providers
@ -148,8 +148,8 @@ Exclude providers you don't want to use (e.g., for data privacy):
```yaml
provider_routing:
ignore:
- "Together"
- "Lepton"
- "together"
- "lepton"
data_collection: "deny"
```
@ -160,14 +160,14 @@ Try your preferred providers first, fall back to others if unavailable:
```yaml
provider_routing:
order:
- "Anthropic"
- "Google"
- "anthropic"
- "google"
require_parameters: true
```
## How It Works
Provider routing preferences are passed to the OpenRouter API via the `extra_body.provider` field on every API call. This applies to both:
Provider routing preferences are passed to OpenRouter or Nous Portal via the `extra_body.provider` field on every API call. (`extra_body` is the OpenAI Python SDK argument; it becomes the top-level `provider` object in the JSON request.) This applies to both:
- **CLI mode** — configured in `~/.hermes/config.yaml`, loaded at startup
- **Gateway mode** — same config file, loaded when the gateway starts
@ -189,7 +189,7 @@ You can combine multiple options. For example, sort by price but exclude certain
```yaml
provider_routing:
sort: "price"
ignore: ["Together"]
ignore: ["together"]
require_parameters: true
data_collection: "deny"
```
@ -197,8 +197,8 @@ provider_routing:
## Default Behavior
When no `provider_routing` section is configured (the default), OpenRouter uses its own default routing logic, which generally balances cost and availability automatically.
When no `provider_routing` section is configured (the default), the aggregator uses its own default routing logic, which generally balances cost and availability automatically.
:::tip Provider Routing vs. Fallback Models
Provider routing controls which **sub-providers within OpenRouter** handle your requests. For automatic failover to an entirely different provider when your primary model fails, see [Fallback Providers](/user-guide/features/fallback-providers).
Provider routing controls which **sub-providers behind OpenRouter or Nous Portal** handle your requests. For automatic failover to an entirely different provider when your primary model fails, see [Fallback Providers](/user-guide/features/fallback-providers).
:::