diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 4571520af..9c153a74d 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -17,7 +17,10 @@ Resolution order for text tasks (auto mode): Resolution order for vision/multimodal tasks (auto mode): 1. OpenRouter 2. Nous Portal - 3. None (steps 3-5 are skipped — they may not support multimodal) + 3. Codex OAuth (gpt-5.3-codex supports vision via Responses API) + 4. Custom endpoint (for local vision models: Qwen-VL, LLaVA, Pixtral, etc.) + 5. None (API-key providers like z.ai/Kimi/MiniMax are skipped — + they may not support multimodal) Per-task provider overrides (e.g. AUXILIARY_VISION_PROVIDER, CONTEXT_COMPRESSION_PROVIDER) can force a specific provider for each task: diff --git a/mini_swe_runner.py b/mini_swe_runner.py index 9be7b7348..5cb337b87 100644 --- a/mini_swe_runner.py +++ b/mini_swe_runner.py @@ -189,29 +189,30 @@ class MiniSWERunner: ) self.logger = logging.getLogger(__name__) - # Initialize OpenAI client - defaults to OpenRouter - from openai import OpenAI - - client_kwargs = {} - - # Default to OpenRouter if no base_url provided - if base_url: - client_kwargs["base_url"] = base_url + # Initialize LLM client via centralized provider router. + # If explicit api_key/base_url are provided (e.g. from CLI args), + # construct directly. Otherwise use the router for OpenRouter. + if api_key or base_url: + from openai import OpenAI + client_kwargs = { + "base_url": base_url or "https://openrouter.ai/api/v1", + "api_key": api_key or os.getenv( + "OPENROUTER_API_KEY", + os.getenv("ANTHROPIC_API_KEY", + os.getenv("OPENAI_API_KEY", ""))), + } + self.client = OpenAI(**client_kwargs) else: - client_kwargs["base_url"] = "https://openrouter.ai/api/v1" - - - - # Handle API key - OpenRouter is the primary provider - if api_key: - client_kwargs["api_key"] = api_key - else: - client_kwargs["api_key"] = os.getenv( - "OPENROUTER_API_KEY", - os.getenv("ANTHROPIC_API_KEY", os.getenv("OPENAI_API_KEY", "")) - ) - - self.client = OpenAI(**client_kwargs) + from agent.auxiliary_client import resolve_provider_client + self.client, _ = resolve_provider_client("openrouter", model=model) + if self.client is None: + # Fallback: try auto-detection + self.client, _ = resolve_provider_client("auto", model=model) + if self.client is None: + from openai import OpenAI + self.client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=os.getenv("OPENROUTER_API_KEY", "")) # Environment will be created per-task self.env = None diff --git a/tools/openrouter_client.py b/tools/openrouter_client.py index 343cf1021..0637a7db0 100644 --- a/tools/openrouter_client.py +++ b/tools/openrouter_client.py @@ -1,39 +1,30 @@ """Shared OpenRouter API client for Hermes tools. Provides a single lazy-initialized AsyncOpenAI client that all tool modules -can share, eliminating the duplicated _get_openrouter_client() / -_get_summarizer_client() pattern previously copy-pasted across web_tools, -vision_tools, mixture_of_agents_tool, and session_search_tool. +can share. Routes through the centralized provider router in +agent/auxiliary_client.py so auth, headers, and API format are handled +consistently. """ import os -from openai import AsyncOpenAI -from hermes_constants import OPENROUTER_BASE_URL - -_client: AsyncOpenAI | None = None +_client = None -def get_async_client() -> AsyncOpenAI: - """Return a shared AsyncOpenAI client pointed at OpenRouter. +def get_async_client(): + """Return a shared async OpenAI-compatible client for OpenRouter. The client is created lazily on first call and reused thereafter. + Uses the centralized provider router for auth and client construction. Raises ValueError if OPENROUTER_API_KEY is not set. """ global _client if _client is None: - api_key = os.getenv("OPENROUTER_API_KEY") - if not api_key: + from agent.auxiliary_client import resolve_provider_client + client, _model = resolve_provider_client("openrouter", async_mode=True) + if client is None: raise ValueError("OPENROUTER_API_KEY environment variable not set") - _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": "productivity,cli-agent", - }, - ) + _client = client return _client diff --git a/tools/skills_guard.py b/tools/skills_guard.py index 0b6d7fee7..8234b0a20 100644 --- a/tools/skills_guard.py +++ b/tools/skills_guard.py @@ -29,7 +29,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import List, Tuple -from hermes_constants import OPENROUTER_BASE_URL + # --------------------------------------------------------------------------- @@ -934,24 +934,14 @@ def llm_audit_skill(skill_path: Path, static_result: ScanResult, if not model: return static_result - # Call the LLM via the OpenAI SDK (same pattern as run_agent.py) + # Call the LLM via the centralized provider router try: - from openai import OpenAI - import os + from agent.auxiliary_client import resolve_provider_client - api_key = os.getenv("OPENROUTER_API_KEY", "") - if not api_key: + client, _default_model = resolve_provider_client("openrouter") + if client is None: return static_result - client = OpenAI( - base_url=OPENROUTER_BASE_URL, - api_key=api_key, - default_headers={ - "HTTP-Referer": "https://github.com/NousResearch/hermes-agent", - "X-OpenRouter-Title": "Hermes Agent", - "X-OpenRouter-Categories": "productivity,cli-agent", - }, - ) response = client.chat.completions.create( model=model, messages=[{ diff --git a/trajectory_compressor.py b/trajectory_compressor.py index 3f49c617b..5f1c84c6a 100644 --- a/trajectory_compressor.py +++ b/trajectory_compressor.py @@ -344,38 +344,61 @@ class TrajectoryCompressor: raise RuntimeError(f"Failed to load tokenizer '{self.config.tokenizer_name}': {e}") def _init_summarizer(self): - """Initialize OpenRouter client for summarization (sync and async).""" - api_key = os.getenv(self.config.api_key_env) - if not api_key: - raise RuntimeError(f"Missing API key. Set {self.config.api_key_env} environment variable.") - - from openai import OpenAI, AsyncOpenAI - - # OpenRouter app attribution headers (only for OpenRouter endpoints) - extra = {} - if "openrouter" in self.config.base_url.lower(): - extra["default_headers"] = { - "HTTP-Referer": "https://github.com/NousResearch/hermes-agent", - "X-OpenRouter-Title": "Hermes Agent", - "X-OpenRouter-Categories": "productivity,cli-agent", - } - - # Sync client (for backwards compatibility) - self.client = OpenAI( - api_key=api_key, - base_url=self.config.base_url, - **extra, - ) - - # Async client for parallel processing - self.async_client = AsyncOpenAI( - api_key=api_key, - base_url=self.config.base_url, - **extra, - ) - - print(f"✅ Initialized OpenRouter client: {self.config.summarization_model}") + """Initialize LLM client for summarization (sync and async). + + Routes through the centralized provider router for known providers + (OpenRouter, Nous, Codex, etc.) so auth and headers are handled + consistently. Falls back to raw construction for custom endpoints. + """ + from agent.auxiliary_client import resolve_provider_client + + provider = self._detect_provider() + if provider: + # Use centralized router — handles auth, headers, Codex adapter + self.client, _ = resolve_provider_client( + provider, model=self.config.summarization_model) + self.async_client, _ = resolve_provider_client( + provider, model=self.config.summarization_model, + async_mode=True) + if self.client is None: + raise RuntimeError( + f"Provider '{provider}' is not configured. " + f"Check your API key or run: hermes setup") + else: + # Custom endpoint — use config's raw base_url + api_key_env + api_key = os.getenv(self.config.api_key_env) + if not api_key: + raise RuntimeError( + f"Missing API key. Set {self.config.api_key_env} " + f"environment variable.") + from openai import OpenAI, AsyncOpenAI + self.client = OpenAI( + api_key=api_key, base_url=self.config.base_url) + self.async_client = AsyncOpenAI( + api_key=api_key, base_url=self.config.base_url) + + print(f"✅ Initialized summarizer client: {self.config.summarization_model}") print(f" Max concurrent requests: {self.config.max_concurrent_requests}") + + def _detect_provider(self) -> str: + """Detect the provider name from the configured base_url.""" + url = self.config.base_url.lower() + if "openrouter" in url: + return "openrouter" + if "nousresearch.com" in url: + return "nous" + if "chatgpt.com/backend-api/codex" in url: + return "codex" + if "api.z.ai" in url: + return "zai" + if "moonshot.ai" in url or "api.kimi.com" in url: + return "kimi-coding" + if "minimaxi.com" in url: + return "minimax-cn" + if "minimax.io" in url: + return "minimax" + # Unknown base_url — not a known provider + return "" def count_tokens(self, text: str) -> int: """Count tokens in text using the configured tokenizer."""