feat(providers): add tencent-tokenhub provider support
Registers tencent-tokenhub (https://tokenhub.tencentmaas.com/v1) as a new API-key provider with model tencent/hy3-preview (256K context). - PROVIDER_REGISTRY entry + TOKENHUB_API_KEY / TOKENHUB_BASE_URL env vars - Aliases: tencent, tokenhub, tencent-cloud, tencentmaas - openai_chat transport with is_tokenhub branch for top-level reasoning_effort (Hy3 is a reasoning model) - tencent/hy3-preview:free added to OpenRouter curated list - 60+ tests (provider registry, aliases, runtime resolution, credentials, model catalog, URL mapping, context length) - Docs: integrations/providers.md, environment-variables.md, model-catalog.json Author: simonweng <simonweng@tencent.com> Salvaged from PR #16860 onto current main (resolved conflicts with #16935 Azure Anthropic env-var hint tests and the --provider choices= list removal in chat_parser).
This commit is contained in:
parent
bd10acd747
commit
a6a6cf047d
@ -94,6 +94,10 @@ _PROVIDER_ALIASES = {
|
||||
"github-models": "copilot",
|
||||
"github-copilot-acp": "copilot-acp",
|
||||
"copilot-acp-agent": "copilot-acp",
|
||||
"tencent": "tencent-tokenhub",
|
||||
"tokenhub": "tencent-tokenhub",
|
||||
"tencent-cloud": "tencent-tokenhub",
|
||||
"tencentmaas": "tencent-tokenhub",
|
||||
}
|
||||
|
||||
|
||||
@ -166,6 +170,7 @@ _API_KEY_PROVIDER_AUX_MODELS: Dict[str, str] = {
|
||||
"opencode-go": "glm-5",
|
||||
"kilocode": "google/gemini-3-flash-preview",
|
||||
"ollama-cloud": "nemotron-3-nano:30b",
|
||||
"tencent-tokenhub": "hy3-preview",
|
||||
}
|
||||
|
||||
# Vision-specific model overrides for direct providers.
|
||||
|
||||
@ -52,6 +52,7 @@ _PROVIDER_PREFIXES: frozenset[str] = frozenset({
|
||||
"xiaomi",
|
||||
"arcee",
|
||||
"gmi",
|
||||
"tencent-tokenhub",
|
||||
"custom", "local",
|
||||
# Common aliases
|
||||
"google", "google-gemini", "google-ai-studio",
|
||||
@ -60,6 +61,7 @@ _PROVIDER_PREFIXES: frozenset[str] = frozenset({
|
||||
"ollama",
|
||||
"stepfun", "opencode", "zen", "go", "vercel", "kilo", "dashscope", "aliyun", "qwen",
|
||||
"mimo", "xiaomi-mimo",
|
||||
"tencent", "tokenhub", "tencent-cloud", "tencentmaas",
|
||||
"arcee-ai", "arceeai",
|
||||
"gmi-cloud", "gmicloud",
|
||||
"xai", "x-ai", "x.ai", "grok",
|
||||
@ -208,6 +210,8 @@ DEFAULT_CONTEXT_LENGTHS = {
|
||||
"grok": 131072, # catch-all (grok-beta, unknown grok-*)
|
||||
# Kimi
|
||||
"kimi": 262144,
|
||||
# Tencent — Hy3 Preview (Hunyuan) with 256K context window
|
||||
"hy3-preview": 256000,
|
||||
# Nemotron — NVIDIA's open-weights series (128K context across all sizes)
|
||||
"nemotron": 131072,
|
||||
# Arcee
|
||||
@ -310,6 +314,7 @@ _URL_TO_PROVIDER: Dict[str, str] = {
|
||||
"api.xiaomimimo.com": "xiaomi",
|
||||
"xiaomimimo.com": "xiaomi",
|
||||
"api.gmi-serving.com": "gmi",
|
||||
"tokenhub.tencentmaas.com": "tencent-tokenhub",
|
||||
"ollama.com": "ollama-cloud",
|
||||
}
|
||||
|
||||
|
||||
@ -188,6 +188,7 @@ class ChatCompletionsTransport(ProviderTransport):
|
||||
anthropic_max_out = params.get("anthropic_max_output")
|
||||
is_nvidia_nim = params.get("is_nvidia_nim", False)
|
||||
is_kimi = params.get("is_kimi", False)
|
||||
is_tokenhub = params.get("is_tokenhub", False)
|
||||
reasoning_config = params.get("reasoning_config")
|
||||
|
||||
if ephemeral is not None and max_tokens_fn:
|
||||
@ -219,6 +220,21 @@ class ChatCompletionsTransport(ProviderTransport):
|
||||
_kimi_effort = _e
|
||||
api_kwargs["reasoning_effort"] = _kimi_effort
|
||||
|
||||
# Tencent TokenHub: top-level reasoning_effort (unless thinking disabled)
|
||||
if is_tokenhub:
|
||||
_tokenhub_thinking_off = bool(
|
||||
reasoning_config
|
||||
and isinstance(reasoning_config, dict)
|
||||
and reasoning_config.get("enabled") is False
|
||||
)
|
||||
if not _tokenhub_thinking_off:
|
||||
_tokenhub_effort = "high"
|
||||
if reasoning_config and isinstance(reasoning_config, dict):
|
||||
_e = (reasoning_config.get("effort") or "").strip().lower()
|
||||
if _e in ("low", "medium", "high"):
|
||||
_tokenhub_effort = _e
|
||||
api_kwargs["reasoning_effort"] = _tokenhub_effort
|
||||
|
||||
# extra_body assembly
|
||||
extra_body: Dict[str, Any] = {}
|
||||
|
||||
|
||||
@ -348,6 +348,14 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
|
||||
api_key_env_vars=("XIAOMI_API_KEY",),
|
||||
base_url_env_var="XIAOMI_BASE_URL",
|
||||
),
|
||||
"tencent-tokenhub": ProviderConfig(
|
||||
id="tencent-tokenhub",
|
||||
name="Tencent TokenHub",
|
||||
auth_type="api_key",
|
||||
inference_base_url="https://tokenhub.tencentmaas.com/v1",
|
||||
api_key_env_vars=("TOKENHUB_API_KEY",),
|
||||
base_url_env_var="TOKENHUB_BASE_URL",
|
||||
),
|
||||
"ollama-cloud": ProviderConfig(
|
||||
id="ollama-cloud",
|
||||
name="Ollama Cloud",
|
||||
@ -1141,6 +1149,8 @@ def resolve_provider(
|
||||
"qwen-portal": "qwen-oauth", "qwen-cli": "qwen-oauth", "qwen-oauth": "qwen-oauth", "google-gemini-cli": "google-gemini-cli", "gemini-cli": "google-gemini-cli", "gemini-oauth": "google-gemini-cli",
|
||||
"hf": "huggingface", "hugging-face": "huggingface", "huggingface-hub": "huggingface",
|
||||
"mimo": "xiaomi", "xiaomi-mimo": "xiaomi",
|
||||
"tencent": "tencent-tokenhub", "tokenhub": "tencent-tokenhub",
|
||||
"tencent-cloud": "tencent-tokenhub", "tencentmaas": "tencent-tokenhub",
|
||||
"aws": "bedrock", "aws-bedrock": "bedrock", "amazon-bedrock": "bedrock", "amazon": "bedrock",
|
||||
"go": "opencode-go", "opencode-go-sub": "opencode-go",
|
||||
"kilo": "kilocode", "kilo-code": "kilocode", "kilo-gateway": "kilocode",
|
||||
|
||||
@ -57,6 +57,7 @@ _PROVIDER_ENV_HINTS = (
|
||||
"OPENCODE_ZEN_API_KEY",
|
||||
"OPENCODE_GO_API_KEY",
|
||||
"XIAOMI_API_KEY",
|
||||
"TOKENHUB_API_KEY",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -1820,6 +1820,7 @@ def select_provider_and_model(args=None):
|
||||
"gmi",
|
||||
"nvidia",
|
||||
"ollama-cloud",
|
||||
"tencent-tokenhub",
|
||||
):
|
||||
_model_flow_api_key_provider(config, selected_provider, current_model)
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ OPENROUTER_MODELS: list[tuple[str, str]] = [
|
||||
("openai/gpt-5.4-mini", ""),
|
||||
("xiaomi/mimo-v2.5-pro", ""),
|
||||
("xiaomi/mimo-v2.5", ""),
|
||||
("tencent/hy3-preview:free", "free"),
|
||||
("openai/gpt-5.3-codex", ""),
|
||||
("google/gemini-3-pro-image-preview", ""),
|
||||
("google/gemini-3-flash-preview", ""),
|
||||
@ -156,6 +157,7 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
||||
"moonshotai/kimi-k2.6",
|
||||
"xiaomi/mimo-v2.5-pro",
|
||||
"xiaomi/mimo-v2.5",
|
||||
"tencent/hy3-preview",
|
||||
"anthropic/claude-opus-4.7",
|
||||
"anthropic/claude-opus-4.6",
|
||||
"anthropic/claude-sonnet-4.6",
|
||||
@ -315,6 +317,9 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
||||
"mimo-v2-omni",
|
||||
"mimo-v2-flash",
|
||||
],
|
||||
"tencent-tokenhub": [
|
||||
"hy3-preview",
|
||||
],
|
||||
"arcee": [
|
||||
"trinity-large-thinking",
|
||||
"trinity-large-preview",
|
||||
@ -767,6 +772,7 @@ CANONICAL_PROVIDERS: list[ProviderEntry] = [
|
||||
ProviderEntry("anthropic", "Anthropic", "Anthropic (Claude models — API key or Claude Code)"),
|
||||
ProviderEntry("openai-codex", "OpenAI Codex", "OpenAI Codex"),
|
||||
ProviderEntry("xiaomi", "Xiaomi MiMo", "Xiaomi MiMo (MiMo-V2.5 and V2 models — pro, omni, flash)"),
|
||||
ProviderEntry("tencent-tokenhub", "Tencent TokenHub", "Tencent TokenHub (Hy3 Preview — direct API via tokenhub.tencentmaas.com)"),
|
||||
ProviderEntry("nvidia", "NVIDIA NIM", "NVIDIA NIM (Nemotron models — build.nvidia.com or local NIM)"),
|
||||
ProviderEntry("qwen-oauth", "Qwen OAuth (Portal)", "Qwen OAuth (reuses local Qwen CLI login)"),
|
||||
ProviderEntry("copilot", "GitHub Copilot", "GitHub Copilot (uses GITHUB_TOKEN or gh auth token)"),
|
||||
@ -849,6 +855,10 @@ _PROVIDER_ALIASES = {
|
||||
"huggingface-hub": "huggingface",
|
||||
"mimo": "xiaomi",
|
||||
"xiaomi-mimo": "xiaomi",
|
||||
"tencent": "tencent-tokenhub",
|
||||
"tokenhub": "tencent-tokenhub",
|
||||
"tencent-cloud": "tencent-tokenhub",
|
||||
"tencentmaas": "tencent-tokenhub",
|
||||
"aws": "bedrock",
|
||||
"aws-bedrock": "bedrock",
|
||||
"amazon-bedrock": "bedrock",
|
||||
|
||||
@ -158,6 +158,10 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = {
|
||||
transport="openai_chat",
|
||||
base_url_env_var="XIAOMI_BASE_URL",
|
||||
),
|
||||
"tencent-tokenhub": HermesOverlay(
|
||||
transport="openai_chat",
|
||||
base_url_env_var="TOKENHUB_BASE_URL",
|
||||
),
|
||||
"arcee": HermesOverlay(
|
||||
transport="openai_chat",
|
||||
base_url_override="https://api.arcee.ai/api/v1",
|
||||
@ -293,6 +297,12 @@ ALIASES: Dict[str, str] = {
|
||||
"mimo": "xiaomi",
|
||||
"xiaomi-mimo": "xiaomi",
|
||||
|
||||
# tencent
|
||||
"tencent": "tencent-tokenhub",
|
||||
"tokenhub": "tencent-tokenhub",
|
||||
"tencent-cloud": "tencent-tokenhub",
|
||||
"tencentmaas": "tencent-tokenhub",
|
||||
|
||||
# bedrock
|
||||
"aws": "bedrock",
|
||||
"aws-bedrock": "bedrock",
|
||||
@ -330,6 +340,7 @@ _LABEL_OVERRIDES: Dict[str, str] = {
|
||||
"stepfun": "StepFun Step Plan",
|
||||
"xiaomi": "Xiaomi MiMo",
|
||||
"gmi": "GMI Cloud",
|
||||
"tencent-tokenhub": "Tencent TokenHub",
|
||||
"local": "Local endpoint",
|
||||
"bedrock": "AWS Bedrock",
|
||||
"ollama-cloud": "Ollama Cloud",
|
||||
|
||||
@ -7871,6 +7871,7 @@ class AIAgent:
|
||||
or base_url_host_matches(self.base_url, "moonshot.ai")
|
||||
or base_url_host_matches(self.base_url, "moonshot.cn")
|
||||
)
|
||||
_is_tokenhub = base_url_host_matches(self._base_url_lower, "tokenhub.tencentmaas.com")
|
||||
|
||||
# Temperature: _fixed_temperature_for_model may return OMIT_TEMPERATURE
|
||||
# sentinel (temperature omitted entirely), a numeric override, or None.
|
||||
@ -7942,6 +7943,7 @@ class AIAgent:
|
||||
is_github_models=_is_gh,
|
||||
is_nvidia_nim=_is_nvidia,
|
||||
is_kimi=_is_kimi,
|
||||
is_tokenhub=_is_tokenhub,
|
||||
is_custom_provider=self.provider == "custom",
|
||||
ollama_num_ctx=self._ollama_num_ctx,
|
||||
provider_preferences=_prefs or None,
|
||||
@ -7989,6 +7991,7 @@ class AIAgent:
|
||||
"x-ai/",
|
||||
"google/gemini-2",
|
||||
"qwen/qwen3",
|
||||
"tencent/hy3-preview",
|
||||
)
|
||||
return any(model.startswith(prefix) for prefix in reasoning_model_prefixes)
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ _OTHER_PROVIDER_KEYS = (
|
||||
"XAI_API_KEY", "KIMI_API_KEY", "KIMI_CN_API_KEY",
|
||||
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", "AI_GATEWAY_API_KEY",
|
||||
"KILOCODE_API_KEY", "HF_TOKEN", "GLM_API_KEY", "ZAI_API_KEY",
|
||||
"XIAOMI_API_KEY", "COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN",
|
||||
"XIAOMI_API_KEY", "TOKENHUB_API_KEY", "COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -1763,7 +1763,6 @@ class TestAzureFoundryResolution:
|
||||
assert resolved["api_mode"] == "codex_responses"
|
||||
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Azure Anthropic — honor user-specified env var hints (key_env / api_key_env)
|
||||
#
|
||||
@ -1962,3 +1961,84 @@ class TestProviderEntryApiKeyEnvAlias:
|
||||
key_env so the set stays in sync with what the runtime actually reads."""
|
||||
from hermes_cli.config import _VALID_CUSTOM_PROVIDER_FIELDS
|
||||
assert "key_env" in _VALID_CUSTOM_PROVIDER_FIELDS
|
||||
# =============================================================================
|
||||
# Tencent TokenHub — API-key provider runtime resolution
|
||||
# =============================================================================
|
||||
|
||||
class TestTencentTokenhubRuntimeResolution:
|
||||
"""Verify Tencent TokenHub resolves correctly through the generic
|
||||
API-key provider path in resolve_runtime_provider."""
|
||||
|
||||
def test_resolves_with_env_key(self, monkeypatch):
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "tencent-tokenhub")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: {})
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "test-tokenhub-key")
|
||||
monkeypatch.delenv("TOKENHUB_BASE_URL", raising=False)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="tencent-tokenhub")
|
||||
|
||||
assert resolved["provider"] == "tencent-tokenhub"
|
||||
assert resolved["api_mode"] == "chat_completions"
|
||||
assert resolved["base_url"] == "https://tokenhub.tencentmaas.com/v1"
|
||||
assert resolved["api_key"] == "test-tokenhub-key"
|
||||
assert resolved["requested_provider"] == "tencent-tokenhub"
|
||||
|
||||
def test_custom_base_url_from_env(self, monkeypatch):
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "tencent-tokenhub")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: {})
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "test-tokenhub-key")
|
||||
monkeypatch.setenv("TOKENHUB_BASE_URL", "https://custom-proxy.example.com/v1")
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="tencent-tokenhub")
|
||||
|
||||
assert resolved["provider"] == "tencent-tokenhub"
|
||||
assert resolved["base_url"] == "https://custom-proxy.example.com/v1"
|
||||
assert resolved["api_key"] == "test-tokenhub-key"
|
||||
|
||||
def test_config_base_url_honoured_when_provider_matches(self, monkeypatch):
|
||||
"""model.base_url in config.yaml should override the hardcoded default
|
||||
when model.provider == tencent-tokenhub."""
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "tencent-tokenhub")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: {
|
||||
"provider": "tencent-tokenhub",
|
||||
"base_url": "https://proxy.internal.com/v1",
|
||||
})
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "test-tokenhub-key")
|
||||
monkeypatch.delenv("TOKENHUB_BASE_URL", raising=False)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="tencent-tokenhub")
|
||||
|
||||
assert resolved["base_url"] == "https://proxy.internal.com/v1"
|
||||
|
||||
def test_config_base_url_ignored_for_different_provider(self, monkeypatch):
|
||||
"""model.base_url should NOT be used when model.provider doesn't match."""
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "tencent-tokenhub")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: {
|
||||
"provider": "openrouter",
|
||||
"base_url": "https://some-other-endpoint.com/v1",
|
||||
})
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "test-tokenhub-key")
|
||||
monkeypatch.delenv("TOKENHUB_BASE_URL", raising=False)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="tencent-tokenhub")
|
||||
|
||||
# Should use the default, NOT the config base_url from a different provider
|
||||
assert resolved["base_url"] == "https://tokenhub.tencentmaas.com/v1"
|
||||
|
||||
def test_explicit_override_skips_env(self, monkeypatch):
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "tencent-tokenhub")
|
||||
monkeypatch.setattr(rp, "_get_model_config", lambda: {})
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "env-key-should-lose")
|
||||
monkeypatch.delenv("TOKENHUB_BASE_URL", raising=False)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(
|
||||
requested="tencent-tokenhub",
|
||||
explicit_api_key="explicit-tokenhub-key",
|
||||
explicit_base_url="https://explicit-proxy.example.com/v1/",
|
||||
)
|
||||
|
||||
assert resolved["provider"] == "tencent-tokenhub"
|
||||
assert resolved["api_key"] == "explicit-tokenhub-key"
|
||||
assert resolved["base_url"] == "https://explicit-proxy.example.com/v1"
|
||||
assert resolved["source"] == "explicit"
|
||||
|
||||
|
||||
494
tests/hermes_cli/test_tencent_tokenhub_provider.py
Normal file
494
tests/hermes_cli/test_tencent_tokenhub_provider.py
Normal file
@ -0,0 +1,494 @@
|
||||
"""Tests for Tencent TokenHub provider support (Hy3 Preview)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.auth import (
|
||||
PROVIDER_REGISTRY,
|
||||
resolve_provider,
|
||||
get_api_key_provider_status,
|
||||
resolve_api_key_provider_credentials,
|
||||
AuthError,
|
||||
)
|
||||
|
||||
|
||||
# Other provider env vars to clear during auto-detection tests
|
||||
_OTHER_PROVIDER_KEYS = (
|
||||
"OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY",
|
||||
"GOOGLE_API_KEY", "GEMINI_API_KEY", "DASHSCOPE_API_KEY",
|
||||
"XAI_API_KEY", "KIMI_API_KEY", "KIMI_CN_API_KEY",
|
||||
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", "AI_GATEWAY_API_KEY",
|
||||
"KILOCODE_API_KEY", "HF_TOKEN", "GLM_API_KEY", "ZAI_API_KEY",
|
||||
"XIAOMI_API_KEY", "OPENROUTER_API_KEY", "COPILOT_GITHUB_TOKEN",
|
||||
"GH_TOKEN", "GITHUB_TOKEN", "ARCEEAI_API_KEY",
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Provider Registry
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubProviderRegistry:
|
||||
"""Verify tencent-tokenhub is registered correctly in the PROVIDER_REGISTRY."""
|
||||
|
||||
def test_registered(self):
|
||||
assert "tencent-tokenhub" in PROVIDER_REGISTRY
|
||||
|
||||
def test_name(self):
|
||||
assert PROVIDER_REGISTRY["tencent-tokenhub"].name == "Tencent TokenHub"
|
||||
|
||||
def test_auth_type(self):
|
||||
assert PROVIDER_REGISTRY["tencent-tokenhub"].auth_type == "api_key"
|
||||
|
||||
def test_inference_base_url(self):
|
||||
assert PROVIDER_REGISTRY["tencent-tokenhub"].inference_base_url == "https://tokenhub.tencentmaas.com/v1"
|
||||
|
||||
def test_api_key_env_vars(self):
|
||||
assert PROVIDER_REGISTRY["tencent-tokenhub"].api_key_env_vars == ("TOKENHUB_API_KEY",)
|
||||
|
||||
def test_base_url_env_var(self):
|
||||
assert PROVIDER_REGISTRY["tencent-tokenhub"].base_url_env_var == "TOKENHUB_BASE_URL"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Aliases
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubAliases:
|
||||
"""All aliases should resolve to 'tencent-tokenhub'."""
|
||||
|
||||
@pytest.mark.parametrize("alias", [
|
||||
"tencent-tokenhub", "tencent", "tokenhub", "tencent-cloud", "tencentmaas",
|
||||
])
|
||||
def test_alias_resolves(self, alias, monkeypatch):
|
||||
for key in _OTHER_PROVIDER_KEYS:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "sk-test-key-12345678")
|
||||
assert resolve_provider(alias) == "tencent-tokenhub"
|
||||
|
||||
def test_normalize_provider_models_py(self):
|
||||
from hermes_cli.models import normalize_provider
|
||||
assert normalize_provider("tencent") == "tencent-tokenhub"
|
||||
assert normalize_provider("tokenhub") == "tencent-tokenhub"
|
||||
assert normalize_provider("tencent-cloud") == "tencent-tokenhub"
|
||||
assert normalize_provider("tencentmaas") == "tencent-tokenhub"
|
||||
|
||||
def test_normalize_provider_providers_py(self):
|
||||
from hermes_cli.providers import normalize_provider
|
||||
assert normalize_provider("tencent") == "tencent-tokenhub"
|
||||
assert normalize_provider("tokenhub") == "tencent-tokenhub"
|
||||
assert normalize_provider("tencent-cloud") == "tencent-tokenhub"
|
||||
assert normalize_provider("tencentmaas") == "tencent-tokenhub"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Auto-detection
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubAutoDetection:
|
||||
"""Setting TOKENHUB_API_KEY should auto-detect the provider."""
|
||||
|
||||
def test_auto_detect(self, monkeypatch):
|
||||
for var in _OTHER_PROVIDER_KEYS:
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "sk-tokenhub-test-12345678")
|
||||
provider = resolve_provider("auto")
|
||||
assert provider == "tencent-tokenhub"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Credentials
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubCredentials:
|
||||
"""Test credential resolution for the tencent-tokenhub provider."""
|
||||
|
||||
def test_status_configured(self, monkeypatch):
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "sk-test-12345678")
|
||||
status = get_api_key_provider_status("tencent-tokenhub")
|
||||
assert status["configured"]
|
||||
|
||||
def test_status_not_configured(self, monkeypatch):
|
||||
monkeypatch.delenv("TOKENHUB_API_KEY", raising=False)
|
||||
status = get_api_key_provider_status("tencent-tokenhub")
|
||||
assert not status["configured"]
|
||||
|
||||
def test_resolve_credentials(self, monkeypatch):
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "sk-test-12345678")
|
||||
monkeypatch.delenv("TOKENHUB_BASE_URL", raising=False)
|
||||
creds = resolve_api_key_provider_credentials("tencent-tokenhub")
|
||||
assert creds["api_key"] == "sk-test-12345678"
|
||||
assert creds["base_url"] == "https://tokenhub.tencentmaas.com/v1"
|
||||
|
||||
def test_openrouter_key_does_not_make_tokenhub_configured(self, monkeypatch):
|
||||
"""OpenRouter users should NOT see tencent-tokenhub as configured."""
|
||||
monkeypatch.delenv("TOKENHUB_API_KEY", raising=False)
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
||||
status = get_api_key_provider_status("tencent-tokenhub")
|
||||
assert not status["configured"]
|
||||
|
||||
def test_custom_base_url_override(self, monkeypatch):
|
||||
monkeypatch.setenv("TOKENHUB_API_KEY", "sk-test-12345678")
|
||||
monkeypatch.setenv("TOKENHUB_BASE_URL", "https://custom.tokenhub.example/v1")
|
||||
creds = resolve_api_key_provider_credentials("tencent-tokenhub")
|
||||
assert creds["base_url"] == "https://custom.tokenhub.example/v1"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Model catalog
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubModelCatalog:
|
||||
"""Tencent TokenHub static model list."""
|
||||
|
||||
def test_static_model_list_exists(self):
|
||||
from hermes_cli.models import _PROVIDER_MODELS
|
||||
assert "tencent-tokenhub" in _PROVIDER_MODELS
|
||||
assert len(_PROVIDER_MODELS["tencent-tokenhub"]) >= 1
|
||||
|
||||
def test_hy3_preview_in_model_list(self):
|
||||
from hermes_cli.models import _PROVIDER_MODELS
|
||||
assert "hy3-preview" in _PROVIDER_MODELS["tencent-tokenhub"]
|
||||
|
||||
def test_default_model(self):
|
||||
from hermes_cli.models import get_default_model_for_provider
|
||||
assert get_default_model_for_provider("tencent-tokenhub") == "hy3-preview"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CANONICAL_PROVIDERS (hermes model picker)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubCanonicalProvider:
|
||||
"""Tencent TokenHub appears in the interactive model picker."""
|
||||
|
||||
def test_in_canonical_providers(self):
|
||||
from hermes_cli.models import CANONICAL_PROVIDERS
|
||||
slugs = [p.slug for p in CANONICAL_PROVIDERS]
|
||||
assert "tencent-tokenhub" in slugs
|
||||
|
||||
def test_label(self):
|
||||
from hermes_cli.models import CANONICAL_PROVIDERS
|
||||
entry = next(p for p in CANONICAL_PROVIDERS if p.slug == "tencent-tokenhub")
|
||||
assert entry.label == "Tencent TokenHub"
|
||||
|
||||
def test_description_contains_hy3(self):
|
||||
from hermes_cli.models import CANONICAL_PROVIDERS
|
||||
entry = next(p for p in CANONICAL_PROVIDERS if p.slug == "tencent-tokenhub")
|
||||
assert "Hy3 Preview" in entry.tui_desc
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OpenRouter / Nous Portal curated lists
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentInOpenRouterAndNous:
|
||||
"""tencent/hy3-preview:free should appear in OpenRouter and Nous curated lists."""
|
||||
|
||||
def test_in_openrouter_fallback(self):
|
||||
from hermes_cli.models import OPENROUTER_MODELS
|
||||
ids = [mid for mid, _ in OPENROUTER_MODELS]
|
||||
assert "tencent/hy3-preview:free" in ids
|
||||
|
||||
def test_in_nous_provider_models(self):
|
||||
from hermes_cli.models import _PROVIDER_MODELS
|
||||
assert "tencent/hy3-preview" in _PROVIDER_MODELS["nous"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Model normalization
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubNormalization:
|
||||
"""Model name normalization — Tencent TokenHub is a direct provider
|
||||
not in _MATCHING_PREFIX_STRIP_PROVIDERS, so names pass through as-is.
|
||||
"""
|
||||
|
||||
def test_bare_name_passthrough(self):
|
||||
"""hy3-preview should remain unchanged when targeting tencent-tokenhub."""
|
||||
from hermes_cli.model_normalize import normalize_model_for_provider
|
||||
result = normalize_model_for_provider("hy3-preview", "tencent-tokenhub")
|
||||
assert result == "hy3-preview"
|
||||
|
||||
def test_vendor_prefixed_passthrough(self):
|
||||
"""tencent/hy3-preview is not stripped since tencent-tokenhub is not in
|
||||
_MATCHING_PREFIX_STRIP_PROVIDERS — the slash survives."""
|
||||
from hermes_cli.model_normalize import normalize_model_for_provider
|
||||
result = normalize_model_for_provider("tencent/hy3-preview", "tencent-tokenhub")
|
||||
# Direct providers not in any special set → passthrough
|
||||
assert result == "tencent/hy3-preview"
|
||||
|
||||
def test_not_in_matching_prefix_strip_set(self):
|
||||
"""tencent-tokenhub does NOT need prefix stripping — it only has
|
||||
one model (hy3-preview) and users won't copy vendor/ form."""
|
||||
from hermes_cli.model_normalize import _MATCHING_PREFIX_STRIP_PROVIDERS
|
||||
assert "tencent-tokenhub" not in _MATCHING_PREFIX_STRIP_PROVIDERS
|
||||
|
||||
def test_not_in_lowercase_providers(self):
|
||||
"""tencent-tokenhub does not require lowercase normalization."""
|
||||
from hermes_cli.model_normalize import _LOWERCASE_MODEL_PROVIDERS
|
||||
assert "tencent-tokenhub" not in _LOWERCASE_MODEL_PROVIDERS
|
||||
|
||||
@pytest.mark.parametrize("empty_input", ["", None, " "])
|
||||
def test_normalize_empty_and_none(self, empty_input):
|
||||
"""None, empty, and whitespace-only inputs return empty string."""
|
||||
from hermes_cli.model_normalize import normalize_model_for_provider
|
||||
result = normalize_model_for_provider(empty_input, "tencent-tokenhub")
|
||||
assert result == "" or result.strip() == ""
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Provider label
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubProviderLabel:
|
||||
"""Test provider_label() from models.py for tencent-tokenhub."""
|
||||
|
||||
def test_label_from_provider_labels_dict(self):
|
||||
from hermes_cli.models import _PROVIDER_LABELS
|
||||
assert _PROVIDER_LABELS["tencent-tokenhub"] == "Tencent TokenHub"
|
||||
|
||||
def test_provider_label_function(self):
|
||||
from hermes_cli.models import provider_label
|
||||
assert provider_label("tencent-tokenhub") == "Tencent TokenHub"
|
||||
|
||||
def test_provider_label_via_alias(self):
|
||||
from hermes_cli.models import provider_label
|
||||
assert provider_label("tencent") == "Tencent TokenHub"
|
||||
assert provider_label("tokenhub") == "Tencent TokenHub"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# URL mapping
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubURLMapping:
|
||||
"""Test URL → provider inference for Tencent TokenHub endpoints."""
|
||||
|
||||
def test_url_to_provider(self):
|
||||
from agent.model_metadata import _URL_TO_PROVIDER
|
||||
assert _URL_TO_PROVIDER.get("tokenhub.tencentmaas.com") == "tencent-tokenhub"
|
||||
|
||||
def test_provider_prefixes(self):
|
||||
from agent.model_metadata import _PROVIDER_PREFIXES
|
||||
assert "tencent-tokenhub" in _PROVIDER_PREFIXES
|
||||
assert "tencent" in _PROVIDER_PREFIXES
|
||||
assert "tokenhub" in _PROVIDER_PREFIXES
|
||||
|
||||
def test_infer_from_url(self):
|
||||
from agent.model_metadata import _infer_provider_from_url
|
||||
assert _infer_provider_from_url("https://tokenhub.tencentmaas.com/v1") == "tencent-tokenhub"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context length
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubContextLength:
|
||||
"""hy3-preview context length is registered."""
|
||||
|
||||
def test_hy3_preview_context_length(self):
|
||||
from agent.model_metadata import get_model_context_length
|
||||
ctx = get_model_context_length("hy3-preview")
|
||||
assert ctx == 256000
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# providers.py (unified provider module)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubProvidersModule:
|
||||
"""Test Tencent TokenHub in the unified providers module."""
|
||||
|
||||
def test_overlay_exists(self):
|
||||
from hermes_cli.providers import HERMES_OVERLAYS
|
||||
assert "tencent-tokenhub" in HERMES_OVERLAYS
|
||||
overlay = HERMES_OVERLAYS["tencent-tokenhub"]
|
||||
assert overlay.transport == "openai_chat"
|
||||
assert overlay.base_url_env_var == "TOKENHUB_BASE_URL"
|
||||
assert not overlay.is_aggregator
|
||||
|
||||
def test_alias_resolves(self):
|
||||
from hermes_cli.providers import normalize_provider
|
||||
assert normalize_provider("tencent") == "tencent-tokenhub"
|
||||
assert normalize_provider("tokenhub") == "tencent-tokenhub"
|
||||
|
||||
def test_label(self):
|
||||
from hermes_cli.providers import get_label
|
||||
assert get_label("tencent-tokenhub") == "Tencent TokenHub"
|
||||
|
||||
def test_get_provider(self):
|
||||
pdef = None
|
||||
try:
|
||||
from hermes_cli.providers import get_provider
|
||||
pdef = get_provider("tencent-tokenhub")
|
||||
except Exception:
|
||||
pass
|
||||
if pdef is not None:
|
||||
assert pdef.id == "tencent-tokenhub"
|
||||
assert pdef.transport == "openai_chat"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Auxiliary client
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubAuxiliary:
|
||||
"""Tencent TokenHub auxiliary model routing."""
|
||||
|
||||
def test_aux_model_registered(self):
|
||||
from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS
|
||||
assert "tencent-tokenhub" in _API_KEY_PROVIDER_AUX_MODELS
|
||||
assert _API_KEY_PROVIDER_AUX_MODELS["tencent-tokenhub"] == "hy3-preview"
|
||||
|
||||
def test_aux_aliases(self):
|
||||
from agent.auxiliary_client import _PROVIDER_ALIASES
|
||||
assert _PROVIDER_ALIASES.get("tencent") == "tencent-tokenhub"
|
||||
assert _PROVIDER_ALIASES.get("tokenhub") == "tencent-tokenhub"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Doctor
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubDoctor:
|
||||
"""Verify hermes doctor recognizes Tencent TokenHub env vars."""
|
||||
|
||||
def test_provider_env_hints(self):
|
||||
from hermes_cli.doctor import _PROVIDER_ENV_HINTS
|
||||
assert "TOKENHUB_API_KEY" in _PROVIDER_ENV_HINTS
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent init (no SyntaxError, correct api_mode)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubAgentInit:
|
||||
"""Verify the agent can be constructed with tencent-tokenhub provider without errors."""
|
||||
|
||||
def test_no_syntax_errors(self):
|
||||
"""Importing run_agent with tencent-tokenhub should not raise."""
|
||||
import importlib
|
||||
importlib.import_module("run_agent")
|
||||
|
||||
def test_api_mode_is_chat_completions(self):
|
||||
from hermes_cli.providers import HERMES_OVERLAYS, TRANSPORT_TO_API_MODE
|
||||
overlay = HERMES_OVERLAYS["tencent-tokenhub"]
|
||||
api_mode = TRANSPORT_TO_API_MODE[overlay.transport]
|
||||
assert api_mode == "chat_completions"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI model flow dispatch (main.py)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubCLIDispatch:
|
||||
"""Verify tencent-tokenhub is routed through _model_flow_api_key_provider."""
|
||||
|
||||
def test_in_api_key_provider_tuple(self):
|
||||
"""tencent-tokenhub must appear in the elif tuple in _model_flow dispatch
|
||||
so ``hermes model`` routes it through the generic api_key_provider flow.
|
||||
"""
|
||||
import inspect
|
||||
from hermes_cli import main as main_mod
|
||||
source = inspect.getsource(main_mod)
|
||||
# The source should contain tencent-tokenhub in the dispatch block
|
||||
assert '"tencent-tokenhub"' in source or "'tencent-tokenhub'" in source
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Remote model catalog (model-catalog.json)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubModelCatalogJSON:
|
||||
"""Verify tencent/hy3-preview:free is present in the website model-catalog.json."""
|
||||
|
||||
def test_in_model_catalog_json(self):
|
||||
catalog_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..", "..",
|
||||
"website", "static", "api", "model-catalog.json",
|
||||
)
|
||||
if not os.path.isfile(catalog_path):
|
||||
pytest.skip("model-catalog.json not found in workspace")
|
||||
with open(catalog_path) as f:
|
||||
data = json.load(f)
|
||||
# Collect all model IDs across all provider lists.
|
||||
# providers is a dict keyed by provider name, each value has a "models" list.
|
||||
all_ids = set()
|
||||
providers = data.get("providers", {})
|
||||
if isinstance(providers, dict):
|
||||
for provider_entry in providers.values():
|
||||
for model in provider_entry.get("models", []):
|
||||
all_ids.add(model.get("id", ""))
|
||||
else:
|
||||
for provider_entry in providers:
|
||||
for model in provider_entry.get("models", []):
|
||||
all_ids.add(model.get("id", ""))
|
||||
assert "tencent/hy3-preview:free" in all_ids
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# determine_api_mode (providers.py)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubApiMode:
|
||||
"""Verify determine_api_mode routes tencent-tokenhub correctly."""
|
||||
|
||||
def test_determine_api_mode_direct(self):
|
||||
from hermes_cli.providers import determine_api_mode
|
||||
mode = determine_api_mode("tencent-tokenhub")
|
||||
assert mode == "chat_completions"
|
||||
|
||||
def test_determine_api_mode_with_base_url(self):
|
||||
from hermes_cli.providers import determine_api_mode
|
||||
mode = determine_api_mode("tencent-tokenhub", "https://tokenhub.tencentmaas.com/v1")
|
||||
assert mode == "chat_completions"
|
||||
|
||||
def test_determine_api_mode_via_alias(self):
|
||||
from hermes_cli.providers import determine_api_mode
|
||||
mode = determine_api_mode("tencent")
|
||||
assert mode == "chat_completions"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# _KNOWN_PROVIDER_NAMES (models.py)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestTencentTokenhubKnownProviderNames:
|
||||
"""Verify tencent-tokenhub and its aliases are recognized as valid
|
||||
provider names for the ``provider:model`` syntax.
|
||||
"""
|
||||
|
||||
def test_canonical_id_known(self):
|
||||
from hermes_cli.models import _KNOWN_PROVIDER_NAMES
|
||||
assert "tencent-tokenhub" in _KNOWN_PROVIDER_NAMES
|
||||
|
||||
@pytest.mark.parametrize("alias", [
|
||||
"tencent", "tokenhub", "tencent-cloud", "tencentmaas",
|
||||
])
|
||||
def test_alias_known(self, alias):
|
||||
from hermes_cli.models import _KNOWN_PROVIDER_NAMES
|
||||
assert alias in _KNOWN_PROVIDER_NAMES
|
||||
|
||||
@ -84,7 +84,8 @@ class TestXiaomiAutoDetection:
|
||||
"DASHSCOPE_API_KEY", "XAI_API_KEY", "KIMI_API_KEY",
|
||||
"MINIMAX_API_KEY", "AI_GATEWAY_API_KEY", "KILOCODE_API_KEY",
|
||||
"HF_TOKEN", "GLM_API_KEY", "COPILOT_GITHUB_TOKEN",
|
||||
"GH_TOKEN", "GITHUB_TOKEN", "MINIMAX_CN_API_KEY"):
|
||||
"GH_TOKEN", "GITHUB_TOKEN", "MINIMAX_CN_API_KEY",
|
||||
"TOKENHUB_API_KEY", "ARCEEAI_API_KEY"):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
monkeypatch.setenv("XIAOMI_API_KEY", "sk-xiaomi-test-12345678")
|
||||
provider = resolve_provider("auto")
|
||||
|
||||
@ -31,6 +31,7 @@ You need at least one way to connect to an LLM. Use `hermes model` to switch pro
|
||||
| **Alibaba Cloud** | `DASHSCOPE_API_KEY` in `~/.hermes/.env` (provider: `alibaba`, aliases: `dashscope`, `qwen`) |
|
||||
| **Kilo Code** | `KILOCODE_API_KEY` in `~/.hermes/.env` (provider: `kilocode`) |
|
||||
| **Xiaomi MiMo** | `XIAOMI_API_KEY` in `~/.hermes/.env` (provider: `xiaomi`, aliases: `mimo`, `xiaomi-mimo`) |
|
||||
| **Tencent TokenHub** | `TOKENHUB_API_KEY` in `~/.hermes/.env` (provider: `tencent-tokenhub`, aliases: `tencent`, `tokenhub`, `tencentmaas`) |
|
||||
| **OpenCode Zen** | `OPENCODE_ZEN_API_KEY` in `~/.hermes/.env` (provider: `opencode-zen`) |
|
||||
| **OpenCode Go** | `OPENCODE_GO_API_KEY` in `~/.hermes/.env` (provider: `opencode-go`) |
|
||||
| **DeepSeek** | `DEEPSEEK_API_KEY` in `~/.hermes/.env` (provider: `deepseek`) |
|
||||
@ -284,6 +285,10 @@ hermes chat --provider alibaba --model qwen3.5-plus
|
||||
hermes chat --provider xiaomi --model mimo-v2-pro
|
||||
# Requires: XIAOMI_API_KEY in ~/.hermes/.env
|
||||
|
||||
# Tencent TokenHub (Hy3 Preview)
|
||||
hermes chat --provider tencent-tokenhub --model hy3-preview
|
||||
# Requires: TOKENHUB_API_KEY in ~/.hermes/.env
|
||||
|
||||
# Arcee AI (Trinity models)
|
||||
hermes chat --provider arcee --model trinity-large-thinking
|
||||
# Requires: ARCEEAI_API_KEY in ~/.hermes/.env
|
||||
@ -301,7 +306,7 @@ model:
|
||||
default: "zai-org/GLM-5.1-FP8"
|
||||
```
|
||||
|
||||
Base URLs can be overridden with `GLM_BASE_URL`, `KIMI_BASE_URL`, `MINIMAX_BASE_URL`, `MINIMAX_CN_BASE_URL`, `DASHSCOPE_BASE_URL`, `XIAOMI_BASE_URL`, or `GMI_BASE_URL` environment variables.
|
||||
Base URLs can be overridden with `GLM_BASE_URL`, `KIMI_BASE_URL`, `MINIMAX_BASE_URL`, `MINIMAX_CN_BASE_URL`, `DASHSCOPE_BASE_URL`, `XIAOMI_BASE_URL`, `GMI_BASE_URL`, or `TOKENHUB_BASE_URL` environment variables.
|
||||
|
||||
:::note Z.AI Endpoint Auto-Detection
|
||||
When using the Z.AI / GLM provider, Hermes automatically probes multiple endpoints (global, China, coding variants) to find one that accepts your API key. You don't need to set `GLM_BASE_URL` manually — the working endpoint is detected and cached automatically.
|
||||
@ -1103,7 +1108,7 @@ You can also select named custom providers from the interactive `hermes model` m
|
||||
| **Cost optimization** | ClawRouter or OpenRouter with `sort: "price"` |
|
||||
| **Maximum privacy** | Ollama, vLLM, or llama.cpp (fully local) |
|
||||
| **Enterprise / Azure** | Azure OpenAI with custom endpoint |
|
||||
| **Chinese AI models** | z.ai (GLM), Kimi/Moonshot (`kimi-coding` or `kimi-coding-cn`), MiniMax, or Xiaomi MiMo (first-class providers) |
|
||||
| **Chinese AI models** | z.ai (GLM), Kimi/Moonshot (`kimi-coding` or `kimi-coding-cn`), MiniMax, Xiaomi MiMo, or Tencent TokenHub (first-class providers) |
|
||||
|
||||
:::tip
|
||||
You can switch between providers at any time with `hermes model` — no restart required. Your conversation history, memory, and skills carry over regardless of which provider you use.
|
||||
@ -1178,7 +1183,7 @@ fallback_model:
|
||||
|
||||
When activated, the fallback swaps the model and provider mid-session without losing your conversation. It fires **at most once** per session.
|
||||
|
||||
Supported providers: `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `deepseek`, `nvidia`, `xai`, `ollama-cloud`, `bedrock`, `ai-gateway`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `custom`.
|
||||
Supported providers: `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `deepseek`, `nvidia`, `xai`, `ollama-cloud`, `bedrock`, `ai-gateway`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `tencent-tokenhub`, `custom`.
|
||||
|
||||
:::tip
|
||||
Fallback is configured exclusively through `config.yaml` — there are no environment variables for it. For full details on when it triggers, supported providers, and how it interacts with auxiliary tasks and delegation, see [Fallback Providers](/docs/user-guide/features/fallback-providers).
|
||||
|
||||
@ -46,6 +46,8 @@ All variables go in `~/.hermes/.env`. You can also set them with `hermes config
|
||||
| `KILOCODE_BASE_URL` | Override Kilo Code base URL (default: `https://api.kilo.ai/api/gateway`) |
|
||||
| `XIAOMI_API_KEY` | Xiaomi MiMo API key ([platform.xiaomimimo.com](https://platform.xiaomimimo.com)) |
|
||||
| `XIAOMI_BASE_URL` | Override Xiaomi MiMo base URL (default: `https://api.xiaomimimo.com/v1`) |
|
||||
| `TOKENHUB_API_KEY` | Tencent TokenHub API key ([tokenhub.tencentmaas.com](https://tokenhub.tencentmaas.com)) |
|
||||
| `TOKENHUB_BASE_URL` | Override Tencent TokenHub base URL (default: `https://tokenhub.tencentmaas.com/v1`) |
|
||||
| `AZURE_FOUNDRY_API_KEY` | Azure AI Foundry / Azure OpenAI API key ([ai.azure.com](https://ai.azure.com/)) |
|
||||
| `AZURE_FOUNDRY_BASE_URL` | Azure AI Foundry endpoint URL (e.g. `https://<resource>.openai.azure.com/openai/v1` for OpenAI-style, or `https://<resource>.services.ai.azure.com/anthropic` for Anthropic-style) |
|
||||
| `AZURE_ANTHROPIC_KEY` | Azure Anthropic API key for `provider: anthropic` + `base_url` pointing at an Azure Foundry Claude deployment (alternative to `ANTHROPIC_API_KEY` when both Anthropic and Azure Anthropic are configured) |
|
||||
|
||||
@ -60,6 +60,10 @@
|
||||
"id": "xiaomi/mimo-v2.5",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "tencent/hy3-preview:free",
|
||||
"description": "free"
|
||||
},
|
||||
{
|
||||
"id": "openai/gpt-5.3-codex",
|
||||
"description": ""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user