merge(r3-27): group E
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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,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",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"},
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user