merge(r3-27): group E

This commit is contained in:
Teknium
2026-09-02 22:52:16 -07:00
39 changed files with 259 additions and 644 deletions
+10 -55
View File
@@ -1,36 +1,11 @@
"""Actual Computer provider profile."""
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,39 +15,19 @@ 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(
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)
@@ -19,14 +19,9 @@ 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",
default_headers={
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Title": "Hermes Agent",
},
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",
)
@@ -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)
+8 -17
View File
@@ -9,45 +9,36 @@ 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"),
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",),
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)
register_provider(alibaba_cn)
register_provider(alibaba_token_plan)
register_provider(alibaba_token_plan_cn)
@@ -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]
@@ -34,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)
+1 -3
View File
@@ -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",
)
@@ -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",
)
+2 -5
View File
@@ -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)
+26 -57
View File
@@ -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
@@ -17,90 +15,61 @@ _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(
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",
"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",
)
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",
)
@@ -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
@@ -30,15 +29,13 @@ 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
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"),
+7 -14
View File
@@ -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:
@@ -47,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",
)
+3 -8
View File
@@ -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,
@@ -66,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
+7 -15
View File
@@ -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():
@@ -34,16 +31,11 @@ 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",
# 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",
+6 -14
View File
@@ -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
@@ -24,12 +22,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] = {}
@@ -43,14 +40,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",
)
@@ -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={
@@ -25,8 +21,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",
),
)
+2 -5
View File
@@ -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",
)
+5 -13
View File
@@ -5,24 +5,16 @@ 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",
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",
),
)
@@ -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",
+2 -5
View File
@@ -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)
+11 -21
View File
@@ -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)
+10 -25
View File
@@ -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
@@ -20,18 +18,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":
@@ -43,23 +34,17 @@ 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",
# 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.
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",
# Muse spends completion budget on hidden reasoning first; low caps can
# finish with empty content.
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"),
)
+9 -27
View File
@@ -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,36 +38,22 @@ 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",
)
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)
@@ -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
@@ -10,14 +8,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 +16,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):
@@ -47,24 +34,16 @@ 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",
"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",
),
)
+10 -28
View File
@@ -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)."""
@@ -79,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)
+5 -13
View File
@@ -5,22 +5,14 @@ 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",
"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",
),
)
+4 -13
View File
@@ -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
@@ -29,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)
@@ -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
@@ -38,11 +36,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)
@@ -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)
@@ -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(
@@ -36,15 +36,12 @@ 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 <placeholder>" 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 <placeholder>" off the wire (free tier 401s it).
default_headers={
"Authorization": "",
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
@@ -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_
@@ -23,7 +21,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 +31,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 +60,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 +81,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 +92,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):
@@ -116,20 +106,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",
)
+13 -36
View File
@@ -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
@@ -70,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}
@@ -80,8 +72,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 +96,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 != "":
@@ -117,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] = {}
@@ -143,7 +129,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-")):
@@ -152,19 +137,11 @@ 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",
"anthropic/claude-sonnet-4.6", "openai/gpt-5.4", "deepseek/deepseek-chat", "google/gemini-3.8-flash",
"qwen/qwen3-plus",
),
)
+5 -14
View File
@@ -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,23 +58,15 @@ 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 {}
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)
+28 -56
View File
@@ -1,22 +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 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.
``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
@@ -51,16 +42,13 @@ 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."""
resolvers: list = [lambda var: os.environ.get(var, "")]
"""Router key (documented var, then alias), preferring dotenv; plain os.environ
is the fallback when the dotenv resolver is unavailable or raises."""
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()
@@ -81,10 +69,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
@@ -102,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:
@@ -116,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
@@ -128,7 +112,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:
@@ -166,9 +150,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 +161,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())
@@ -203,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
@@ -231,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:
@@ -244,14 +222,13 @@ 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:
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)."""
@@ -262,25 +239,20 @@ 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(
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",
# Empty on purpose: model IDs are account-scoped; the picker uses fetch_models().
fallback_models=(),
supports_vision=True, default_aux_model="gpt-5.4-mini",
fallback_models=(), # account-scoped IDs; the picker uses fetch_models()
)
register_provider(router)
+2 -5
View File
@@ -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)
+9 -18
View File
@@ -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,22 +36,16 @@ 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 {}
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",),
)
+9 -20
View File
@@ -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
@@ -38,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)
+2 -6
View File
@@ -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}"},
)
+1 -3
View File
@@ -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
+7 -17
View File
@@ -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
@@ -27,14 +25,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 +53,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:
@@ -71,14 +65,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",
)