refactor: route ad-hoc LLM consumers through centralized provider router
Route all remaining ad-hoc auxiliary LLM call sites through
resolve_provider_client() so auth, headers, and API format (Chat
Completions vs Responses API) are handled consistently in one place.
Files changed:
- tools/openrouter_client.py: Replace manual AsyncOpenAI construction
with resolve_provider_client('openrouter', async_mode=True). The
shared client module now delegates entirely to the router.
- tools/skills_guard.py: Replace inline OpenAI client construction
(hardcoded OpenRouter base_url, manual api_key lookup, manual
headers) with resolve_provider_client('openrouter'). Remove unused
OPENROUTER_BASE_URL import.
- trajectory_compressor.py: Add _detect_provider() to map config
base_url to a provider name, then route through
resolve_provider_client. Falls back to raw construction for
unrecognized custom endpoints.
- mini_swe_runner.py: Route default case (no explicit api_key/base_url)
through resolve_provider_client('openrouter') with auto-detection
fallback. Preserves direct construction when explicit creds are
passed via CLI args.
- agent/auxiliary_client.py: Fix stale module docstring — vision auto
mode now correctly documents that Codex and custom endpoints are
tried (not skipped).
This commit is contained in:
@@ -17,7 +17,10 @@ Resolution order for text tasks (auto mode):
|
|||||||
Resolution order for vision/multimodal tasks (auto mode):
|
Resolution order for vision/multimodal tasks (auto mode):
|
||||||
1. OpenRouter
|
1. OpenRouter
|
||||||
2. Nous Portal
|
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,
|
Per-task provider overrides (e.g. AUXILIARY_VISION_PROVIDER,
|
||||||
CONTEXT_COMPRESSION_PROVIDER) can force a specific provider for each task:
|
CONTEXT_COMPRESSION_PROVIDER) can force a specific provider for each task:
|
||||||
|
|||||||
@@ -189,29 +189,30 @@ class MiniSWERunner:
|
|||||||
)
|
)
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Initialize OpenAI client - defaults to OpenRouter
|
# Initialize LLM client via centralized provider router.
|
||||||
from openai import OpenAI
|
# If explicit api_key/base_url are provided (e.g. from CLI args),
|
||||||
|
# construct directly. Otherwise use the router for OpenRouter.
|
||||||
client_kwargs = {}
|
if api_key or base_url:
|
||||||
|
from openai import OpenAI
|
||||||
# Default to OpenRouter if no base_url provided
|
client_kwargs = {
|
||||||
if base_url:
|
"base_url": base_url or "https://openrouter.ai/api/v1",
|
||||||
client_kwargs["base_url"] = base_url
|
"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:
|
else:
|
||||||
client_kwargs["base_url"] = "https://openrouter.ai/api/v1"
|
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
|
||||||
# Handle API key - OpenRouter is the primary provider
|
self.client, _ = resolve_provider_client("auto", model=model)
|
||||||
if api_key:
|
if self.client is None:
|
||||||
client_kwargs["api_key"] = api_key
|
from openai import OpenAI
|
||||||
else:
|
self.client = OpenAI(
|
||||||
client_kwargs["api_key"] = os.getenv(
|
base_url="https://openrouter.ai/api/v1",
|
||||||
"OPENROUTER_API_KEY",
|
api_key=os.getenv("OPENROUTER_API_KEY", ""))
|
||||||
os.getenv("ANTHROPIC_API_KEY", os.getenv("OPENAI_API_KEY", ""))
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client = OpenAI(**client_kwargs)
|
|
||||||
|
|
||||||
# Environment will be created per-task
|
# Environment will be created per-task
|
||||||
self.env = None
|
self.env = None
|
||||||
|
|||||||
@@ -1,39 +1,30 @@
|
|||||||
"""Shared OpenRouter API client for Hermes tools.
|
"""Shared OpenRouter API client for Hermes tools.
|
||||||
|
|
||||||
Provides a single lazy-initialized AsyncOpenAI client that all tool modules
|
Provides a single lazy-initialized AsyncOpenAI client that all tool modules
|
||||||
can share, eliminating the duplicated _get_openrouter_client() /
|
can share. Routes through the centralized provider router in
|
||||||
_get_summarizer_client() pattern previously copy-pasted across web_tools,
|
agent/auxiliary_client.py so auth, headers, and API format are handled
|
||||||
vision_tools, mixture_of_agents_tool, and session_search_tool.
|
consistently.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from openai import AsyncOpenAI
|
_client = None
|
||||||
from hermes_constants import OPENROUTER_BASE_URL
|
|
||||||
|
|
||||||
_client: AsyncOpenAI | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_async_client() -> AsyncOpenAI:
|
def get_async_client():
|
||||||
"""Return a shared AsyncOpenAI client pointed at OpenRouter.
|
"""Return a shared async OpenAI-compatible client for OpenRouter.
|
||||||
|
|
||||||
The client is created lazily on first call and reused thereafter.
|
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.
|
Raises ValueError if OPENROUTER_API_KEY is not set.
|
||||||
"""
|
"""
|
||||||
global _client
|
global _client
|
||||||
if _client is None:
|
if _client is None:
|
||||||
api_key = os.getenv("OPENROUTER_API_KEY")
|
from agent.auxiliary_client import resolve_provider_client
|
||||||
if not api_key:
|
client, _model = resolve_provider_client("openrouter", async_mode=True)
|
||||||
|
if client is None:
|
||||||
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
||||||
_client = AsyncOpenAI(
|
_client = client
|
||||||
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",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return _client
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Tuple
|
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:
|
if not model:
|
||||||
return static_result
|
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:
|
try:
|
||||||
from openai import OpenAI
|
from agent.auxiliary_client import resolve_provider_client
|
||||||
import os
|
|
||||||
|
|
||||||
api_key = os.getenv("OPENROUTER_API_KEY", "")
|
client, _default_model = resolve_provider_client("openrouter")
|
||||||
if not api_key:
|
if client is None:
|
||||||
return static_result
|
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(
|
response = client.chat.completions.create(
|
||||||
model=model,
|
model=model,
|
||||||
messages=[{
|
messages=[{
|
||||||
|
|||||||
@@ -344,38 +344,61 @@ class TrajectoryCompressor:
|
|||||||
raise RuntimeError(f"Failed to load tokenizer '{self.config.tokenizer_name}': {e}")
|
raise RuntimeError(f"Failed to load tokenizer '{self.config.tokenizer_name}': {e}")
|
||||||
|
|
||||||
def _init_summarizer(self):
|
def _init_summarizer(self):
|
||||||
"""Initialize OpenRouter client for summarization (sync and async)."""
|
"""Initialize LLM client for summarization (sync and async).
|
||||||
api_key = os.getenv(self.config.api_key_env)
|
|
||||||
if not api_key:
|
Routes through the centralized provider router for known providers
|
||||||
raise RuntimeError(f"Missing API key. Set {self.config.api_key_env} environment variable.")
|
(OpenRouter, Nous, Codex, etc.) so auth and headers are handled
|
||||||
|
consistently. Falls back to raw construction for custom endpoints.
|
||||||
from openai import OpenAI, AsyncOpenAI
|
"""
|
||||||
|
from agent.auxiliary_client import resolve_provider_client
|
||||||
# OpenRouter app attribution headers (only for OpenRouter endpoints)
|
|
||||||
extra = {}
|
provider = self._detect_provider()
|
||||||
if "openrouter" in self.config.base_url.lower():
|
if provider:
|
||||||
extra["default_headers"] = {
|
# Use centralized router — handles auth, headers, Codex adapter
|
||||||
"HTTP-Referer": "https://github.com/NousResearch/hermes-agent",
|
self.client, _ = resolve_provider_client(
|
||||||
"X-OpenRouter-Title": "Hermes Agent",
|
provider, model=self.config.summarization_model)
|
||||||
"X-OpenRouter-Categories": "productivity,cli-agent",
|
self.async_client, _ = resolve_provider_client(
|
||||||
}
|
provider, model=self.config.summarization_model,
|
||||||
|
async_mode=True)
|
||||||
# Sync client (for backwards compatibility)
|
if self.client is None:
|
||||||
self.client = OpenAI(
|
raise RuntimeError(
|
||||||
api_key=api_key,
|
f"Provider '{provider}' is not configured. "
|
||||||
base_url=self.config.base_url,
|
f"Check your API key or run: hermes setup")
|
||||||
**extra,
|
else:
|
||||||
)
|
# Custom endpoint — use config's raw base_url + api_key_env
|
||||||
|
api_key = os.getenv(self.config.api_key_env)
|
||||||
# Async client for parallel processing
|
if not api_key:
|
||||||
self.async_client = AsyncOpenAI(
|
raise RuntimeError(
|
||||||
api_key=api_key,
|
f"Missing API key. Set {self.config.api_key_env} "
|
||||||
base_url=self.config.base_url,
|
f"environment variable.")
|
||||||
**extra,
|
from openai import OpenAI, AsyncOpenAI
|
||||||
)
|
self.client = OpenAI(
|
||||||
|
api_key=api_key, base_url=self.config.base_url)
|
||||||
print(f"✅ Initialized OpenRouter client: {self.config.summarization_model}")
|
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}")
|
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:
|
def count_tokens(self, text: str) -> int:
|
||||||
"""Count tokens in text using the configured tokenizer."""
|
"""Count tokens in text using the configured tokenizer."""
|
||||||
|
|||||||
Reference in New Issue
Block a user