From 7a3656aea21d6d9c5f0fd660e0eb91a47166658a Mon Sep 17 00:00:00 2001 From: teknium1 Date: Wed, 25 Feb 2026 18:39:36 -0800 Subject: [PATCH 1/2] refactor: integrate Nous Portal support in auxiliary client - Added functionality to include product attribution tags for Nous Portal in auxiliary API calls. - Introduced a mechanism to determine if the auxiliary client is backed by Nous Portal, affecting the extra body of requests. - Updated various tools to utilize the new extra body configuration for enhanced tracking in API calls. --- agent/auxiliary_client.py | 19 +++++++++++++++++++ tools/openrouter_client.py | 10 +++++++++- tools/session_search_tool.py | 3 +++ tools/vision_tools.py | 3 +++ tools/web_tools.py | 6 ++++++ 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index b8229bb5e..0ad4de220 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -35,6 +35,14 @@ _OR_HEADERS = { "X-OpenRouter-Categories": "cli-agent", } +# Nous Portal extra_body for product attribution. +# Callers should pass this as extra_body in chat.completions.create() +# when the auxiliary client is backed by Nous Portal. +NOUS_EXTRA_BODY = {"tags": ["product=hermes-agent"]} + +# Set at resolve time — True if the auxiliary client points to Nous Portal +auxiliary_is_nous: bool = False + # Default auxiliary models per provider _OPENROUTER_MODEL = "google/gemini-3-flash-preview" _NOUS_MODEL = "gemini-3-flash" @@ -91,6 +99,8 @@ def get_text_auxiliary_client() -> Tuple[Optional[OpenAI], Optional[str]]: # 2. Nous Portal nous = _read_nous_auth() if nous: + global auxiliary_is_nous + auxiliary_is_nous = True logger.debug("Auxiliary text client: Nous Portal") return ( OpenAI(api_key=_nous_api_key(nous), base_url=_nous_base_url()), @@ -135,3 +145,12 @@ def get_vision_auxiliary_client() -> Tuple[Optional[OpenAI], Optional[str]]: # 3. Nothing suitable logger.debug("Auxiliary vision client: none available") return None, None + + +def get_auxiliary_extra_body() -> dict: + """Return extra_body kwargs for auxiliary API calls. + + Includes Nous Portal product tags when the auxiliary client is backed + by Nous Portal. Returns empty dict otherwise. + """ + return dict(NOUS_EXTRA_BODY) if auxiliary_is_nous else {} diff --git a/tools/openrouter_client.py b/tools/openrouter_client.py index c960f4f4a..7d30e6eec 100644 --- a/tools/openrouter_client.py +++ b/tools/openrouter_client.py @@ -25,7 +25,15 @@ def get_async_client() -> AsyncOpenAI: api_key = os.getenv("OPENROUTER_API_KEY") if not api_key: raise ValueError("OPENROUTER_API_KEY environment variable not set") - _client = AsyncOpenAI(api_key=api_key, base_url=OPENROUTER_BASE_URL) + _client = AsyncOpenAI( + api_key=api_key, + base_url=OPENROUTER_BASE_URL, + default_headers={ + "HTTP-Referer": "https://github.com/NousResearch/hermes-agent", + "X-OpenRouter-Title": "Hermes Agent", + "X-OpenRouter-Categories": "cli-agent", + }, + ) return _client diff --git a/tools/session_search_tool.py b/tools/session_search_tool.py index 179d485c0..299286d98 100644 --- a/tools/session_search_tool.py +++ b/tools/session_search_tool.py @@ -170,12 +170,15 @@ async def _summarize_session( max_retries = 3 for attempt in range(max_retries): try: + from agent.auxiliary_client import get_auxiliary_extra_body + _extra = get_auxiliary_extra_body() response = await _async_aux_client.chat.completions.create( model=_SUMMARIZER_MODEL, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], + **({} if not _extra else {"extra_body": _extra}), temperature=0.1, max_tokens=MAX_SUMMARY_TOKENS, ) diff --git a/tools/vision_tools.py b/tools/vision_tools.py index 4b88724ab..456f85583 100644 --- a/tools/vision_tools.py +++ b/tools/vision_tools.py @@ -314,11 +314,14 @@ async def vision_analyze_tool( logger.info("Processing image with %s...", model) # Call the vision API + from agent.auxiliary_client import get_auxiliary_extra_body + _extra = get_auxiliary_extra_body() response = await _aux_async_client.chat.completions.create( model=model, messages=messages, temperature=0.1, max_tokens=2000, + **({} if not _extra else {"extra_body": _extra}), ) # Extract the analysis diff --git a/tools/web_tools.py b/tools/web_tools.py index 8610e38c6..a7f64166e 100644 --- a/tools/web_tools.py +++ b/tools/web_tools.py @@ -242,6 +242,8 @@ Create a markdown summary that captures all key information in a well-organized, if _aux_async_client is None: logger.warning("No auxiliary model available for web content processing") return None + from agent.auxiliary_client import get_auxiliary_extra_body + _extra = get_auxiliary_extra_body() response = await _aux_async_client.chat.completions.create( model=model, messages=[ @@ -250,6 +252,7 @@ Create a markdown summary that captures all key information in a well-organized, ], temperature=0.1, max_tokens=max_tokens, + **({} if not _extra else {"extra_body": _extra}), ) return response.choices[0].message.content.strip() except Exception as api_error: @@ -362,6 +365,8 @@ Create a single, unified markdown summary.""" fallback = fallback[:max_output_size] + "\n\n[... truncated ...]" return fallback + from agent.auxiliary_client import get_auxiliary_extra_body + _extra = get_auxiliary_extra_body() response = await _aux_async_client.chat.completions.create( model=model, messages=[ @@ -370,6 +375,7 @@ Create a single, unified markdown summary.""" ], temperature=0.1, max_tokens=4000, + **({} if not _extra else {"extra_body": _extra}), ) final_summary = response.choices[0].message.content.strip() From cbde8548f4b57dacc0ab8ad62d708e7fac7cb504 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Wed, 25 Feb 2026 18:51:28 -0800 Subject: [PATCH 2/2] Fix for gateway not using nous auth: issue #28 --- cron/scheduler.py | 10 ++++++++++ gateway/run.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/cron/scheduler.py b/cron/scheduler.py index 62987cca6..689d7871c 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -185,6 +185,16 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: elif isinstance(_model_cfg, dict): model = _model_cfg.get("default", model) base_url = _model_cfg.get("base_url", base_url) + # Check if provider is nous — resolve OAuth credentials + provider = _model_cfg.get("provider", "") if isinstance(_model_cfg, dict) else "" + if provider == "nous": + try: + from hermes_cli.auth import resolve_nous_runtime_credentials + creds = resolve_nous_runtime_credentials(min_key_ttl_seconds=5 * 60) + api_key = creds.get("api_key", api_key) + base_url = creds.get("base_url", base_url) + except Exception as nous_err: + logging.warning("Nous Portal credential resolution failed for cron: %s", nous_err) except Exception: pass diff --git a/gateway/run.py b/gateway/run.py index 214a026ab..0a96141fe 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1404,6 +1404,16 @@ class GatewayRunner: elif isinstance(_model_cfg, dict): model = _model_cfg.get("default", model) base_url = _model_cfg.get("base_url", base_url) + # Check if provider is nous — resolve OAuth credentials + provider = _model_cfg.get("provider", "") if isinstance(_model_cfg, dict) else "" + if provider == "nous": + try: + from hermes_cli.auth import resolve_nous_runtime_credentials + creds = resolve_nous_runtime_credentials(min_key_ttl_seconds=5 * 60) + api_key = creds.get("api_key", api_key) + base_url = creds.get("base_url", base_url) + except Exception as nous_err: + logger.warning("Nous Portal credential resolution failed: %s", nous_err) except Exception: pass