hermes-agent/plugins/model-providers/nous/__init__.py

88 lines
3.6 KiB
Python

"""Nous Portal provider profile."""
from typing import Any
from agent.portal_tags import get_conversation_context, nous_portal_tags
from providers import register_provider
from providers.base import ProviderProfile
class NousProfile(ProviderProfile):
"""Nous Portal — product tags, reasoning with Nous-specific omission."""
def build_extra_body(
self, *, session_id: str | None = None, **context
) -> dict[str, Any]:
body: dict[str, Any] = {"tags": nous_portal_tags(session_id=session_id)}
# Top-level session_id → provider sticky routing key. Pins every
# turn of a session to the same upstream endpoint so explicit
# Anthropic cache_control breakpoints stay warm instead of
# cold-writing a fresh cache on each reroute (Anthropic/Vertex/
# Bedrock caches are instance-local). Mirrors the OpenRouter
# profile; without it the portal falls back to hashing the opening
# messages, which breaks pinning whenever those shift.
#
# Resolve it exactly like ``nous_portal_tags`` resolves the
# ``conversation=`` tag: ambient context first (the lineage ROOT id
# published by the agent loop), explicit argument as fallback.
#
# The gap this closes is the auxiliary call sites — compression,
# title generation, vision, web_extract, session_search, MoA slots.
# They funnel through ``agent.auxiliary_client`` which has no session
# handle, so they never pass ``session_id``: they carried the
# ``conversation=`` tag but NO sticky key at all, and each one routed
# independently of the conversation it belongs to. Reading the same
# ambient contextvar the tag already uses fixes that with zero
# per-call-site plumbing.
#
# For the main loop the two agree anyway under the default
# ``compression.in_place: true`` (#38763), where compaction keeps the
# session id; the ambient root additionally keeps the key stable for
# installs that opt back into rotating compaction, and across
# delegate-subagent trees.
sticky_key = get_conversation_context() or session_id
if sticky_key:
body["session_id"] = sticky_key
provider_preferences = context.get("provider_preferences")
if provider_preferences:
body["provider"] = provider_preferences
return body
def build_api_kwargs_extras(
self,
*,
reasoning_config: dict | None = None,
supports_reasoning: bool = False,
**context,
) -> tuple[dict[str, Any], dict[str, Any]]:
"""Nous: passes full reasoning_config, but OMITS when disabled."""
extra_body = {}
if supports_reasoning:
if reasoning_config is not None:
rc = dict(reasoning_config)
if rc.get("enabled") is False:
pass # Nous omits reasoning when disabled
else:
extra_body["reasoning"] = rc
else:
extra_body["reasoning"] = {"enabled": True, "effort": "medium"}
return extra_body, {}
nous = NousProfile(
name="nous",
aliases=("nous-portal", "nousresearch"),
env_vars=("NOUS_API_KEY",),
display_name="Nous Research",
description="Nous Research — Hermes model family",
signup_url="https://nousresearch.com/",
fallback_models=(
"hermes-3-405b",
"hermes-3-70b",
),
base_url="https://inference-api.nousresearch.com/v1",
auth_type="oauth_device_code",
)
register_provider(nous)