diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index c3df449bdfb7..331d1f3402b6 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -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 diff --git a/plugins/model-providers/nous/__init__.py b/plugins/model-providers/nous/__init__.py index 5a61952d7453..b158494e3a3f 100644 --- a/plugins/model-providers/nous/__init__.py +++ b/plugins/model-providers/nous/__init__.py @@ -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, diff --git a/tests/providers/test_provider_profiles.py b/tests/providers/test_provider_profiles.py index 5f2996d8ebf8..0c02ba0d78b2 100644 --- a/tests/providers/test_provider_profiles.py +++ b/tests/providers/test_provider_profiles.py @@ -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" diff --git a/tests/providers/test_transport_parity.py b/tests/providers/test_transport_parity.py index dec63edf8eb4..b77526917c94 100644 --- a/tests/providers/test_transport_parity.py +++ b/tests/providers/test_transport_parity.py @@ -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( diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index dda0b66c621e..a3aa670dd9c7 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -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() diff --git a/website/docs/user-guide/features/provider-routing.md b/website/docs/user-guide/features/provider-routing.md index 3dd6e69787e6..4c00642efa38 100644 --- a/website/docs/user-guide/features/provider-routing.md +++ b/website/docs/user-guide/features/provider-routing.md @@ -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). :::