From 8c83b89dd43bf8d5e31f0cc7d61559dfa27257c1 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:04:38 -0700 Subject: [PATCH 1/4] refactor(model-providers): actual reuses base fetch_models + hermes_cli normalizer; commandcode anthropic subclasses chat profile; compact router/openrouter docstrings --- plugins/model-providers/actual/__init__.py | 46 ++---------------- .../model-providers/commandcode/__init__.py | 48 +++++++------------ .../model-providers/openrouter/__init__.py | 21 +++----- plugins/model-providers/router/__init__.py | 43 +++++++---------- 4 files changed, 45 insertions(+), 113 deletions(-) diff --git a/plugins/model-providers/actual/__init__.py b/plugins/model-providers/actual/__init__.py index 9cce101ef8..3541312129 100644 --- a/plugins/model-providers/actual/__init__.py +++ b/plugins/model-providers/actual/__init__.py @@ -2,35 +2,12 @@ from __future__ import annotations -import json -import logging import os -from urllib.parse import urlparse -import urllib.request from providers import register_provider -from providers.base import ProviderProfile, _profile_user_agent - -logger = logging.getLogger(__name__) +from providers.base import ProviderProfile DEFAULT_ACTUAL_BASE_URL = "https://api.actual.inc/v1" -_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1", "0.0.0.0"} - - -def _normalize_actual_base_url(base_url: str) -> str: - """Append /v1 to a bare hosted or local host; pass anything else through.""" - url = str(base_url or "").strip().rstrip("/") - if not url: - return DEFAULT_ACTUAL_BASE_URL - try: - parsed = urlparse(url) - host = (parsed.hostname or "").lower().rstrip(".") - path = parsed.path.rstrip("/") - except Exception: - return url - if (host == "api.actual.inc" or host in _LOCAL_HOSTS) and path in {"", "/"}: - return url + "/v1" - return url class ActualProfile(ProviderProfile): @@ -40,24 +17,11 @@ class ActualProfile(ProviderProfile): def fetch_models( self, *, api_key: str | None = None, base_url: str | None = None, timeout: float = 8.0 ) -> list[str] | None: - from hermes_cli.urllib_security import open_credentialed_url + """ACTUAL_BASE_URL wins over the caller's base_url; bare hosts get ``/v1`` appended.""" + from hermes_cli.auth import normalize_actual_base_url - base_url = _normalize_actual_base_url( - os.getenv("ACTUAL_BASE_URL", "").strip() or base_url or self.base_url - ) - req = urllib.request.Request(base_url + "/models") - if api_key: - req.add_header("Authorization", f"Bearer {api_key}") - req.add_header("Accept", "application/json") - req.add_header("User-Agent", _profile_user_agent()) - try: - with open_credentialed_url(req, timeout=timeout) as resp: - data = json.loads(resp.read().decode()) - items = data if isinstance(data, list) else data.get("data", []) - return [m["id"] for m in items if isinstance(m, dict) and "id" in m] - except Exception as exc: - logger.debug("fetch_models(actual): %s", exc) - return None + base_url = normalize_actual_base_url(os.getenv("ACTUAL_BASE_URL", "").strip() or base_url or self.base_url) + return super().fetch_models(api_key=api_key, base_url=base_url, timeout=timeout) actual = ActualProfile( diff --git a/plugins/model-providers/commandcode/__init__.py b/plugins/model-providers/commandcode/__init__.py index 7191d9d4f0..960324060d 100644 --- a/plugins/model-providers/commandcode/__init__.py +++ b/plugins/model-providers/commandcode/__init__.py @@ -17,50 +17,38 @@ _COMMANDCODE_BASE = "https://api.commandcode.ai/provider/v1" _COMMANDCODE_MODELS_URL = f"{_COMMANDCODE_BASE}/models" -def _fetch_commandcode_models( - timeout: float = 10.0, - base_url: str | None = None, -) -> list[str] | None: - """Fetch model IDs from the public (unauthenticated) /models endpoint. - - The picker passes base_url unconditionally, so only a value differing from - the default counts as a customised endpoint. - """ - caller_base = (base_url or "").strip().rstrip("/") - custom = caller_base and caller_base != _COMMANDCODE_BASE - models_url = caller_base + "/models" if custom else _COMMANDCODE_MODELS_URL - try: - req = urllib.request.Request(models_url) - req.add_header("Accept", "application/json") - req.add_header("User-Agent", _profile_user_agent()) - with urllib.request.urlopen(req, timeout=timeout) as resp: - data = json.loads(resp.read().decode()) - return [m["id"] for m in data.get("data", []) if isinstance(m, dict) and "id" in m] - except Exception as exc: - logger.debug("fetch_models(commandcode): %s", exc) - return None - - class CommandCodeProfile(ProviderProfile): """CommandCode — OpenAI-compatible chat completions endpoint.""" def fetch_models( self, *, api_key: str | None = None, base_url: str | None = None, timeout: float = 8.0 ) -> list[str] | None: - return _fetch_commandcode_models(timeout=timeout, base_url=base_url) + """Public (unauthenticated) /models endpoint. The picker passes base_url + unconditionally, so only a value differing from the default is a custom endpoint.""" + caller_base = (base_url or "").strip().rstrip("/") + custom = caller_base and caller_base != _COMMANDCODE_BASE + models_url = caller_base + "/models" if custom else _COMMANDCODE_MODELS_URL + try: + req = urllib.request.Request(models_url) + req.add_header("Accept", "application/json") + req.add_header("User-Agent", _profile_user_agent()) + with urllib.request.urlopen(req, timeout=timeout) as resp: + data = json.loads(resp.read().decode()) + return [m["id"] for m in data.get("data", []) if isinstance(m, dict) and "id" in m] + except Exception as exc: + logger.debug("fetch_models(commandcode): %s", exc) + return None -class CommandCodeAnthropicProfile(ProviderProfile): +class CommandCodeAnthropicProfile(CommandCodeProfile): """CommandCode — Anthropic Messages API-compatible endpoint.""" def fetch_models( self, *, api_key: str | None = None, base_url: str | None = None, timeout: float = 8.0 ) -> list[str] | None: """Public /models endpoint, filtered to Anthropic-family models.""" - all_models = _fetch_commandcode_models(timeout=timeout, base_url=base_url) - if all_models is None: - return None - return [m for m in all_models if m.startswith("claude-")] + all_models = super().fetch_models(api_key=api_key, base_url=base_url, timeout=timeout) + return None if all_models is None else [m for m in all_models if m.startswith("claude-")] commandcode = CommandCodeProfile( diff --git a/plugins/model-providers/openrouter/__init__.py b/plugins/model-providers/openrouter/__init__.py index 20f0597ec1..f25ee79b5e 100644 --- a/plugins/model-providers/openrouter/__init__.py +++ b/plugins/model-providers/openrouter/__init__.py @@ -37,13 +37,9 @@ def _anthropic_reasoning_is_mandatory(model: str | None) -> bool: def _sticky_key(session_id: str | None) -> str | None: """Declared routing scope, then ambient conversation, then explicit session_id. - Aux call sites (compression, titles, vision, MoA…) pass no ``session_id``, - so the ambient lineage ROOT keeps them pinned to their conversation. - """ - return _cache_scope_from_session_id( - get_affinity_scope() or get_conversation_context() or session_id - ) + so the ambient lineage ROOT keeps them pinned to their conversation.""" + return _cache_scope_from_session_id(get_affinity_scope() or get_conversation_context() or session_id) class OpenRouterProfile(ProviderProfile): @@ -51,11 +47,8 @@ class OpenRouterProfile(ProviderProfile): @staticmethod def _clamp_reasoning_to_catalog(cfg: dict[str, Any], model: str | None) -> dict[str, Any]: - """Clamp ``cfg["effort"]`` to the nearest LOWER catalog-advertised level. - - No-op when the catalog is unreachable, the model is unlisted, or no - supported_efforts list is published (None = all levels accepted). - """ + """Clamp ``cfg["effort"]`` to the nearest LOWER catalog-advertised level. No-op when the + catalog is unreachable, the model is unlisted, or no supported_efforts list is published.""" effort = cfg.get("effort") if not effort or cfg.get("enabled") is False: return cfg @@ -80,8 +73,8 @@ class OpenRouterProfile(ProviderProfile): def fetch_models( self, *, api_key: str | None = None, base_url: str | None = None, timeout: float = 8.0 ) -> list[str] | None: - """Fetch from the public OpenRouter catalog (no auth). Tool-call filtering - happens in hermes_cli/models.py, which the picker reaches first.""" + """Public OpenRouter catalog (no auth), cached per process. Tool-call + filtering happens in hermes_cli/models.py, which the picker reaches first.""" global _CACHE # noqa: PLW0603 if _CACHE is not None: return _CACHE @@ -104,7 +97,6 @@ class OpenRouterProfile(ProviderProfile): prefs = context.get("provider_preferences") if prefs: body["provider"] = prefs - # Pareto Code router plugin is only meaningful for openrouter/pareto-code. score = context.get("openrouter_min_coding_score") if (context.get("model") or "") == "openrouter/pareto-code" and score is not None and score != "": @@ -143,7 +135,6 @@ class OpenRouterProfile(ProviderProfile): extra_body["reasoning"] = self._clamp_reasoning_to_catalog(dict(reasoning_config), model) else: extra_body["reasoning"] = {"enabled": True, "effort": "medium"} - # xAI's prompt cache is pinned per backend server via this header. grok_conv_id = _sticky_key(session_id) if grok_conv_id and model and model.startswith(("x-ai/grok-", "xai/grok-")): diff --git a/plugins/model-providers/router/__init__.py b/plugins/model-providers/router/__init__.py index ee20972fdb..9604d354fa 100644 --- a/plugins/model-providers/router/__init__.py +++ b/plugins/model-providers/router/__init__.py @@ -1,18 +1,12 @@ """Ramp Router (router.com) provider profile: Responses-only LLM gateway. -Wire notes (verified live against api.router.com): -* Responses API is the native wire; ``/chat/completions`` is only a thin shim. - ``api_mode="codex_responses"`` plus the ``api.router.com`` host mandate in - ``hermes_cli/providers.py`` keep every path on it. -* The catalog is account-scoped (BYOK accounts see extra IDs), so this profile - ships no ``fallback_models`` — the picker relies on ``fetch_models()``. -* Router 400s on ``reasoning.effort`` levels outside a model's published - vocabulary and on any reasoning field for non-reasoning models. The efforts - map from ``GET /v1/models`` is cached (memory + disk mirror, background - warmer; never HTTP on the request hot path) and fed to the codex transport's - clamp via ``supported_reasoning_efforts``. -* ``store: false``, ``prompt_cache_key``, encrypted reasoning replay, tools and - streaming pass through unchanged — no Router-specific request surgery. +Wire notes (verified live): the Responses API is the native wire (``api_mode= +"codex_responses"`` + the ``api.router.com`` host mandate in ``hermes_cli/providers.py``); +the catalog is account-scoped so there are no ``fallback_models`` (picker uses +``fetch_models()``); Router 400s on ``reasoning.effort`` levels outside a model's +published vocabulary and on any reasoning field for non-reasoning models, so the +efforts map from ``GET /v1/models`` is cached (memory + disk mirror, background +warmer; never HTTP on the request hot path) and fed to the codex transport's clamp. """ from __future__ import annotations @@ -51,8 +45,8 @@ def _base_url() -> str: def _resolve_api_key() -> str: - """Resolve the Router key (documented var, then alias), preferring dotenv; - plain os.environ is the fallback when the dotenv resolver is unavailable or raises.""" + """Router key (documented var, then alias), preferring dotenv; plain os.environ + is the fallback when the dotenv resolver is unavailable or raises.""" resolvers: list = [lambda var: os.environ.get(var, "")] try: from hermes_cli.config import get_env_value_prefer_dotenv @@ -81,10 +75,9 @@ def _dig(obj: Any, *keys: str) -> Any: def _parse_efforts(items: Any) -> Optional[dict[str, list[str]]]: """Parse a ``/v1/models`` ``data`` array into the efforts map (None if unusable). - Ladder-unknown levels are dropped: clamp_effort ignores them, so an - all-unknown vocabulary would pass the effort through unclamped to a Router - 400. ``supported=True`` with no recognized level leaves the model out - (unknown) so the transport keeps its default clamp behavior. + Ladder-unknown levels are dropped: clamp_effort ignores them, so an all-unknown + vocabulary would pass the effort through unclamped to a Router 400. ``supported=True`` + with no recognized level leaves the model out (unknown -> transport default clamp). """ if not isinstance(items, list): return None @@ -166,9 +159,7 @@ def _seed_efforts(items: Any) -> Optional[dict[str, list[str]]]: return parsed -def _fetch_catalog_items( - *, api_key: str = "", base_url: str = "", timeout: float = 8.0 -) -> Optional[list]: +def _fetch_catalog_items(*, api_key: str = "", base_url: str = "", timeout: float = 8.0) -> Optional[list]: """Fetch the raw ``/v1/models`` ``data`` array. None on any failure.""" import urllib.request @@ -179,8 +170,7 @@ def _fetch_catalog_items( if key: req.add_header("Authorization", f"Bearer {key}") req.add_header("Accept", "application/json") - # Router's WAF rejects the default Python-urllib UA. - req.add_header("User-Agent", _profile_user_agent()) + req.add_header("User-Agent", _profile_user_agent()) # Router's WAF rejects the default urllib UA try: with open_credentialed_url(req, timeout=timeout) as resp: data = json.loads(resp.read().decode()) @@ -244,7 +234,7 @@ class RouterProfile(ProviderProfile): def fetch_models( self, *, api_key: Optional[str] = None, base_url: Optional[str] = None, timeout: float = 8.0 ) -> Optional[list[str]]: - """Fetch the live, key-scoped catalog; the same payload seeds the caps cache. + """Live, key-scoped catalog; the same payload seeds the caps cache. Deduped but not sorted: Router's listing order is deliberate presentation.""" items = _fetch_catalog_items(api_key=api_key or "", base_url=base_url or "", timeout=timeout) if items is None: @@ -279,8 +269,7 @@ router = RouterProfile( default_headers={"User-Agent": f"Hermes-Agent/{_HERMES_VERSION}"}, supports_vision=True, default_aux_model="gpt-5.4-mini", - # Empty on purpose: model IDs are account-scoped; the picker uses fetch_models(). - fallback_models=(), + fallback_models=(), # account-scoped IDs; the picker uses fetch_models() ) register_provider(router) From 9338e21093cbd7674ce186aa982851e348553d54 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:36:03 -0700 Subject: [PATCH 2/4] refactor(model-providers): compact reasoning-translation profiles (opencode, kimi, zai, minimax, nous, qwen, custom, upstage, nebius, meta-ai, deepseek, copilot, deepinfra, vertex, gemini), fold catalog tuples --- .../model-providers/ai-gateway/__init__.py | 5 +-- plugins/model-providers/alibaba/__init__.py | 5 ++- plugins/model-providers/anthropic/__init__.py | 5 ++- .../model-providers/commandcode/__init__.py | 14 ++------ .../model-providers/copilot-acp/__init__.py | 14 ++++---- plugins/model-providers/copilot/__init__.py | 15 +++------ plugins/model-providers/custom/__init__.py | 8 ++--- plugins/model-providers/deepinfra/__init__.py | 12 +++---- plugins/model-providers/deepseek/__init__.py | 7 ++-- plugins/model-providers/fireworks/__init__.py | 3 +- plugins/model-providers/gmi/__init__.py | 8 ++--- .../model-providers/kimi-coding/__init__.py | 32 +++++++------------ plugins/model-providers/meta-ai/__init__.py | 23 ++++--------- plugins/model-providers/minimax/__init__.py | 25 ++++----------- .../nebius-token-factory/__init__.py | 29 ++++------------- plugins/model-providers/nous/__init__.py | 25 ++++----------- plugins/model-providers/novita/__init__.py | 8 ++--- plugins/model-providers/nvidia/__init__.py | 6 +--- .../model-providers/opencode-free/__init__.py | 11 +++---- .../model-providers/opencode-zen/__init__.py | 20 ++++-------- .../model-providers/openrouter/__init__.py | 5 +-- .../model-providers/qwen-oauth/__init__.py | 11 ++----- plugins/model-providers/router/__init__.py | 11 +++---- plugins/model-providers/upstage/__init__.py | 16 ++++------ plugins/model-providers/vertex/__init__.py | 22 ++++--------- plugins/model-providers/zai/__init__.py | 10 ++---- 26 files changed, 105 insertions(+), 245 deletions(-) diff --git a/plugins/model-providers/ai-gateway/__init__.py b/plugins/model-providers/ai-gateway/__init__.py index a89c7c2d33..33a4c73ec5 100644 --- a/plugins/model-providers/ai-gateway/__init__.py +++ b/plugins/model-providers/ai-gateway/__init__.py @@ -23,10 +23,7 @@ vercel = VercelAIGatewayProfile( aliases=("vercel", "vercel-ai-gateway", "ai_gateway", "aigateway"), env_vars=("AI_GATEWAY_API_KEY",), base_url="https://ai-gateway.vercel.sh/v1", - default_headers={ - "HTTP-Referer": "https://hermes-agent.nousresearch.com", - "X-Title": "Hermes Agent", - }, + default_headers={"HTTP-Referer": "https://hermes-agent.nousresearch.com", "X-Title": "Hermes Agent"}, default_aux_model="google/gemini-3-flash", ) diff --git a/plugins/model-providers/alibaba/__init__.py b/plugins/model-providers/alibaba/__init__.py index 6ed2f58c45..7dd5519e62 100644 --- a/plugins/model-providers/alibaba/__init__.py +++ b/plugins/model-providers/alibaba/__init__.py @@ -24,9 +24,6 @@ alibaba_cn = ProviderProfile( base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", ) -register_provider(alibaba) -register_provider(alibaba_cn) - alibaba_token_plan = ProviderProfile( name="alibaba-token-plan", aliases=("dashscope-token-plan",), @@ -49,5 +46,7 @@ alibaba_token_plan_cn = ProviderProfile( auth_type="api_key", ) +register_provider(alibaba) +register_provider(alibaba_cn) register_provider(alibaba_token_plan) register_provider(alibaba_token_plan_cn) diff --git a/plugins/model-providers/anthropic/__init__.py b/plugins/model-providers/anthropic/__init__.py index fbe5269f9f..1647e16b77 100644 --- a/plugins/model-providers/anthropic/__init__.py +++ b/plugins/model-providers/anthropic/__init__.py @@ -22,9 +22,8 @@ class AnthropicProfile(ProviderProfile): return None try: req = urllib.request.Request("https://api.anthropic.com/v1/models") - req.add_header("x-api-key", api_key) - req.add_header("anthropic-version", "2023-06-01") - req.add_header("Accept", "application/json") + for k, v in (("x-api-key", api_key), ("anthropic-version", "2023-06-01"), ("Accept", "application/json")): + req.add_header(k, v) with open_credentialed_url(req, timeout=timeout) as resp: data = json.loads(resp.read().decode()) return [m["id"] for m in data.get("data", []) if isinstance(m, dict) and "id" in m] diff --git a/plugins/model-providers/commandcode/__init__.py b/plugins/model-providers/commandcode/__init__.py index 960324060d..e4eb2dc498 100644 --- a/plugins/model-providers/commandcode/__init__.py +++ b/plugins/model-providers/commandcode/__init__.py @@ -64,17 +64,9 @@ commandcode = CommandCodeProfile( base_url=_COMMANDCODE_BASE, models_url=_COMMANDCODE_MODELS_URL, fallback_models=( - "deepseek/deepseek-v4-pro", - "deepseek/deepseek-v4-flash", - "Qwen/Qwen3.7-Max", - "Qwen/Qwen3.6-Plus", - "moonshotai/Kimi-K2.6", - "zai-org/GLM-5.1", - "MiniMaxAI/MiniMax-M2.7", - "stepfun/Step-3.5-Flash", - "xiaomi/mimo-v2.5-pro", - "google/gemini-3.5-flash", - "gpt-5.5", + "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash", "Qwen/Qwen3.7-Max", "Qwen/Qwen3.6-Plus", + "moonshotai/Kimi-K2.6", "zai-org/GLM-5.1", "MiniMaxAI/MiniMax-M2.7", "stepfun/Step-3.5-Flash", + "xiaomi/mimo-v2.5-pro", "google/gemini-3.5-flash", "gpt-5.5", ), default_aux_model="deepseek/deepseek-v4-flash", ) diff --git a/plugins/model-providers/copilot-acp/__init__.py b/plugins/model-providers/copilot-acp/__init__.py index ef7d90ec46..86d8ee3009 100644 --- a/plugins/model-providers/copilot-acp/__init__.py +++ b/plugins/model-providers/copilot-acp/__init__.py @@ -1,10 +1,9 @@ """GitHub Copilot ACP provider profile. -copilot-acp does not speak OpenAI-over-HTTP: it drives an external ACP -subprocess over stdio, so the profile supplies its own client via -:meth:`ProviderProfile.create_client`. An out-of-tree ACP provider registered -from ``~/.hermes/plugins/model-providers/`` or a pip entry point uses the same -three lines without touching core. +copilot-acp does not speak OpenAI-over-HTTP: it drives an external ACP subprocess over +stdio, so the profile supplies its own client via :meth:`ProviderProfile.create_client`. +An out-of-tree ACP provider (``~/.hermes/plugins/model-providers/`` or a pip entry point) +uses the same three lines without touching core. """ from typing import Any @@ -36,9 +35,8 @@ copilot_acp = CopilotACPProfile( env_vars=(), # Managed by ACP subprocess base_url="acp://copilot", # ACP internal scheme auth_type="external_process", - # How to launch the CLI. Previously hardcoded in - # hermes_cli/auth.py::resolve_external_process_provider_credentials; the env - # var names are unchanged, so existing setups keep working. + # How to launch the CLI; env var names predate this profile (formerly hardcoded in + # hermes_cli/auth.py), so existing setups keep working. process_command="copilot", process_args=("--acp", "--stdio"), process_command_env_vars=("HERMES_COPILOT_ACP_COMMAND", "COPILOT_CLI_PATH"), diff --git a/plugins/model-providers/copilot/__init__.py b/plugins/model-providers/copilot/__init__.py index 903ef2b3f9..23d21c4dd4 100644 --- a/plugins/model-providers/copilot/__init__.py +++ b/plugins/model-providers/copilot/__init__.py @@ -15,12 +15,8 @@ class CopilotProfile(ProviderProfile): """GitHub Copilot / GitHub Models — editor headers + reasoning.""" def build_api_kwargs_extras( - self, - *, - model: str | None = None, - reasoning_config: dict | None = None, - supports_reasoning: bool = False, - **ctx, + self, *, model: str | None = None, reasoning_config: dict | None = None, + supports_reasoning: bool = False, **ctx, ) -> tuple[dict[str, Any], dict[str, Any]]: if not (supports_reasoning and model): return {}, {} @@ -33,10 +29,9 @@ class CopilotProfile(ProviderProfile): if not reasoning_config: return {"reasoning": {"effort": "medium"}}, {} effort = reasoning_config.get("effort", "medium") - # Honor a level the live catalog lists; otherwise clamp to the nearest - # WEAKER supported level (never drop straight to medium, which inverted - # the ladder: ultra < high). Bespoke levels the ladder can't place fall - # to medium (or the first supported level). + # Honor a level the live catalog lists; otherwise clamp to the nearest WEAKER + # supported level (never drop straight to medium, which inverted the ladder: + # ultra < high). Bespoke levels the ladder can't place fall to medium (or [0]). if effort not in supported: effort = clamp_reasoning_effort_to_supported(effort, list(supported)) if effort not in supported: diff --git a/plugins/model-providers/custom/__init__.py b/plugins/model-providers/custom/__init__.py index 7260096833..cc8c0bb445 100644 --- a/plugins/model-providers/custom/__init__.py +++ b/plugins/model-providers/custom/__init__.py @@ -11,16 +11,13 @@ from providers.base import ProviderProfile def _looks_like_ollama_endpoint(base_url: str | None) -> bool: """True only for explicit Ollama signatures (port 11434 or an ``ollama`` host label). - ``think`` is Ollama-native; strict hosts (Mistral, Groq) 422 on it, and - arbitrary localhost may be llama.cpp / vLLM / LM Studio. - """ + arbitrary localhost may be llama.cpp / vLLM / LM Studio.""" raw = (base_url or "").strip() if not raw: return False parsed = urlparse(raw if "://" in raw else f"//{raw}") - # urlparse raises ValueError on malformed ports ("host:99999"); treat as not-Ollama. - try: + try: # urlparse raises ValueError on malformed ports ("host:99999"); treat as not-Ollama. if parsed.port == 11434: return True except ValueError: @@ -39,7 +36,6 @@ class CustomProfile(ProviderProfile): top_level: dict[str, Any] = {} if ollama_num_ctx: extra_body["options"] = {"num_ctx": ollama_num_ctx} - # disabled -> top-level reasoning_effort="none" (Ollama's /v1 ignores # extra_body.think) plus think=False only on Ollama URLs; enabled+effort -> # top-level reasoning_effort clamped to the OpenAI-compat wire (GLM/ARK, diff --git a/plugins/model-providers/deepinfra/__init__.py b/plugins/model-providers/deepinfra/__init__.py index f57077f43b..61c6bbdd75 100644 --- a/plugins/model-providers/deepinfra/__init__.py +++ b/plugins/model-providers/deepinfra/__init__.py @@ -10,12 +10,9 @@ class _DeepInfraProfile(ProviderProfile): resolution in ``agent/auxiliary_client.py`` stays provider-agnostic.""" def default_vision_model(self): # type: ignore[override] - """First vision-capable *chat* model from the live catalog, or None. - - Key-gated so a box without DEEPINFRA_API_KEY never pays the round-trip. - Requires the ``chat`` surface tag so an image-gen model carrying a - ``vision`` tag can't be picked as a chat-completions vision backend. - """ + """First vision-capable *chat* model from the live catalog, or None. Key-gated so a box + without DEEPINFRA_API_KEY never pays the round-trip; requires the ``chat`` surface tag so + an image-gen model carrying a ``vision`` tag can't be picked as a chat vision backend.""" from agent.secret_scope import get_secret if not (get_secret("DEEPINFRA_API_KEY") or "").strip(): @@ -42,8 +39,7 @@ deepinfra = _DeepInfraProfile( env_vars=("DEEPINFRA_API_KEY", "DEEPINFRA_BASE_URL"), base_url="https://api.deepinfra.com/v1/openai", auth_type="api_key", - # No provider-wide cap: DeepInfra applies its documented per-model limit. - default_max_tokens=None, + default_max_tokens=None, # DeepInfra applies its documented per-model limit # The only hardcoded DeepInfra model: aux resolution is synchronous, so it # can't wait on a catalog round-trip. Everything else is discovered live. default_aux_model="deepseek-ai/DeepSeek-V4-Flash", diff --git a/plugins/model-providers/deepseek/__init__.py b/plugins/model-providers/deepseek/__init__.py index b669f66595..e253863d83 100644 --- a/plugins/model-providers/deepseek/__init__.py +++ b/plugins/model-providers/deepseek/__init__.py @@ -24,12 +24,11 @@ class DeepSeekProfile(ProviderProfile): self, *, reasoning_config: dict | None = None, model: str | None = None, **context ) -> tuple[dict[str, Any], dict[str, Any]]: m = (model or "").strip().lower() - # deepseek-v4-* and every later generation; v3 explicitly excluded. - if not m.startswith("deepseek-v") or m.startswith("deepseek-v3"): + if not m.startswith("deepseek-v") or m.startswith("deepseek-v3"): # v4+ only; v3 excluded return {}, {} rc = reasoning_config if isinstance(reasoning_config, dict) else None - # Always set explicitly (default enabled, matching the API default) to - # avoid the reasoning_content echo trap on subsequent turns. + # Always set thinking explicitly (default enabled, matching the API default) + # to avoid the reasoning_content echo trap on subsequent turns. if rc is not None and rc.get("enabled") is False: return {"thinking": {"type": "disabled"}}, {} top_level: dict[str, Any] = {} diff --git a/plugins/model-providers/fireworks/__init__.py b/plugins/model-providers/fireworks/__init__.py index 92a39c9cd8..6e681b3af3 100644 --- a/plugins/model-providers/fireworks/__init__.py +++ b/plugins/model-providers/fireworks/__init__.py @@ -25,8 +25,7 @@ fireworks = ProviderProfile( default_aux_model="accounts/fireworks/models/glm-5p2", # Picker safety net when the live catalog fetch fails. fallback_models=( - "accounts/fireworks/models/kimi-k2p6", - "accounts/fireworks/models/glm-5p2", + "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/kimi-k2p7-code", ), ) diff --git a/plugins/model-providers/gmi/__init__.py b/plugins/model-providers/gmi/__init__.py index 307b657a53..2009d78598 100644 --- a/plugins/model-providers/gmi/__init__.py +++ b/plugins/model-providers/gmi/__init__.py @@ -17,12 +17,8 @@ gmi = ProviderProfile( default_headers={"User-Agent": f"HermesAgent/{_HERMES_VERSION}"}, default_aux_model="google/gemini-3.1-flash-lite-preview", fallback_models=( - "zai-org/GLM-5.1-FP8", - "deepseek-ai/DeepSeek-V3.2", - "moonshotai/Kimi-K2.5", - "google/gemini-3.1-flash-lite-preview", - "anthropic/claude-sonnet-5", - "anthropic/claude-sonnet-4.6", + "zai-org/GLM-5.1-FP8", "deepseek-ai/DeepSeek-V3.2", "moonshotai/Kimi-K2.5", + "google/gemini-3.1-flash-lite-preview", "anthropic/claude-sonnet-5", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4", ), ) diff --git a/plugins/model-providers/kimi-coding/__init__.py b/plugins/model-providers/kimi-coding/__init__.py index 1e9525cdc1..256a6a8d00 100644 --- a/plugins/model-providers/kimi-coding/__init__.py +++ b/plugins/model-providers/kimi-coding/__init__.py @@ -17,7 +17,7 @@ _HEADERS = { def _is_confirmed_kimi_coding_url(base_url: str) -> bool: - """Return True only for Kimi Code's canonical HTTPS API surfaces.""" + """True only for Kimi Code's canonical HTTPS API surfaces.""" try: p = urlparse(base_url) port = p.port @@ -61,27 +61,17 @@ class KimiProfile(ProviderProfile): return {"thinking": {"type": "enabled"}}, {} -kimi = KimiProfile( - name="kimi-coding", - aliases=("kimi", "moonshot", "kimi-for-coding"), - env_vars=("KIMI_API_KEY", "KIMI_CODING_API_KEY"), - base_url="https://api.moonshot.ai/v1", - fixed_temperature=OMIT_TEMPERATURE, - default_max_tokens=32000, - default_headers=dict(_HEADERS), - default_aux_model="kimi-k2-turbo-preview", -) +def _kimi(name: str, aliases: tuple, env_vars: tuple, base_url: str) -> KimiProfile: + return KimiProfile( + name=name, aliases=aliases, env_vars=env_vars, base_url=base_url, + fixed_temperature=OMIT_TEMPERATURE, default_max_tokens=32000, + default_headers=dict(_HEADERS), default_aux_model="kimi-k2-turbo-preview", + ) -kimi_cn = KimiProfile( - name="kimi-coding-cn", - aliases=("kimi-cn", "moonshot-cn"), - env_vars=("KIMI_CN_API_KEY",), - base_url="https://api.moonshot.cn/v1", - fixed_temperature=OMIT_TEMPERATURE, - default_max_tokens=32000, - default_headers=dict(_HEADERS), - default_aux_model="kimi-k2-turbo-preview", -) + +kimi = _kimi("kimi-coding", ("kimi", "moonshot", "kimi-for-coding"), ("KIMI_API_KEY", "KIMI_CODING_API_KEY"), + "https://api.moonshot.ai/v1") +kimi_cn = _kimi("kimi-coding-cn", ("kimi-cn", "moonshot-cn"), ("KIMI_CN_API_KEY",), "https://api.moonshot.cn/v1") register_provider(kimi) register_provider(kimi_cn) diff --git a/plugins/model-providers/meta-ai/__init__.py b/plugins/model-providers/meta-ai/__init__.py index f349f8461f..061a5da9b4 100644 --- a/plugins/model-providers/meta-ai/__init__.py +++ b/plugins/model-providers/meta-ai/__init__.py @@ -20,18 +20,11 @@ class MetaAIProfile(ProviderProfile): """Meta Model API — top-level reasoning_effort, self-contained.""" def build_api_kwargs_extras( - self, - *, - reasoning_config: dict | None = None, - supports_reasoning: bool = False, # noqa: ARG002 — we self-gate below - **context: Any, + self, *, reasoning_config: dict | None = None, supports_reasoning: bool = False, **context: Any ) -> tuple[dict[str, Any], dict[str, Any]]: - """Ignores the core ``supports_reasoning`` gate (host-allowlist driven); - Muse Spark always accepts ``reasoning_effort``. - - Muse 400s on ``none``: disabled/"none" -> ``minimal`` (closest to off); - unset/bespoke levels -> ``medium``. - """ + """Ignores the core ``supports_reasoning`` gate (host-allowlist driven); Muse Spark always + accepts ``reasoning_effort``. Muse 400s on ``none``: disabled/"none" -> ``minimal`` + (closest to off); unset/bespoke levels -> ``medium``.""" rc = reasoning_config or {} effort = str(rc.get("effort") or "").strip().lower() if rc.get("enabled") is False or effort == "none": @@ -52,14 +45,12 @@ meta_ai = MetaAIProfile( env_vars=("MODEL_API_KEY", "META_API_KEY", "META_MODEL_API_KEY", "META_BASE_URL"), base_url=os.getenv("META_BASE_URL", "").strip() or "https://api.meta.ai/v1", auth_type="api_key", - # Responses API engages Muse prompt caching (0 cached tokens on - # chat/completions vs 93-99% hits on /v1/responses); the chat-completions - # hook above still covers custom non-api.meta.ai base URLs. + # Responses API engages Muse prompt caching (0 cached tokens on chat/completions vs + # 93-99% hits on /v1/responses); the hook above still covers custom non-api.meta.ai base URLs. api_mode="codex_responses", supports_vision=True, default_aux_model="muse-spark-1.2-contributor", - # Muse spends completion budget on hidden reasoning first; low caps can - # finish with empty content. + # Muse spends completion budget on hidden reasoning first; low caps can finish with empty content. default_max_tokens=16384, fallback_models=("muse-spark-1.2", "muse-spark-1.2-contributor"), ) diff --git a/plugins/model-providers/minimax/__init__.py b/plugins/model-providers/minimax/__init__.py index 00dcbe2f66..896bbc876b 100644 --- a/plugins/model-providers/minimax/__init__.py +++ b/plugins/model-providers/minimax/__init__.py @@ -21,12 +21,8 @@ class MiniMaxProfile(ProviderProfile): """MiniMax — M3 OpenAI-compatible reasoning controls.""" def build_api_kwargs_extras( - self, - *, - reasoning_config: dict | None = None, - model: str | None = None, - base_url: str | None = None, - **context: Any, + self, *, reasoning_config: dict | None = None, model: str | None = None, + base_url: str | None = None, **context: Any, ) -> tuple[dict[str, Any], dict[str, Any]]: """M3 on api.minimax.io/v1 keeps thinking inline unless ``reasoning_split`` is sent; effort levels only select adaptive vs disabled ``thinking``.""" @@ -42,22 +38,13 @@ class MiniMaxProfile(ProviderProfile): minimax = MiniMaxProfile( - name="minimax", - aliases=("mini-max",), - api_mode="anthropic_messages", - env_vars=("MINIMAX_API_KEY",), - base_url="https://api.minimax.io/anthropic", - auth_type="api_key", - default_aux_model="MiniMax-M3", + name="minimax", aliases=("mini-max",), api_mode="anthropic_messages", env_vars=("MINIMAX_API_KEY",), + base_url="https://api.minimax.io/anthropic", auth_type="api_key", default_aux_model="MiniMax-M3", ) minimax_cn = MiniMaxProfile( - name="minimax-cn", - aliases=("minimax-china", "minimax_cn"), - api_mode="anthropic_messages", - env_vars=("MINIMAX_CN_API_KEY",), - base_url="https://api.minimaxi.com/anthropic", - auth_type="api_key", + name="minimax-cn", aliases=("minimax-china", "minimax_cn"), api_mode="anthropic_messages", + env_vars=("MINIMAX_CN_API_KEY",), base_url="https://api.minimaxi.com/anthropic", auth_type="api_key", default_aux_model="MiniMax-M3", ) diff --git a/plugins/model-providers/nebius-token-factory/__init__.py b/plugins/model-providers/nebius-token-factory/__init__.py index 198dd8d21a..0e1b6d7743 100644 --- a/plugins/model-providers/nebius-token-factory/__init__.py +++ b/plugins/model-providers/nebius-token-factory/__init__.py @@ -10,14 +10,7 @@ from providers.base import ProviderProfile # Conservative allowlist of model families that expose reasoning effort. _REASONING_MARKERS = ( - "deepseek-r1", - "deepseek-v4", - "deepseek-reasoner", - "gpt-oss", - "glm-5", - "kimi-k2", - "minimax-m2", - "qwen3", + "deepseek-r1", "deepseek-v4", "deepseek-reasoner", "gpt-oss", "glm-5", "kimi-k2", "minimax-m2", "qwen3", ) @@ -25,12 +18,8 @@ class NebiusTokenFactoryProfile(ProviderProfile): """Nebius Token Factory - top-level reasoning_effort.""" def build_api_kwargs_extras( - self, - *, - reasoning_config: dict | None = None, - model: str | None = None, - supports_reasoning: bool = False, - **context: Any, + self, *, reasoning_config: dict | None = None, model: str | None = None, + supports_reasoning: bool = False, **context: Any, ) -> tuple[dict[str, Any], dict[str, Any]]: model_name = (model or "").strip().rsplit("/", 1)[-1].lower() if not supports_reasoning and not any(marker in model_name for marker in _REASONING_MARKERS): @@ -56,15 +45,9 @@ nebius_token_factory = NebiusTokenFactoryProfile( auth_type="api_key", default_aux_model="nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B", fallback_models=( - "Qwen/Qwen3.5-397B-A17B-fast", - "deepseek-ai/DeepSeek-V4-Pro", - "zai-org/GLM-5.1", - "moonshotai/Kimi-K2.5-fast", - "MiniMaxAI/MiniMax-M2.5-fast", - "deepseek-ai/DeepSeek-V3.2-fast", - "NousResearch/Hermes-4-70B", - "openai/gpt-oss-120b-fast", - "meta-llama/Llama-3.3-70B-Instruct", + "Qwen/Qwen3.5-397B-A17B-fast", "deepseek-ai/DeepSeek-V4-Pro", "zai-org/GLM-5.1", "moonshotai/Kimi-K2.5-fast", + "MiniMaxAI/MiniMax-M2.5-fast", "deepseek-ai/DeepSeek-V3.2-fast", "NousResearch/Hermes-4-70B", + "openai/gpt-oss-120b-fast", "meta-llama/Llama-3.3-70B-Instruct", ), ) diff --git a/plugins/model-providers/nous/__init__.py b/plugins/model-providers/nous/__init__.py index 24df5f8503..5cbd3ebad9 100644 --- a/plugins/model-providers/nous/__init__.py +++ b/plugins/model-providers/nous/__init__.py @@ -2,11 +2,7 @@ from typing import Any -from agent.portal_tags import ( - get_affinity_scope, - get_conversation_context, - nous_portal_tags, -) +from agent.portal_tags import get_affinity_scope, get_conversation_context, nous_portal_tags from agent.transports.codex import _cache_scope_from_session_id from providers import register_provider from providers.base import ProviderProfile @@ -30,9 +26,7 @@ class NousProfile(ProviderProfile): # breakpoints stay warm on one upstream instance. Resolved like the # ``conversation=`` tag: declared scope, then the ambient lineage ROOT # (covers aux call sites that pass no session_id), then the explicit argument. - sticky_key = _cache_scope_from_session_id( - get_affinity_scope() or get_conversation_context() or session_id - ) + sticky_key = _cache_scope_from_session_id(get_affinity_scope() or get_conversation_context() or session_id) if sticky_key: body["session_id"] = sticky_key provider_preferences = context.get("provider_preferences") @@ -42,11 +36,8 @@ class NousProfile(ProviderProfile): @staticmethod def _cannot_disable_reasoning(model: str | None) -> bool: - """True when ``reasoning: {enabled: false}`` would 400 on *model*. - - Cache-only catalog lookup; unknown/cold (warmer kicked) and - no-reasoning-parameter routes both answer True (omit rather than risk a 400). - """ + """True when ``reasoning: {enabled: false}`` would 400 on *model*. Cache-only catalog + lookup; unknown/cold (warmer kicked) and no-reasoning routes both answer True (omit > 400).""" try: from hermes_cli.models import nous_model_reasoning_capabilities, warm_nous_reasoning_caps_async @@ -59,12 +50,8 @@ class NousProfile(ProviderProfile): return not caps.get("supports_reasoning") or bool(caps.get("mandatory")) def build_api_kwargs_extras( - self, - *, - reasoning_config: dict | None = None, - supports_reasoning: bool = False, - model: str | None = None, - **context, + self, *, reasoning_config: dict | None = None, supports_reasoning: bool = False, + model: str | None = None, **context, ) -> tuple[dict[str, Any], dict[str, Any]]: """Pass the full reasoning_config, disable included (the Portal honors it; omitting it means the upstream default, thinking ON for V4-class models).""" diff --git a/plugins/model-providers/novita/__init__.py b/plugins/model-providers/novita/__init__.py index e49e289a0d..42a1dabafd 100644 --- a/plugins/model-providers/novita/__init__.py +++ b/plugins/model-providers/novita/__init__.py @@ -15,12 +15,8 @@ novita = ProviderProfile( auth_type="api_key", default_aux_model="deepseek/deepseek-v3-0324", fallback_models=( - "moonshotai/kimi-k2.5", - "minimax/minimax-m2.7", - "zai-org/glm-5", - "deepseek/deepseek-v3-0324", - "deepseek/deepseek-r1-0528", - "qwen/qwen3-235b-a22b-fp8", + "moonshotai/kimi-k2.5", "minimax/minimax-m2.7", "zai-org/glm-5", "deepseek/deepseek-v3-0324", + "deepseek/deepseek-r1-0528", "qwen/qwen3-235b-a22b-fp8", ), ) diff --git a/plugins/model-providers/nvidia/__init__.py b/plugins/model-providers/nvidia/__init__.py index abc81f45c0..1d48a225c7 100644 --- a/plugins/model-providers/nvidia/__init__.py +++ b/plugins/model-providers/nvidia/__init__.py @@ -11,11 +11,7 @@ class NvidiaProviderProfile(ProviderProfile): @staticmethod def _needs_strip(msg: Any) -> bool: - return ( - isinstance(msg, dict) - and msg.get("role") == "tool" - and ("name" in msg or "tool_name" in msg) - ) + return isinstance(msg, dict) and msg.get("role") == "tool" and ("name" in msg or "tool_name" in msg) def prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Copy-on-write: only tool messages that lose a field are copied diff --git a/plugins/model-providers/opencode-free/__init__.py b/plugins/model-providers/opencode-free/__init__.py index d8251415cc..9778b6a3a5 100644 --- a/plugins/model-providers/opencode-free/__init__.py +++ b/plugins/model-providers/opencode-free/__init__.py @@ -16,9 +16,9 @@ from providers.base import ProviderProfile class OpenCodeFreeProfile(ProviderProfile): """OpenCode Free — keyless, with Ox Alpha reasoning controls. - Ox Alpha (x-preview-f-free) is also reachable via opencode-zen with the same - wire contract; the translation lives in the zen plugin and is resolved through - the registered zen profile's module so the two providers can never drift. + Ox Alpha (x-preview-f-free) is also reachable via opencode-zen with the same wire + contract; the translation lives in the zen plugin and is resolved through the + registered zen profile's module so the two providers can never drift. """ def build_api_kwargs_extras( @@ -42,9 +42,8 @@ opencode_free = OpenCodeFreeProfile( base_url="https://opencode.ai/zen/v1", display_name="OpenCode Free", description="OpenCode free models — keyless, no account needed", - # Attribution headers (same values as opencode-zen/go) plus the empty - # Authorization override that keeps the SDK's "Bearer " off the - # wire (the free tier 401s any unrecognized bearer). + # Attribution headers (same values as opencode-zen/go) plus the empty Authorization + # override that keeps the SDK's "Bearer " off the wire (free tier 401s it). default_headers={ "Authorization": "", "HTTP-Referer": "https://hermes-agent.nousresearch.com", diff --git a/plugins/model-providers/opencode-zen/__init__.py b/plugins/model-providers/opencode-zen/__init__.py index b8601771f6..dba9aa6c43 100644 --- a/plugins/model-providers/opencode-zen/__init__.py +++ b/plugins/model-providers/opencode-zen/__init__.py @@ -23,7 +23,7 @@ _ATTRIBUTION_HEADERS = { def _flat_model_name(model: str | None) -> str: - """Return the bare OpenCode model ID, tolerating aggregator prefixes.""" + """Bare OpenCode model ID, tolerating aggregator prefixes.""" return (model or "").strip().rsplit("/", 1)[-1].lower() @@ -33,7 +33,7 @@ def _is_deepseek_thinking_model(model: str | None) -> bool: def _is_glm_5_2_model(model: str | None) -> bool: - """Detect GLM-5.2 across alias spellings (glm-5.2 / glm-5-2 / glm-5p2).""" + """GLM-5.2 across alias spellings (glm-5.2 / glm-5-2 / glm-5p2).""" m = _flat_model_name(model) return any(token in m for token in ("glm-5.2", "glm-5-2", "glm-5p2")) @@ -62,9 +62,7 @@ class OpenCodeGoProfile(ProviderProfile): # The relay's default max_tokens (262144) exceeds what Xiaomi accepts for # mimo-v2.5-pro and 400s; keys are normalized via _flat_model_name(). - _MODEL_MAX_TOKENS: dict[str, int] = { - "mimo-v2.5-pro": 131072, - } + _MODEL_MAX_TOKENS: dict[str, int] = {"mimo-v2.5-pro": 131072} def get_max_tokens(self, model: str | None) -> int | None: cap = self._MODEL_MAX_TOKENS.get(_flat_model_name(model)) @@ -85,9 +83,7 @@ class OpenCodeGoProfile(ProviderProfile): return {}, {} return _thinking_toggle_extras(reasoning_config, re_.KIMI_K2_EFFORTS) if _is_deepseek_thinking_model(model): - return _thinking_toggle_extras( - reasoning_config, re_.DEEPSEEK_V4_EFFORTS, re_.DEEPSEEK_V4_OVERRIDES - ) + return _thinking_toggle_extras(reasoning_config, re_.DEEPSEEK_V4_EFFORTS, re_.DEEPSEEK_V4_OVERRIDES) return {}, {} @@ -98,12 +94,8 @@ def _build_ox_alpha_reasoning_extras( opencode-free profile (low/high/max only; anything else 400s).""" if _flat_model_name(model) != "x-preview-f-free": return {}, {} - clamped = re_.clamp_effort( - _requested_effort(reasoning_config), re_.OX_ALPHA_EFFORTS, re_.OX_ALPHA_OVERRIDES - ) - if clamped not in re_.OX_ALPHA_EFFORTS: - return {}, {} - return {}, {"reasoning_effort": clamped} + clamped = re_.clamp_effort(_requested_effort(reasoning_config), re_.OX_ALPHA_EFFORTS, re_.OX_ALPHA_OVERRIDES) + return ({}, {"reasoning_effort": clamped}) if clamped in re_.OX_ALPHA_EFFORTS else ({}, {}) class OpenCodeZenProfile(ProviderProfile): diff --git a/plugins/model-providers/openrouter/__init__.py b/plugins/model-providers/openrouter/__init__.py index f25ee79b5e..cf9fb6147c 100644 --- a/plugins/model-providers/openrouter/__init__.py +++ b/plugins/model-providers/openrouter/__init__.py @@ -152,10 +152,7 @@ openrouter = OpenRouterProfile( base_url="https://openrouter.ai/api/v1", models_url="https://openrouter.ai/api/v1/models", fallback_models=( - "anthropic/claude-sonnet-4.6", - "openai/gpt-5.4", - "deepseek/deepseek-chat", - "google/gemini-3.8-flash", + "anthropic/claude-sonnet-4.6", "openai/gpt-5.4", "deepseek/deepseek-chat", "google/gemini-3.8-flash", "qwen/qwen3-plus", ), ) diff --git a/plugins/model-providers/qwen-oauth/__init__.py b/plugins/model-providers/qwen-oauth/__init__.py index 6e1a88f95c..98e166bf1e 100644 --- a/plugins/model-providers/qwen-oauth/__init__.py +++ b/plugins/model-providers/qwen-oauth/__init__.py @@ -27,8 +27,8 @@ class QwenProfile(ProviderProfile): """Qwen Portal — message normalization, vl_high_resolution, metadata top-level.""" def prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Normalize content to list-of-dicts and inject cache_control on the system - message. Copy-on-write: only touched messages/parts are copied.""" + """Normalize content to list-of-dicts and inject cache_control on the (first) + system message. Copy-on-write: only touched messages/parts are copied.""" if not messages: return [] prepared = list(messages) @@ -45,7 +45,6 @@ class QwenProfile(ProviderProfile): parts = _normalize_parts(content) if parts is not None: prepared[idx] = {**msg, "content": parts} - if system_idx is not None: msg = prepared[system_idx] content = msg.get("content") @@ -59,11 +58,7 @@ class QwenProfile(ProviderProfile): return {"vl_high_resolution_images": True} def build_api_kwargs_extras( - self, - *, - reasoning_config: dict | None = None, - qwen_session_metadata: dict | None = None, - **context, + self, *, reasoning_config: dict | None = None, qwen_session_metadata: dict | None = None, **context ) -> tuple[dict[str, Any], dict[str, Any]]: """Qwen metadata goes to top-level api_kwargs, not extra_body.""" return {}, {"metadata": qwen_session_metadata} if qwen_session_metadata else {} diff --git a/plugins/model-providers/router/__init__.py b/plugins/model-providers/router/__init__.py index 9604d354fa..01f296986a 100644 --- a/plugins/model-providers/router/__init__.py +++ b/plugins/model-providers/router/__init__.py @@ -47,14 +47,11 @@ def _base_url() -> str: def _resolve_api_key() -> str: """Router key (documented var, then alias), preferring dotenv; plain os.environ is the fallback when the dotenv resolver is unavailable or raises.""" - resolvers: list = [lambda var: os.environ.get(var, "")] try: - from hermes_cli.config import get_env_value_prefer_dotenv - - resolvers.insert(0, get_env_value_prefer_dotenv) + from hermes_cli.config import get_env_value_prefer_dotenv as prefer_dotenv except Exception: - pass - for resolve in resolvers: + prefer_dotenv = None + for resolve in filter(None, (prefer_dotenv, os.environ.get)): for var in ("RAMP_ROUTER_API_KEY", "ROUTER_API_KEY"): try: value = str(resolve(var) or "").strip() @@ -121,7 +118,7 @@ def _save_disk(efforts_by_id: dict[str, list[str]]) -> None: return try: path.parent.mkdir(parents=True, exist_ok=True) - tmp = path.with_suffix(".tmp") + tmp = path.with_suffix(".tmp") # write-then-rename keeps readers from seeing a torn file tmp.write_text(json.dumps({"ts": time.time(), "efforts": efforts_by_id}), encoding="utf-8") tmp.replace(path) except Exception as exc: diff --git a/plugins/model-providers/upstage/__init__.py b/plugins/model-providers/upstage/__init__.py index 186671e164..324c79a3c5 100644 --- a/plugins/model-providers/upstage/__init__.py +++ b/plugins/model-providers/upstage/__init__.py @@ -11,9 +11,8 @@ from agent.reasoning_effort import EFFORT_LADDER, SOLAR_EFFORTS, clamp_effort from providers import register_provider from providers.base import ProviderProfile -# Deny-list on purpose: new Solar models are assumed reasoning-capable; only -# these known non-reasoning families ignore reasoning_effort. Substring match -# so dated variants (``solar-mini-250127``) are covered. +# Deny-list on purpose: new Solar models are assumed reasoning-capable; only these known +# non-reasoning families ignore reasoning_effort. Substring match covers dated variants. _NON_REASONING_MODEL_MARKERS = ("solar-mini", "syn-pro") @@ -26,12 +25,10 @@ class UpstageProfile(ProviderProfile): m = (model or "").strip().lower() if any(marker in m for marker in _NON_REASONING_MODEL_MARKERS): return {}, {} - # Unset -> default reasoning ON for agents. if not reasoning_config or not isinstance(reasoning_config, dict): - return {}, {"reasoning_effort": "medium"} - # Explicitly disabled -> omit so Solar applies its own default (minimal = off). + return {}, {"reasoning_effort": "medium"} # unset -> reasoning ON for agents if reasoning_config.get("enabled") is False: - return {}, {} + return {}, {} # explicitly disabled -> Solar's own default (minimal = off) effort = (reasoning_config.get("effort") or "").strip().lower() if not effort: return {}, {"reasoning_effort": "medium"} @@ -39,9 +36,8 @@ class UpstageProfile(ProviderProfile): return {}, {} mapped = clamp_effort(effort, SOLAR_EFFORTS) if mapped not in SOLAR_EFFORTS: - # Bespoke level outside the ladder runs at full strength rather - # than quietly falling to the default; ladder levels that still - # don't map are omitted. + # Bespoke level outside the ladder runs at full strength rather than quietly + # falling to the default; ladder levels that still don't map are omitted. mapped = "high" if effort not in EFFORT_LADDER else None return {}, {"reasoning_effort": mapped} if mapped else {} diff --git a/plugins/model-providers/vertex/__init__.py b/plugins/model-providers/vertex/__init__.py index ef761c2656..4b7583a5c5 100644 --- a/plugins/model-providers/vertex/__init__.py +++ b/plugins/model-providers/vertex/__init__.py @@ -1,10 +1,8 @@ -"""Google Vertex AI provider profile: Gemini via Google Cloud's OpenAI-compatible -endpoint. +"""Google Vertex AI provider profile: Gemini via Google Cloud's OpenAI-compatible endpoint. -Auth is OAuth2 (service-account JSON or ADC), not a static key: ``agent/ -vertex_adapter.py`` mints ``(token, base_url)`` and the token is passed as -``api_key``. ``auth_type="vertex"`` keeps it out of the api_key provider path so -a credentials-file path is never mistaken for a key. +Auth is OAuth2 (service-account JSON or ADC), not a static key: ``agent/vertex_adapter.py`` +mints ``(token, base_url)`` and the token is passed as ``api_key``. ``auth_type="vertex"`` +keeps it out of the api_key provider path so a credentials-file path is never mistaken for a key. """ from typing import Any @@ -17,18 +15,12 @@ class VertexProfile(ProviderProfile): """Vertex AI — reuse Gemini's thinking_config translation for extra_body.""" def build_extra_body(self, *, session_id: str | None = None, **context: Any) -> dict[str, Any]: - """Emit ``extra_body.google.thinking_config`` like the ``gemini`` provider's - OpenAI-compat subpath.""" - from agent.transports.chat_completions import ( - _build_gemini_thinking_config, - _snake_case_gemini_thinking_config, - ) + """Emit ``extra_body.google.thinking_config`` like the ``gemini`` provider's OpenAI-compat subpath.""" + from agent.transports.chat_completions import _build_gemini_thinking_config, _snake_case_gemini_thinking_config raw = _build_gemini_thinking_config(context.get("model") or "", context.get("reasoning_config")) thinking_config = _snake_case_gemini_thinking_config(raw) if raw else None - if not thinking_config: - return {} - return {"extra_body": {"google": {"thinking_config": thinking_config}}} + return {"extra_body": {"google": {"thinking_config": thinking_config}}} if thinking_config else {} def fetch_models( self, *, api_key: str | None = None, base_url: str | None = None, timeout: float = 8.0 diff --git a/plugins/model-providers/zai/__init__.py b/plugins/model-providers/zai/__init__.py index 11a57c3dc0..05105e779f 100644 --- a/plugins/model-providers/zai/__init__.py +++ b/plugins/model-providers/zai/__init__.py @@ -27,14 +27,12 @@ def _model_supports_thinking(model: str | None) -> bool: def _has_token(model: str | None, tokens: tuple[str, ...]) -> bool: m = (model or "").strip().lower() - return bool(m) and any(token in m for token in tokens) + return any(token in m for token in tokens) def _glm_5_2_reasoning_effort(reasoning_config: dict | None, *, model: str | None = None) -> str | None: - """Map Hermes effort onto GLM's vocabulary (5.2: high/max; 5.3: low..max). - - Below-floor efforts clamp to the floor; disabled/unset leaves the server default. - """ + """Hermes effort -> GLM vocabulary (5.2: high/max; 5.3: low..max). Below-floor + efforts clamp to the floor; disabled/unset leaves the server default.""" effort = re_.requested_effort(reasoning_config) if effort is None or effort == "none": return None @@ -57,12 +55,10 @@ class ZaiProfile(ProviderProfile): is_5_2 = _has_token(model, _GLM_5_2_TOKENS) if not _model_supports_thinking(model) and not is_5_2: return extra_body, top_level - # Only emit when the user expressed a preference (server default = enabled). if isinstance(reasoning_config, dict): enabled = reasoning_config.get("enabled") is not False extra_body["thinking"] = {"type": "enabled" if enabled else "disabled"} - if is_5_2: effort = _glm_5_2_reasoning_effort(reasoning_config, model=model) if effort is not None: From 40722b322c1f6a543d8a9b26577f25eaf3e0d01d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:45:13 -0700 Subject: [PATCH 3/4] refactor(model-providers): pack keyword-only profile constructors (comments and multi-line values keep their lines) --- plugins/model-providers/actual/__init__.py | 17 +++++----------- .../model-providers/ai-gateway/__init__.py | 6 ++---- .../alibaba-coding-plan/__init__.py | 12 ++++------- plugins/model-providers/alibaba/__init__.py | 20 ++++++------------- plugins/model-providers/anthropic/__init__.py | 8 ++------ plugins/model-providers/arcee/__init__.py | 4 +--- .../model-providers/azure-foundry/__init__.py | 7 ++----- plugins/model-providers/bedrock/__init__.py | 7 ++----- .../model-providers/commandcode/__init__.py | 19 +++++------------- .../model-providers/copilot-acp/__init__.py | 3 +-- plugins/model-providers/copilot/__init__.py | 6 ++---- plugins/model-providers/custom/__init__.py | 3 +-- plugins/model-providers/deepinfra/__init__.py | 10 +++------- plugins/model-providers/deepseek/__init__.py | 11 +++------- plugins/model-providers/fireworks/__init__.py | 10 +++------- plugins/model-providers/gemini/__init__.py | 7 ++----- plugins/model-providers/gmi/__init__.py | 10 +++------- .../model-providers/huggingface/__init__.py | 7 ++----- plugins/model-providers/kilocode/__init__.py | 7 ++----- plugins/model-providers/meta-ai/__init__.py | 10 +++------- plugins/model-providers/minimax/__init__.py | 11 +++------- .../nebius-token-factory/__init__.py | 6 ++---- plugins/model-providers/nous/__init__.py | 13 ++++-------- plugins/model-providers/novita/__init__.py | 10 +++------- plugins/model-providers/nvidia/__init__.py | 11 +++------- .../model-providers/ollama-cloud/__init__.py | 7 ++----- .../model-providers/openai-codex/__init__.py | 7 ++----- .../model-providers/opencode-free/__init__.py | 6 ++---- .../model-providers/opencode-zen/__init__.py | 14 ++++--------- .../model-providers/openrouter/__init__.py | 11 +++------- .../model-providers/qwen-oauth/__init__.py | 8 ++------ plugins/model-providers/router/__init__.py | 10 +++------- plugins/model-providers/stepfun/__init__.py | 7 ++----- plugins/model-providers/upstage/__init__.py | 11 +++------- plugins/model-providers/vertex/__init__.py | 7 ++----- plugins/model-providers/xai/__init__.py | 8 ++------ plugins/model-providers/xiaomi/__init__.py | 4 +--- plugins/model-providers/zai/__init__.py | 12 ++++------- 38 files changed, 101 insertions(+), 246 deletions(-) diff --git a/plugins/model-providers/actual/__init__.py b/plugins/model-providers/actual/__init__.py index 3541312129..8506eece8b 100644 --- a/plugins/model-providers/actual/__init__.py +++ b/plugins/model-providers/actual/__init__.py @@ -25,18 +25,11 @@ class ActualProfile(ProviderProfile): actual = ActualProfile( - name="actual", - aliases=("actual-computer", "actualcomputer", "aci"), - display_name="Actual Computer", - description=( - "Actual Computer - hosted inference via api.actual.inc, or local " - "offline inference via ACTUAL_BASE_URL" - ), - signup_url="https://actual.inc", - env_vars=("ACTUAL_API_KEY", "ACTUAL_BASE_URL"), - base_url=DEFAULT_ACTUAL_BASE_URL, - auth_type="api_key", - api_mode="codex_responses", + name="actual", aliases=("actual-computer", "actualcomputer", "aci"), display_name="Actual Computer", + description="Actual Computer - hosted inference via api.actual.inc, or local " + "offline inference via ACTUAL_BASE_URL", + signup_url="https://actual.inc", env_vars=("ACTUAL_API_KEY", "ACTUAL_BASE_URL"), + base_url=DEFAULT_ACTUAL_BASE_URL, auth_type="api_key", api_mode="codex_responses", ) register_provider(actual) diff --git a/plugins/model-providers/ai-gateway/__init__.py b/plugins/model-providers/ai-gateway/__init__.py index 33a4c73ec5..409476ecf6 100644 --- a/plugins/model-providers/ai-gateway/__init__.py +++ b/plugins/model-providers/ai-gateway/__init__.py @@ -19,10 +19,8 @@ class VercelAIGatewayProfile(ProviderProfile): vercel = VercelAIGatewayProfile( - name="ai-gateway", - aliases=("vercel", "vercel-ai-gateway", "ai_gateway", "aigateway"), - env_vars=("AI_GATEWAY_API_KEY",), - base_url="https://ai-gateway.vercel.sh/v1", + name="ai-gateway", aliases=("vercel", "vercel-ai-gateway", "ai_gateway", "aigateway"), + env_vars=("AI_GATEWAY_API_KEY",), base_url="https://ai-gateway.vercel.sh/v1", default_headers={"HTTP-Referer": "https://hermes-agent.nousresearch.com", "X-Title": "Hermes Agent"}, default_aux_model="google/gemini-3-flash", ) diff --git a/plugins/model-providers/alibaba-coding-plan/__init__.py b/plugins/model-providers/alibaba-coding-plan/__init__.py index 9c19d153c1..9fc14cf7f4 100644 --- a/plugins/model-providers/alibaba-coding-plan/__init__.py +++ b/plugins/model-providers/alibaba-coding-plan/__init__.py @@ -9,25 +9,21 @@ from providers import register_provider from providers.base import ProviderProfile alibaba_coding_plan = ProviderProfile( - name="alibaba-coding-plan", - aliases=("alibaba_coding", "alibaba-coding", "dashscope-coding"), + name="alibaba-coding-plan", aliases=("alibaba_coding", "alibaba-coding", "dashscope-coding"), display_name="Alibaba Cloud (Coding Plan)", description="Alibaba Cloud Coding Plan (Dedicated coding tier)", signup_url="https://help.aliyun.com/zh/model-studio/", env_vars=("ALIBABA_CODING_PLAN_API_KEY", "DASHSCOPE_API_KEY", "ALIBABA_CODING_PLAN_BASE_URL"), - base_url="https://coding-intl.dashscope.aliyuncs.com/v1", - auth_type="api_key", + base_url="https://coding-intl.dashscope.aliyuncs.com/v1", auth_type="api_key", ) alibaba_coding_plan_cn = ProviderProfile( - name="alibaba-coding-plan-cn", - aliases=("alibaba-coding-cn", "dashscope-coding-cn"), + name="alibaba-coding-plan-cn", aliases=("alibaba-coding-cn", "dashscope-coding-cn"), display_name="Alibaba Cloud (Coding Plan, China)", description="Alibaba Cloud Coding Plan, mainland-China endpoint", signup_url="https://help.aliyun.com/zh/model-studio/", env_vars=("ALIBABA_CODING_PLAN_CN_API_KEY", "ALIBABA_CODING_PLAN_API_KEY", "DASHSCOPE_API_KEY", "ALIBABA_CODING_PLAN_CN_BASE_URL"), - base_url="https://coding.dashscope.aliyuncs.com/v1", - auth_type="api_key", + base_url="https://coding.dashscope.aliyuncs.com/v1", auth_type="api_key", ) register_provider(alibaba_coding_plan) diff --git a/plugins/model-providers/alibaba/__init__.py b/plugins/model-providers/alibaba/__init__.py index 7dd5519e62..a867b765c6 100644 --- a/plugins/model-providers/alibaba/__init__.py +++ b/plugins/model-providers/alibaba/__init__.py @@ -9,15 +9,12 @@ from providers import register_provider from providers.base import ProviderProfile alibaba = ProviderProfile( - name="alibaba", - aliases=("dashscope", "alibaba-cloud", "qwen-dashscope"), - env_vars=("DASHSCOPE_API_KEY",), + name="alibaba", aliases=("dashscope", "alibaba-cloud", "qwen-dashscope"), env_vars=("DASHSCOPE_API_KEY",), base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", ) alibaba_cn = ProviderProfile( - name="alibaba-cn", - aliases=("dashscope-cn", "alibaba-cloud-cn"), + name="alibaba-cn", aliases=("dashscope-cn", "alibaba-cloud-cn"), display_name="Alibaba Cloud DashScope (China)", description="Alibaba Cloud DashScope, mainland-China endpoint", env_vars=("DASHSCOPE_API_KEY", "DASHSCOPE_CN_BASE_URL"), @@ -25,25 +22,20 @@ alibaba_cn = ProviderProfile( ) alibaba_token_plan = ProviderProfile( - name="alibaba-token-plan", - aliases=("dashscope-token-plan",), - display_name="Alibaba Cloud (Token Plan)", + name="alibaba-token-plan", aliases=("dashscope-token-plan",), display_name="Alibaba Cloud (Token Plan)", description="Alibaba Cloud Model Studio Token Plan (flat-token tier)", signup_url="https://help.aliyun.com/zh/model-studio/", env_vars=("ALIBABA_TOKEN_PLAN_API_KEY", "ALIBABA_TOKEN_PLAN_BASE_URL"), - base_url="https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", - auth_type="api_key", + base_url="https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", auth_type="api_key", ) alibaba_token_plan_cn = ProviderProfile( - name="alibaba-token-plan-cn", - aliases=("dashscope-token-plan-cn",), + name="alibaba-token-plan-cn", aliases=("dashscope-token-plan-cn",), display_name="Alibaba Cloud (Token Plan, China)", description="Alibaba Cloud Model Studio Token Plan, mainland-China endpoint", signup_url="https://help.aliyun.com/zh/model-studio/", env_vars=("ALIBABA_TOKEN_PLAN_CN_API_KEY", "ALIBABA_TOKEN_PLAN_API_KEY", "ALIBABA_TOKEN_PLAN_CN_BASE_URL"), - base_url="https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", - auth_type="api_key", + base_url="https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", auth_type="api_key", ) register_provider(alibaba) diff --git a/plugins/model-providers/anthropic/__init__.py b/plugins/model-providers/anthropic/__init__.py index 1647e16b77..267902b274 100644 --- a/plugins/model-providers/anthropic/__init__.py +++ b/plugins/model-providers/anthropic/__init__.py @@ -33,13 +33,9 @@ class AnthropicProfile(ProviderProfile): anthropic = AnthropicProfile( - name="anthropic", - aliases=("claude", "claude-oauth", "claude-code"), - api_mode="anthropic_messages", + name="anthropic", aliases=("claude", "claude-oauth", "claude-code"), api_mode="anthropic_messages", env_vars=("ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"), - base_url="https://api.anthropic.com", - auth_type="api_key", - default_aux_model="claude-haiku-4-5-20251001", + base_url="https://api.anthropic.com", auth_type="api_key", default_aux_model="claude-haiku-4-5-20251001", ) register_provider(anthropic) diff --git a/plugins/model-providers/arcee/__init__.py b/plugins/model-providers/arcee/__init__.py index 46afb6e16e..4e39bc8469 100644 --- a/plugins/model-providers/arcee/__init__.py +++ b/plugins/model-providers/arcee/__init__.py @@ -4,9 +4,7 @@ from providers import register_provider from providers.base import ProviderProfile arcee = ProviderProfile( - name="arcee", - aliases=("arcee-ai", "arceeai"), - env_vars=("ARCEEAI_API_KEY",), + name="arcee", aliases=("arcee-ai", "arceeai"), env_vars=("ARCEEAI_API_KEY",), base_url="https://api.arcee.ai/api/v1", ) diff --git a/plugins/model-providers/azure-foundry/__init__.py b/plugins/model-providers/azure-foundry/__init__.py index a0fc7d2d60..283e79c2f4 100644 --- a/plugins/model-providers/azure-foundry/__init__.py +++ b/plugins/model-providers/azure-foundry/__init__.py @@ -5,12 +5,9 @@ from providers import register_provider from providers.base import ProviderProfile azure_foundry = ProviderProfile( - name="azure-foundry", - aliases=("azure", "azure-ai-foundry", "azure-ai"), - display_name="Azure Foundry", + name="azure-foundry", aliases=("azure", "azure-ai-foundry", "azure-ai"), display_name="Azure Foundry", description="Microsoft Foundry - OpenAI-compatible endpoint (user-supplied base URL)", - signup_url="https://ai.azure.com/", - env_vars=("AZURE_FOUNDRY_API_KEY", "AZURE_FOUNDRY_BASE_URL"), + signup_url="https://ai.azure.com/", env_vars=("AZURE_FOUNDRY_API_KEY", "AZURE_FOUNDRY_BASE_URL"), base_url="", # per-resource; user provides at setup auth_type="api_key", ) diff --git a/plugins/model-providers/bedrock/__init__.py b/plugins/model-providers/bedrock/__init__.py index 0b23f4cbdd..f1d3194901 100644 --- a/plugins/model-providers/bedrock/__init__.py +++ b/plugins/model-providers/bedrock/__init__.py @@ -15,12 +15,9 @@ class BedrockProfile(ProviderProfile): bedrock = BedrockProfile( - name="bedrock", - aliases=("aws", "aws-bedrock", "amazon-bedrock", "amazon"), - api_mode="bedrock_converse", + name="bedrock", aliases=("aws", "aws-bedrock", "amazon-bedrock", "amazon"), api_mode="bedrock_converse", env_vars=(), # AWS SDK credentials — not env vars - base_url="https://bedrock-runtime.us-east-1.amazonaws.com", - auth_type="aws_sdk", + base_url="https://bedrock-runtime.us-east-1.amazonaws.com", auth_type="aws_sdk", ) register_provider(bedrock) diff --git a/plugins/model-providers/commandcode/__init__.py b/plugins/model-providers/commandcode/__init__.py index e4eb2dc498..4346b096e2 100644 --- a/plugins/model-providers/commandcode/__init__.py +++ b/plugins/model-providers/commandcode/__init__.py @@ -52,17 +52,12 @@ class CommandCodeAnthropicProfile(CommandCodeProfile): commandcode = CommandCodeProfile( - name="commandcode", - aliases=("commandcode-chat",), - api_mode="chat_completions", + name="commandcode", aliases=("commandcode-chat",), api_mode="chat_completions", # Same key as the anthropic profile; distinct base-URL override vars so each # profile renders its own card on the desktop Keys tab (rows keyed by env var). env_vars=("COMMANDCODE_API_KEY", "COMMANDCODE_BASE_URL"), - display_name="CommandCode", - description="CommandCode — 20+ models via OpenAI-compatible API", - signup_url="https://commandcode.ai/", - base_url=_COMMANDCODE_BASE, - models_url=_COMMANDCODE_MODELS_URL, + display_name="CommandCode", description="CommandCode — 20+ models via OpenAI-compatible API", + signup_url="https://commandcode.ai/", base_url=_COMMANDCODE_BASE, models_url=_COMMANDCODE_MODELS_URL, fallback_models=( "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash", "Qwen/Qwen3.7-Max", "Qwen/Qwen3.6-Plus", "moonshotai/Kimi-K2.6", "zai-org/GLM-5.1", "MiniMaxAI/MiniMax-M2.7", "stepfun/Step-3.5-Flash", @@ -72,15 +67,11 @@ commandcode = CommandCodeProfile( ) commandcode_anthropic = CommandCodeAnthropicProfile( - name="commandcode-anthropic", - aliases=("commandcode-claude",), - api_mode="anthropic_messages", + name="commandcode-anthropic", aliases=("commandcode-claude",), api_mode="anthropic_messages", env_vars=("COMMANDCODE_API_KEY", "COMMANDCODE_ANTHROPIC_BASE_URL"), display_name="CommandCode (Anthropic)", description="CommandCode — Claude models via Anthropic Messages API", - signup_url="https://commandcode.ai/", - base_url=_COMMANDCODE_BASE, - models_url=_COMMANDCODE_MODELS_URL, + signup_url="https://commandcode.ai/", base_url=_COMMANDCODE_BASE, models_url=_COMMANDCODE_MODELS_URL, fallback_models=("claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5-20251001"), default_aux_model="claude-haiku-4-5-20251001", ) diff --git a/plugins/model-providers/copilot-acp/__init__.py b/plugins/model-providers/copilot-acp/__init__.py index 86d8ee3009..0f3b87ca1d 100644 --- a/plugins/model-providers/copilot-acp/__init__.py +++ b/plugins/model-providers/copilot-acp/__init__.py @@ -29,8 +29,7 @@ class CopilotACPProfile(ProviderProfile): copilot_acp = CopilotACPProfile( - name="copilot-acp", - aliases=("github-copilot-acp", "copilot-acp-agent"), + name="copilot-acp", aliases=("github-copilot-acp", "copilot-acp-agent"), api_mode="chat_completions", # ACP subprocess uses chat_completions routing env_vars=(), # Managed by ACP subprocess base_url="acp://copilot", # ACP internal scheme diff --git a/plugins/model-providers/copilot/__init__.py b/plugins/model-providers/copilot/__init__.py index 23d21c4dd4..37cec65951 100644 --- a/plugins/model-providers/copilot/__init__.py +++ b/plugins/model-providers/copilot/__init__.py @@ -42,10 +42,8 @@ class CopilotProfile(ProviderProfile): copilot = CopilotProfile( - name="copilot", - aliases=("github-copilot", "github-models", "github-model", "github"), - env_vars=("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"), - base_url="https://api.githubcopilot.com", + name="copilot", aliases=("github-copilot", "github-models", "github-model", "github"), + env_vars=("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"), base_url="https://api.githubcopilot.com", auth_type="copilot", ) diff --git a/plugins/model-providers/custom/__init__.py b/plugins/model-providers/custom/__init__.py index cc8c0bb445..ea186482cc 100644 --- a/plugins/model-providers/custom/__init__.py +++ b/plugins/model-providers/custom/__init__.py @@ -62,8 +62,7 @@ class CustomProfile(ProviderProfile): custom = CustomProfile( - name="custom", - aliases=("ollama", "local", "vllm", "llamacpp", "llama.cpp", "llama-cpp"), + name="custom", aliases=("ollama", "local", "vllm", "llamacpp", "llama.cpp", "llama-cpp"), env_vars=(), # No fixed key — custom endpoint base_url="", # User-configured # Floor only (user model.max_tokens overrides); without it Ollama falls diff --git a/plugins/model-providers/deepinfra/__init__.py b/plugins/model-providers/deepinfra/__init__.py index 61c6bbdd75..313683e94f 100644 --- a/plugins/model-providers/deepinfra/__init__.py +++ b/plugins/model-providers/deepinfra/__init__.py @@ -31,13 +31,9 @@ class _DeepInfraProfile(ProviderProfile): deepinfra = _DeepInfraProfile( - name="deepinfra", - aliases=("deep-infra", "deepinfra-ai"), - display_name="DeepInfra", - description="DeepInfra — 100+ open models, pay-per-use", - signup_url="https://deepinfra.com/dash/api_keys", - env_vars=("DEEPINFRA_API_KEY", "DEEPINFRA_BASE_URL"), - base_url="https://api.deepinfra.com/v1/openai", + name="deepinfra", aliases=("deep-infra", "deepinfra-ai"), display_name="DeepInfra", + description="DeepInfra — 100+ open models, pay-per-use", signup_url="https://deepinfra.com/dash/api_keys", + env_vars=("DEEPINFRA_API_KEY", "DEEPINFRA_BASE_URL"), base_url="https://api.deepinfra.com/v1/openai", auth_type="api_key", default_max_tokens=None, # DeepInfra applies its documented per-model limit # The only hardcoded DeepInfra model: aux resolution is synchronous, so it diff --git a/plugins/model-providers/deepseek/__init__.py b/plugins/model-providers/deepseek/__init__.py index e253863d83..6d175375f3 100644 --- a/plugins/model-providers/deepseek/__init__.py +++ b/plugins/model-providers/deepseek/__init__.py @@ -42,14 +42,9 @@ class DeepSeekProfile(ProviderProfile): deepseek = DeepSeekProfile( - name="deepseek", - aliases=("deepseek-chat",), - env_vars=("DEEPSEEK_API_KEY",), - display_name="DeepSeek", - description="DeepSeek — native DeepSeek API", - signup_url="https://platform.deepseek.com/", - fallback_models=("deepseek-v4-pro", "deepseek-v4-flash"), - base_url="https://api.deepseek.com/v1", + name="deepseek", aliases=("deepseek-chat",), env_vars=("DEEPSEEK_API_KEY",), display_name="DeepSeek", + description="DeepSeek — native DeepSeek API", signup_url="https://platform.deepseek.com/", + fallback_models=("deepseek-v4-pro", "deepseek-v4-flash"), base_url="https://api.deepseek.com/v1", default_aux_model="deepseek-v4-flash", ) diff --git a/plugins/model-providers/fireworks/__init__.py b/plugins/model-providers/fireworks/__init__.py index 6e681b3af3..3d57f99b63 100644 --- a/plugins/model-providers/fireworks/__init__.py +++ b/plugins/model-providers/fireworks/__init__.py @@ -7,14 +7,10 @@ from providers.base import ProviderProfile fireworks = ProviderProfile( - name="fireworks", - aliases=("fireworks-ai", "fw"), - display_name="Fireworks AI", + name="fireworks", aliases=("fireworks-ai", "fw"), display_name="Fireworks AI", description="Fireworks AI — OpenAI-compatible direct model API", - signup_url="https://app.fireworks.ai/settings/users/api-keys", - env_vars=("FIREWORKS_API_KEY",), - base_url="https://api.fireworks.ai/inference/v1", - auth_type="api_key", + signup_url="https://app.fireworks.ai/settings/users/api-keys", env_vars=("FIREWORKS_API_KEY",), + base_url="https://api.fireworks.ai/inference/v1", auth_type="api_key", # Attribution headers (canonical Hermes set); via default_headers so they # survive switch_model and credential rotation. default_headers={ diff --git a/plugins/model-providers/gemini/__init__.py b/plugins/model-providers/gemini/__init__.py index baea33c4dd..ad99a7b3f3 100644 --- a/plugins/model-providers/gemini/__init__.py +++ b/plugins/model-providers/gemini/__init__.py @@ -32,12 +32,9 @@ class GeminiProfile(ProviderProfile): gemini = GeminiProfile( - name="gemini", - aliases=("google", "google-gemini", "google-ai-studio"), - api_mode="chat_completions", + name="gemini", aliases=("google", "google-gemini", "google-ai-studio"), api_mode="chat_completions", env_vars=("GOOGLE_API_KEY", "GEMINI_API_KEY"), - base_url="https://generativelanguage.googleapis.com/v1beta", - auth_type="api_key", + base_url="https://generativelanguage.googleapis.com/v1beta", auth_type="api_key", default_aux_model="gemini-3.6-flash", ) diff --git a/plugins/model-providers/gmi/__init__.py b/plugins/model-providers/gmi/__init__.py index 2009d78598..5319f71f09 100644 --- a/plugins/model-providers/gmi/__init__.py +++ b/plugins/model-providers/gmi/__init__.py @@ -5,14 +5,10 @@ from providers import register_provider from providers.base import ProviderProfile gmi = ProviderProfile( - name="gmi", - aliases=("gmi-cloud", "gmicloud"), - display_name="GMI Cloud", + name="gmi", aliases=("gmi-cloud", "gmicloud"), display_name="GMI Cloud", description="GMI Cloud — multi-model direct API (slash-form model IDs)", - signup_url="https://www.gmicloud.ai/", - env_vars=("GMI_API_KEY", "GMI_BASE_URL"), - base_url="https://api.gmi-serving.com/v1", - auth_type="api_key", + signup_url="https://www.gmicloud.ai/", env_vars=("GMI_API_KEY", "GMI_BASE_URL"), + base_url="https://api.gmi-serving.com/v1", auth_type="api_key", # Attribution so GMI can identify Hermes Agent traffic. default_headers={"User-Agent": f"HermesAgent/{_HERMES_VERSION}"}, default_aux_model="google/gemini-3.1-flash-lite-preview", diff --git a/plugins/model-providers/huggingface/__init__.py b/plugins/model-providers/huggingface/__init__.py index 7fcc4b32bc..65078b4556 100644 --- a/plugins/model-providers/huggingface/__init__.py +++ b/plugins/model-providers/huggingface/__init__.py @@ -4,11 +4,8 @@ from providers import register_provider from providers.base import ProviderProfile huggingface = ProviderProfile( - name="huggingface", - aliases=("hf", "hugging-face", "huggingface-hub"), - env_vars=("HF_TOKEN",), - display_name="HuggingFace", - description="HuggingFace Inference API", + name="huggingface", aliases=("hf", "hugging-face", "huggingface-hub"), env_vars=("HF_TOKEN",), + display_name="HuggingFace", description="HuggingFace Inference API", signup_url="https://huggingface.co/settings/tokens", fallback_models=("Qwen/Qwen3.5-72B-Instruct", "deepseek-ai/DeepSeek-V3.2"), base_url="https://router.huggingface.co/v1", diff --git a/plugins/model-providers/kilocode/__init__.py b/plugins/model-providers/kilocode/__init__.py index f95696acb6..eb44f07782 100644 --- a/plugins/model-providers/kilocode/__init__.py +++ b/plugins/model-providers/kilocode/__init__.py @@ -4,11 +4,8 @@ from providers import register_provider from providers.base import ProviderProfile kilocode = ProviderProfile( - name="kilocode", - aliases=("kilo-code", "kilo", "kilo-gateway"), - env_vars=("KILOCODE_API_KEY",), - base_url="https://api.kilo.ai/api/gateway", - default_aux_model="google/gemini-3.6-flash", + name="kilocode", aliases=("kilo-code", "kilo", "kilo-gateway"), env_vars=("KILOCODE_API_KEY",), + base_url="https://api.kilo.ai/api/gateway", default_aux_model="google/gemini-3.6-flash", ) register_provider(kilocode) diff --git a/plugins/model-providers/meta-ai/__init__.py b/plugins/model-providers/meta-ai/__init__.py index 061a5da9b4..8cddbbb43e 100644 --- a/plugins/model-providers/meta-ai/__init__.py +++ b/plugins/model-providers/meta-ai/__init__.py @@ -36,20 +36,16 @@ class MetaAIProfile(ProviderProfile): meta_ai = MetaAIProfile( - name="meta-ai", - aliases=("meta", "muse", "muse-spark", "model-api", "msl"), - display_name="Meta Model API", + name="meta-ai", aliases=("meta", "muse", "muse-spark", "model-api", "msl"), display_name="Meta Model API", description="Meta Muse Spark family (Meta Superintelligence Labs)", signup_url="https://developer.meta.com/ai/", # MODEL_API_KEY is Meta's documented env var; the aliases are conveniences. env_vars=("MODEL_API_KEY", "META_API_KEY", "META_MODEL_API_KEY", "META_BASE_URL"), - base_url=os.getenv("META_BASE_URL", "").strip() or "https://api.meta.ai/v1", - auth_type="api_key", + base_url=os.getenv("META_BASE_URL", "").strip() or "https://api.meta.ai/v1", auth_type="api_key", # Responses API engages Muse prompt caching (0 cached tokens on chat/completions vs # 93-99% hits on /v1/responses); the hook above still covers custom non-api.meta.ai base URLs. api_mode="codex_responses", - supports_vision=True, - default_aux_model="muse-spark-1.2-contributor", + supports_vision=True, default_aux_model="muse-spark-1.2-contributor", # Muse spends completion budget on hidden reasoning first; low caps can finish with empty content. default_max_tokens=16384, fallback_models=("muse-spark-1.2", "muse-spark-1.2-contributor"), diff --git a/plugins/model-providers/minimax/__init__.py b/plugins/model-providers/minimax/__init__.py index 896bbc876b..0a71be3f83 100644 --- a/plugins/model-providers/minimax/__init__.py +++ b/plugins/model-providers/minimax/__init__.py @@ -49,16 +49,11 @@ minimax_cn = MiniMaxProfile( ) minimax_oauth = MiniMaxProfile( - name="minimax-oauth", - aliases=("minimax_oauth", "minimax-oauth-io"), - api_mode="anthropic_messages", - display_name="MiniMax (OAuth)", - description="MiniMax via OAuth browser flow — no API key required", + name="minimax-oauth", aliases=("minimax_oauth", "minimax-oauth-io"), api_mode="anthropic_messages", + display_name="MiniMax (OAuth)", description="MiniMax via OAuth browser flow — no API key required", signup_url="https://api.minimax.io/", env_vars=(), # OAuth — tokens in auth.json, not env - base_url="https://api.minimax.io/anthropic", - auth_type="oauth_external", - default_aux_model="MiniMax-M2.7", + base_url="https://api.minimax.io/anthropic", auth_type="oauth_external", default_aux_model="MiniMax-M2.7", ) register_provider(minimax) diff --git a/plugins/model-providers/nebius-token-factory/__init__.py b/plugins/model-providers/nebius-token-factory/__init__.py index 0e1b6d7743..74794c0bf0 100644 --- a/plugins/model-providers/nebius-token-factory/__init__.py +++ b/plugins/model-providers/nebius-token-factory/__init__.py @@ -36,13 +36,11 @@ class NebiusTokenFactoryProfile(ProviderProfile): nebius_token_factory = NebiusTokenFactoryProfile( name="nebius-token-factory", aliases=("nebius", "nebius-tokenfactory", "nebius-tf", "token-factory", "tokenfactory"), - display_name="Nebius Token Factory", - description="Nebius Token Factory — OpenAI-compatible inference", + display_name="Nebius Token Factory", description="Nebius Token Factory — OpenAI-compatible inference", signup_url="https://tokenfactory.nebius.com/", env_vars=("NEBIUS_API_KEY", "NEBIUS_TOKEN_FACTORY_API_KEY", "NEBIUS_BASE_URL"), base_url="https://api.tokenfactory.nebius.com/v1", - models_url="https://api.tokenfactory.nebius.com/v1/models?verbose=true", - auth_type="api_key", + models_url="https://api.tokenfactory.nebius.com/v1/models?verbose=true", auth_type="api_key", default_aux_model="nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B", fallback_models=( "Qwen/Qwen3.5-397B-A17B-fast", "deepseek-ai/DeepSeek-V4-Pro", "zai-org/GLM-5.1", "moonshotai/Kimi-K2.5-fast", diff --git a/plugins/model-providers/nous/__init__.py b/plugins/model-providers/nous/__init__.py index 5cbd3ebad9..a6413d1f0b 100644 --- a/plugins/model-providers/nous/__init__.py +++ b/plugins/model-providers/nous/__init__.py @@ -66,15 +66,10 @@ class NousProfile(ProviderProfile): 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", + 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) diff --git a/plugins/model-providers/novita/__init__.py b/plugins/model-providers/novita/__init__.py index 42a1dabafd..d02b64ac4c 100644 --- a/plugins/model-providers/novita/__init__.py +++ b/plugins/model-providers/novita/__init__.py @@ -5,14 +5,10 @@ from providers.base import ProviderProfile novita = ProviderProfile( - name="novita", - aliases=("novita-ai", "novitaai"), - display_name="NovitaAI", + name="novita", aliases=("novita-ai", "novitaai"), display_name="NovitaAI", description="NovitaAI — AI-native cloud for builders and agents", - signup_url="https://novita.ai/settings/key-management", - env_vars=("NOVITA_API_KEY", "NOVITA_BASE_URL"), - base_url="https://api.novita.ai/openai/v1", - auth_type="api_key", + signup_url="https://novita.ai/settings/key-management", env_vars=("NOVITA_API_KEY", "NOVITA_BASE_URL"), + base_url="https://api.novita.ai/openai/v1", auth_type="api_key", default_aux_model="deepseek/deepseek-v3-0324", fallback_models=( "moonshotai/kimi-k2.5", "minimax/minimax-m2.7", "zai-org/glm-5", "deepseek/deepseek-v3-0324", diff --git a/plugins/model-providers/nvidia/__init__.py b/plugins/model-providers/nvidia/__init__.py index 1d48a225c7..335f10147c 100644 --- a/plugins/model-providers/nvidia/__init__.py +++ b/plugins/model-providers/nvidia/__init__.py @@ -25,15 +25,10 @@ class NvidiaProviderProfile(ProviderProfile): nvidia = NvidiaProviderProfile( - name="nvidia", - aliases=("nvidia-nim",), - env_vars=("NVIDIA_API_KEY",), - display_name="NVIDIA NIM", - description="NVIDIA NIM — accelerated inference", - signup_url="https://build.nvidia.com/", + name="nvidia", aliases=("nvidia-nim",), env_vars=("NVIDIA_API_KEY",), display_name="NVIDIA NIM", + description="NVIDIA NIM — accelerated inference", signup_url="https://build.nvidia.com/", fallback_models=("nvidia/llama-3.1-nemotron-70b-instruct", "nvidia/llama-3.3-70b-instruct"), - base_url="https://integrate.api.nvidia.com/v1", - default_max_tokens=16384, + base_url="https://integrate.api.nvidia.com/v1", default_max_tokens=16384, ) register_provider(nvidia) diff --git a/plugins/model-providers/ollama-cloud/__init__.py b/plugins/model-providers/ollama-cloud/__init__.py index a9e467586e..9218d54c1a 100644 --- a/plugins/model-providers/ollama-cloud/__init__.py +++ b/plugins/model-providers/ollama-cloud/__init__.py @@ -38,11 +38,8 @@ class OllamaCloudProfile(ProviderProfile): ollama_cloud = OllamaCloudProfile( - name="ollama-cloud", - aliases=("ollama_cloud",), - default_aux_model="nemotron-3-nano:30b", - env_vars=("OLLAMA_API_KEY",), - base_url="https://ollama.com/v1", + name="ollama-cloud", aliases=("ollama_cloud",), default_aux_model="nemotron-3-nano:30b", + env_vars=("OLLAMA_API_KEY",), base_url="https://ollama.com/v1", ) register_provider(ollama_cloud) diff --git a/plugins/model-providers/openai-codex/__init__.py b/plugins/model-providers/openai-codex/__init__.py index 8124b9efe4..69e44a1527 100644 --- a/plugins/model-providers/openai-codex/__init__.py +++ b/plugins/model-providers/openai-codex/__init__.py @@ -4,12 +4,9 @@ from providers import register_provider from providers.base import ProviderProfile openai_codex = ProviderProfile( - name="openai-codex", - aliases=("codex", "openai_codex"), - api_mode="codex_responses", + name="openai-codex", aliases=("codex", "openai_codex"), api_mode="codex_responses", env_vars=(), # OAuth external — no API key - base_url="https://chatgpt.com/backend-api/codex", - auth_type="oauth_external", + base_url="https://chatgpt.com/backend-api/codex", auth_type="oauth_external", ) register_provider(openai_codex) diff --git a/plugins/model-providers/opencode-free/__init__.py b/plugins/model-providers/opencode-free/__init__.py index 9778b6a3a5..29bc4a8617 100644 --- a/plugins/model-providers/opencode-free/__init__.py +++ b/plugins/model-providers/opencode-free/__init__.py @@ -36,11 +36,9 @@ class OpenCodeFreeProfile(ProviderProfile): opencode_free = OpenCodeFreeProfile( - name="opencode-free", - aliases=("free", "opencode_free"), + name="opencode-free", aliases=("free", "opencode_free"), env_vars=(), # keyless — nothing to configure - base_url="https://opencode.ai/zen/v1", - display_name="OpenCode Free", + base_url="https://opencode.ai/zen/v1", display_name="OpenCode Free", description="OpenCode free models — keyless, no account needed", # Attribution headers (same values as opencode-zen/go) plus the empty Authorization # override that keeps the SDK's "Bearer " off the wire (free tier 401s it). diff --git a/plugins/model-providers/opencode-zen/__init__.py b/plugins/model-providers/opencode-zen/__init__.py index dba9aa6c43..a94bbca85a 100644 --- a/plugins/model-providers/opencode-zen/__init__.py +++ b/plugins/model-providers/opencode-zen/__init__.py @@ -108,20 +108,14 @@ class OpenCodeZenProfile(ProviderProfile): opencode_zen = OpenCodeZenProfile( - name="opencode-zen", - aliases=("opencode", "opencode_zen", "zen"), - env_vars=("OPENCODE_ZEN_API_KEY",), - base_url="https://opencode.ai/zen/v1", - default_headers=dict(_ATTRIBUTION_HEADERS), + name="opencode-zen", aliases=("opencode", "opencode_zen", "zen"), env_vars=("OPENCODE_ZEN_API_KEY",), + base_url="https://opencode.ai/zen/v1", default_headers=dict(_ATTRIBUTION_HEADERS), default_aux_model="gemini-3-flash", ) opencode_go = OpenCodeGoProfile( - name="opencode-go", - aliases=("opencode_go", "go", "opencode-go-sub"), - env_vars=("OPENCODE_GO_API_KEY",), - base_url="https://opencode.ai/zen/go/v1", - default_headers=dict(_ATTRIBUTION_HEADERS), + name="opencode-go", aliases=("opencode_go", "go", "opencode-go-sub"), env_vars=("OPENCODE_GO_API_KEY",), + base_url="https://opencode.ai/zen/go/v1", default_headers=dict(_ATTRIBUTION_HEADERS), default_aux_model="glm-5", ) diff --git a/plugins/model-providers/openrouter/__init__.py b/plugins/model-providers/openrouter/__init__.py index cf9fb6147c..580b999184 100644 --- a/plugins/model-providers/openrouter/__init__.py +++ b/plugins/model-providers/openrouter/__init__.py @@ -143,14 +143,9 @@ class OpenRouterProfile(ProviderProfile): openrouter = OpenRouterProfile( - name="openrouter", - aliases=("or",), - env_vars=("OPENROUTER_API_KEY",), - display_name="OpenRouter", - description="OpenRouter — unified API for 200+ models", - signup_url="https://openrouter.ai/keys", - base_url="https://openrouter.ai/api/v1", - models_url="https://openrouter.ai/api/v1/models", + name="openrouter", aliases=("or",), env_vars=("OPENROUTER_API_KEY",), display_name="OpenRouter", + description="OpenRouter — unified API for 200+ models", signup_url="https://openrouter.ai/keys", + base_url="https://openrouter.ai/api/v1", models_url="https://openrouter.ai/api/v1/models", fallback_models=( "anthropic/claude-sonnet-4.6", "openai/gpt-5.4", "deepseek/deepseek-chat", "google/gemini-3.8-flash", "qwen/qwen3-plus", diff --git a/plugins/model-providers/qwen-oauth/__init__.py b/plugins/model-providers/qwen-oauth/__init__.py index 98e166bf1e..4047856b04 100644 --- a/plugins/model-providers/qwen-oauth/__init__.py +++ b/plugins/model-providers/qwen-oauth/__init__.py @@ -65,12 +65,8 @@ class QwenProfile(ProviderProfile): qwen = QwenProfile( - name="qwen-oauth", - aliases=("qwen", "qwen-portal", "qwen-cli"), - env_vars=("QWEN_API_KEY",), - base_url="https://portal.qwen.ai/v1", - auth_type="oauth_external", - default_max_tokens=65536, + name="qwen-oauth", aliases=("qwen", "qwen-portal", "qwen-cli"), env_vars=("QWEN_API_KEY",), + base_url="https://portal.qwen.ai/v1", auth_type="oauth_external", default_max_tokens=65536, ) register_provider(qwen) diff --git a/plugins/model-providers/router/__init__.py b/plugins/model-providers/router/__init__.py index 01f296986a..35a4d1dd8a 100644 --- a/plugins/model-providers/router/__init__.py +++ b/plugins/model-providers/router/__init__.py @@ -253,19 +253,15 @@ class RouterProfile(ProviderProfile): router = RouterProfile( - name="router", - aliases=("ramp-router", "ramp", "router.com"), - api_mode="codex_responses", + name="router", aliases=("ramp-router", "ramp", "router.com"), api_mode="codex_responses", display_name="Ramp Router", description="Ramp Router (router.com) — routes each request to the cheapest model that clears your quality bar", signup_url="https://app.router.com/keys", - env_vars=("RAMP_ROUTER_API_KEY", "ROUTER_API_KEY", "RAMP_ROUTER_BASE_URL"), - base_url=_base_url(), + env_vars=("RAMP_ROUTER_API_KEY", "ROUTER_API_KEY", "RAMP_ROUTER_BASE_URL"), base_url=_base_url(), auth_type="api_key", # Router attributes coding-agent clients by UA prefix; its WAF rejects default UAs. default_headers={"User-Agent": f"Hermes-Agent/{_HERMES_VERSION}"}, - supports_vision=True, - default_aux_model="gpt-5.4-mini", + supports_vision=True, default_aux_model="gpt-5.4-mini", fallback_models=(), # account-scoped IDs; the picker uses fetch_models() ) diff --git a/plugins/model-providers/stepfun/__init__.py b/plugins/model-providers/stepfun/__init__.py index 1ec92cd8be..dae2714ee8 100644 --- a/plugins/model-providers/stepfun/__init__.py +++ b/plugins/model-providers/stepfun/__init__.py @@ -4,11 +4,8 @@ from providers import register_provider from providers.base import ProviderProfile stepfun = ProviderProfile( - name="stepfun", - aliases=("step", "stepfun-coding-plan"), - default_aux_model="step-3.5-flash", - env_vars=("STEPFUN_API_KEY",), - base_url="https://api.stepfun.ai/step_plan/v1", + name="stepfun", aliases=("step", "stepfun-coding-plan"), default_aux_model="step-3.5-flash", + env_vars=("STEPFUN_API_KEY",), base_url="https://api.stepfun.ai/step_plan/v1", ) register_provider(stepfun) diff --git a/plugins/model-providers/upstage/__init__.py b/plugins/model-providers/upstage/__init__.py index 324c79a3c5..f054e79b9e 100644 --- a/plugins/model-providers/upstage/__init__.py +++ b/plugins/model-providers/upstage/__init__.py @@ -43,14 +43,9 @@ class UpstageProfile(ProviderProfile): upstage = UpstageProfile( - name="upstage", - aliases=("solar",), - display_name="Upstage Solar", - description="Upstage (Solar API)", - signup_url="https://console.upstage.ai/api-keys", - env_vars=("UPSTAGE_API_KEY", "UPSTAGE_BASE_URL"), - base_url="https://api.upstage.ai/v1", - auth_type="api_key", + name="upstage", aliases=("solar",), display_name="Upstage Solar", description="Upstage (Solar API)", + signup_url="https://console.upstage.ai/api-keys", env_vars=("UPSTAGE_API_KEY", "UPSTAGE_BASE_URL"), + base_url="https://api.upstage.ai/v1", auth_type="api_key", # No default_aux_model: auxiliary tasks use the main model. [0] is the setup default. fallback_models=("solar-pro3",), ) diff --git a/plugins/model-providers/vertex/__init__.py b/plugins/model-providers/vertex/__init__.py index 4b7583a5c5..51fe68ca41 100644 --- a/plugins/model-providers/vertex/__init__.py +++ b/plugins/model-providers/vertex/__init__.py @@ -30,13 +30,10 @@ class VertexProfile(ProviderProfile): vertex = VertexProfile( - name="vertex", - aliases=("google-vertex", "vertex-ai", "gcp-vertex"), - api_mode="chat_completions", + name="vertex", aliases=("google-vertex", "vertex-ai", "gcp-vertex"), api_mode="chat_completions", env_vars=(), # OAuth2 via service account / ADC — not a static key env var base_url="https://aiplatform.googleapis.com", # real base_url computed at runtime - auth_type="vertex", - default_aux_model="google/gemini-3.6-flash", + auth_type="vertex", default_aux_model="google/gemini-3.6-flash", ) register_provider(vertex) diff --git a/plugins/model-providers/xai/__init__.py b/plugins/model-providers/xai/__init__.py index 89b77d1e1a..a1e6cf27ac 100644 --- a/plugins/model-providers/xai/__init__.py +++ b/plugins/model-providers/xai/__init__.py @@ -5,12 +5,8 @@ from providers import register_provider from providers.base import ProviderProfile xai = ProviderProfile( - name="xai", - aliases=("grok", "x-ai", "x.ai"), - api_mode="codex_responses", - env_vars=("XAI_API_KEY",), - base_url="https://api.x.ai/v1", - auth_type="api_key", + name="xai", aliases=("grok", "x-ai", "x.ai"), api_mode="codex_responses", env_vars=("XAI_API_KEY",), + base_url="https://api.x.ai/v1", auth_type="api_key", default_headers={"User-Agent": f"Hermes-Agent/{_HERMES_VERSION}"}, ) diff --git a/plugins/model-providers/xiaomi/__init__.py b/plugins/model-providers/xiaomi/__init__.py index 8cd378d760..a9db68301a 100644 --- a/plugins/model-providers/xiaomi/__init__.py +++ b/plugins/model-providers/xiaomi/__init__.py @@ -4,9 +4,7 @@ from providers import register_provider from providers.base import ProviderProfile xiaomi = ProviderProfile( - name="xiaomi", - aliases=("mimo", "xiaomi-mimo"), - env_vars=("XIAOMI_API_KEY",), + name="xiaomi", aliases=("mimo", "xiaomi-mimo"), env_vars=("XIAOMI_API_KEY",), base_url="https://api.xiaomimimo.com/v1", supports_health_check=False, # /v1/models returns 401 even with valid key supports_vision=True, # mimo-v2-omni is vision-capable diff --git a/plugins/model-providers/zai/__init__.py b/plugins/model-providers/zai/__init__.py index 05105e779f..482a11866d 100644 --- a/plugins/model-providers/zai/__init__.py +++ b/plugins/model-providers/zai/__init__.py @@ -67,14 +67,10 @@ class ZaiProfile(ProviderProfile): zai = ZaiProfile( - name="zai", - aliases=("glm", "z-ai", "z.ai", "zhipu"), - env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"), - display_name="Z.AI (GLM)", - description="Z.AI / GLM — Zhipu AI models", - signup_url="https://z.ai/", - fallback_models=("glm-5.2", "glm-5", "glm-4-9b"), - base_url="https://api.z.ai/api/paas/v4", + name="zai", aliases=("glm", "z-ai", "z.ai", "zhipu"), + env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"), display_name="Z.AI (GLM)", + description="Z.AI / GLM — Zhipu AI models", signup_url="https://z.ai/", + fallback_models=("glm-5.2", "glm-5", "glm-4-9b"), base_url="https://api.z.ai/api/paas/v4", default_aux_model="glm-4.5-flash", ) From 48da3172114c0e3749c16b881bfdda3baea7e1cc Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:58:26 -0700 Subject: [PATCH 4/4] refactor(model-providers): tighten router/openrouter bodies, drop __future__ annotations (py>=3.11) --- plugins/model-providers/actual/__init__.py | 2 -- .../model-providers/commandcode/__init__.py | 2 -- plugins/model-providers/deepseek/__init__.py | 2 -- plugins/model-providers/meta-ai/__init__.py | 2 -- .../nebius-token-factory/__init__.py | 2 -- .../model-providers/ollama-cloud/__init__.py | 2 -- .../model-providers/opencode-zen/__init__.py | 2 -- .../model-providers/openrouter/__init__.py | 12 ++----- plugins/model-providers/router/__init__.py | 34 +++++++------------ plugins/model-providers/zai/__init__.py | 2 -- 10 files changed, 15 insertions(+), 47 deletions(-) diff --git a/plugins/model-providers/actual/__init__.py b/plugins/model-providers/actual/__init__.py index 8506eece8b..03a3a500c8 100644 --- a/plugins/model-providers/actual/__init__.py +++ b/plugins/model-providers/actual/__init__.py @@ -1,7 +1,5 @@ """Actual Computer provider profile.""" -from __future__ import annotations - import os from providers import register_provider diff --git a/plugins/model-providers/commandcode/__init__.py b/plugins/model-providers/commandcode/__init__.py index 4346b096e2..cf031c6813 100644 --- a/plugins/model-providers/commandcode/__init__.py +++ b/plugins/model-providers/commandcode/__init__.py @@ -2,8 +2,6 @@ ``commandcode-anthropic`` (anthropic_messages, Bearer auth — see ``agent/anthropic_adapter.py``). Same key and base URL for both.""" -from __future__ import annotations - import json import logging import urllib.request diff --git a/plugins/model-providers/deepseek/__init__.py b/plugins/model-providers/deepseek/__init__.py index 6d175375f3..27e49dc623 100644 --- a/plugins/model-providers/deepseek/__init__.py +++ b/plugins/model-providers/deepseek/__init__.py @@ -8,8 +8,6 @@ Retired ``deepseek-chat``/``deepseek-reasoner`` IDs are remapped in ``hermes_cli.model_normalize`` before reaching here. """ -from __future__ import annotations - from typing import Any from agent.reasoning_effort import DEEPSEEK_V4_EFFORTS, DEEPSEEK_V4_OVERRIDES, clamp_effort diff --git a/plugins/model-providers/meta-ai/__init__.py b/plugins/model-providers/meta-ai/__init__.py index 8cddbbb43e..fcef99fc25 100644 --- a/plugins/model-providers/meta-ai/__init__.py +++ b/plugins/model-providers/meta-ai/__init__.py @@ -6,8 +6,6 @@ top-level ``reasoning_effort`` kwarg — not ``extra_body.reasoning``, whose emission is gated by a core host allowlist a third-party plugin must not edit. """ -from __future__ import annotations - import os from typing import Any diff --git a/plugins/model-providers/nebius-token-factory/__init__.py b/plugins/model-providers/nebius-token-factory/__init__.py index 74794c0bf0..5eac728883 100644 --- a/plugins/model-providers/nebius-token-factory/__init__.py +++ b/plugins/model-providers/nebius-token-factory/__init__.py @@ -1,7 +1,5 @@ """Nebius Token Factory provider profile.""" -from __future__ import annotations - from typing import Any from agent.reasoning_effort import NEBIUS_EFFORTS, clamp_effort diff --git a/plugins/model-providers/ollama-cloud/__init__.py b/plugins/model-providers/ollama-cloud/__init__.py index 9218d54c1a..5418e19334 100644 --- a/plugins/model-providers/ollama-cloud/__init__.py +++ b/plugins/model-providers/ollama-cloud/__init__.py @@ -5,8 +5,6 @@ high|max (``max`` is undocumented but real — ~2.5x more thinking tokens on DeepSeek V4); Hermes' ``xhigh`` maps to ``max``. """ -from __future__ import annotations - from typing import Any from agent.reasoning_effort import OLLAMA_CLOUD_EFFORTS, OLLAMA_CLOUD_OVERRIDES, clamp_effort diff --git a/plugins/model-providers/opencode-zen/__init__.py b/plugins/model-providers/opencode-zen/__init__.py index a94bbca85a..28aacf5956 100644 --- a/plugins/model-providers/opencode-zen/__init__.py +++ b/plugins/model-providers/opencode-zen/__init__.py @@ -4,8 +4,6 @@ Both route api_mode per model in core; these profiles carry the chat_completions reasoning translations (GLM-5.2, Kimi K2, DeepSeek, Ox Alpha). """ -from __future__ import annotations - from typing import Any from agent import reasoning_effort as re_ diff --git a/plugins/model-providers/openrouter/__init__.py b/plugins/model-providers/openrouter/__init__.py index 580b999184..391167b409 100644 --- a/plugins/model-providers/openrouter/__init__.py +++ b/plugins/model-providers/openrouter/__init__.py @@ -63,8 +63,7 @@ class OpenRouterProfile(ProviderProfile): return cfg if clamped and clamped != effort: logger.debug( - "openrouter: clamped reasoning effort %r → %r for %s " - "(catalog supported_efforts=%s)", + "openrouter: clamped reasoning effort %r → %r for %s (catalog supported_efforts=%s)", effort, clamped, model, caps.get("supported_efforts"), ) cfg = {**cfg, "effort": clamped} @@ -109,13 +108,8 @@ class OpenRouterProfile(ProviderProfile): return body def build_api_kwargs_extras( - self, - *, - reasoning_config: dict | None = None, - supports_reasoning: bool = False, - model: str | None = None, - session_id: str | None = None, - **context: Any, + self, *, reasoning_config: dict | None = None, supports_reasoning: bool = False, + model: str | None = None, session_id: str | None = None, **context: Any, ) -> tuple[dict[str, Any], dict[str, Any]]: """Pass reasoning_config as extra_body.reasoning; pin Grok's cache via x-grok-conv-id.""" extra_body: dict[str, Any] = {} diff --git a/plugins/model-providers/router/__init__.py b/plugins/model-providers/router/__init__.py index 35a4d1dd8a..1c485e8d02 100644 --- a/plugins/model-providers/router/__init__.py +++ b/plugins/model-providers/router/__init__.py @@ -1,16 +1,13 @@ -"""Ramp Router (router.com) provider profile: Responses-only LLM gateway. +"""Ramp Router (router.com) provider profile: Responses-only LLM gateway (verified live). -Wire notes (verified live): the Responses API is the native wire (``api_mode= -"codex_responses"`` + the ``api.router.com`` host mandate in ``hermes_cli/providers.py``); -the catalog is account-scoped so there are no ``fallback_models`` (picker uses -``fetch_models()``); Router 400s on ``reasoning.effort`` levels outside a model's -published vocabulary and on any reasoning field for non-reasoning models, so the -efforts map from ``GET /v1/models`` is cached (memory + disk mirror, background -warmer; never HTTP on the request hot path) and fed to the codex transport's clamp. +``api_mode="codex_responses"`` + the ``api.router.com`` host mandate in ``hermes_cli/providers.py`` +keep every path on the native wire. The catalog is account-scoped, so no ``fallback_models`` +(picker uses ``fetch_models()``). Router 400s on ``reasoning.effort`` levels outside a model's +published vocabulary and on any reasoning field for non-reasoning models, so the efforts map +from ``GET /v1/models`` is cached (memory + disk mirror, background warmer; never HTTP on the +request hot path) and fed to the codex transport's clamp via ``supported_reasoning_efforts``. """ -from __future__ import annotations - import json import logging import os @@ -92,10 +89,8 @@ def _parse_efforts(items: Any) -> Optional[dict[str, list[str]]]: unknown = [level for level in levels if level not in EFFORT_LADDER] if unknown: logger.info( - "router: model %s publishes unrecognized reasoning effort " - "level(s) %s; ignoring them (update agent/reasoning_effort " - "EFFORT_LADDER to adopt new vendor tiers)", - mid, unknown, + "router: model %s publishes unrecognized reasoning effort level(s) %s; ignoring them " + "(update agent/reasoning_effort EFFORT_LADDER to adopt new vendor tiers)", mid, unknown, ) levels = [level for level in levels if level in EFFORT_LADDER] if levels: @@ -106,7 +101,6 @@ def _parse_efforts(items: Any) -> Optional[dict[str, list[str]]]: def _disk_path() -> Optional[Path]: try: from hermes_constants import get_hermes_home - return get_hermes_home() / "cache" / "router_catalog.json" except Exception: return None @@ -190,9 +184,7 @@ def _efforts_cache_only() -> Optional[dict[str, list[str]]]: if parsed is None: return None with _efforts_lock: - if _efforts_cache is None: - _efforts_cache = parsed - cached = _efforts_cache + _efforts_cache = cached = _efforts_cache if _efforts_cache is not None else parsed if age >= _DISK_TTL_SECONDS: _warm_efforts_async() return cached @@ -218,7 +210,6 @@ def _warm_efforts_async() -> None: items = _fetch_catalog_items() if items is not None: _seed_efforts(items) - try: threading.Thread(target=_refresh, name="router-caps-warm", daemon=True).start() except Exception as exc: @@ -237,8 +228,7 @@ class RouterProfile(ProviderProfile): if items is None: return None _seed_efforts(items) - ids = list(dict.fromkeys(str(i["id"]) for i in items if isinstance(i, dict) and i.get("id"))) - return ids or None + return list(dict.fromkeys(str(i["id"]) for i in items if isinstance(i, dict) and i.get("id"))) or None def supported_reasoning_efforts(self, model: Optional[str]) -> Optional[tuple[str, ...]]: """Catalog-declared effort vocabulary (cache-only; cold cache -> None + warm).""" @@ -249,7 +239,7 @@ class RouterProfile(ProviderProfile): if efforts_by_id is None: _warm_efforts_async() return None - return None if mid not in efforts_by_id else tuple(efforts_by_id[mid]) + return tuple(efforts_by_id[mid]) if mid in efforts_by_id else None router = RouterProfile( diff --git a/plugins/model-providers/zai/__init__.py b/plugins/model-providers/zai/__init__.py index 482a11866d..f27cfa0f69 100644 --- a/plugins/model-providers/zai/__init__.py +++ b/plugins/model-providers/zai/__init__.py @@ -4,8 +4,6 @@ GLM-4.5+ defaults to thinking ON, so ``reasoning_config`` is translated to ``extra_body.thinking``; GLM-5.2/5.3 also take a native ``reasoning_effort``. """ -from __future__ import annotations - import re from typing import Any