mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-18 14:52:04 +00:00
fix(nous): forward provider routing through Portal
This commit is contained in:
parent
69f1460c3d
commit
3aeaf3755d
6 changed files with 102 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
:::
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue