diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index e69d12913d883..984080f1ecb43 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -347,6 +347,17 @@ class ProviderConfig: api_key_env_vars=("ALIBABA_CODING_PLAN_API_KEY", "DASHSCOPE_API_KEY"), base_url_env_var="ALIBABA_CODING_PLAN_BASE_URL", ), + "alibaba-token-plan": ProviderConfig( + id="alibaba-token-plan", + name="Alibaba Cloud (Token Plan)", + auth_type="api_key", + # Alibaba's Hermes Agent setup guide recommends the Anthropic-compatible + # endpoint for Token Plan. Users can still override this with + # ALIBABA_TOKEN_PLAN_BASE_URL if they need the OpenAI-compatible URL. + inference_base_url="https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic", + api_key_env_vars=("ALIBABA_TOKEN_PLAN_API_KEY",), + base_url_env_var="ALIBABA_TOKEN_PLAN_BASE_URL", + ), "minimax-cn": ProviderConfig( id="minimax-cn", name="MiniMax (China)", @@ -1428,6 +1439,8 @@ def resolve_provider( "minimax-portal": "minimax-oauth", "minimax-global": "minimax-oauth", "minimax_oauth": "minimax-oauth", "alibaba_coding": "alibaba-coding-plan", "alibaba-coding": "alibaba-coding-plan", "alibaba_coding_plan": "alibaba-coding-plan", + "alibaba_token": "alibaba-token-plan", "alibaba-token": "alibaba-token-plan", + "alibaba_token_plan": "alibaba-token-plan", "claude": "anthropic", "claude-code": "anthropic", "github": "copilot", "github-copilot": "copilot", "github-models": "copilot", "github-model": "copilot", diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 9444c90e762a4..caf5af2e5660b 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -415,6 +415,22 @@ def _xai_curated_models() -> list[str]: "glm-4.7", "MiniMax-M2.5", ], + # Alibaba Token Plan (Team Edition) — broader model selection including + # DeepSeek, latest Qwen, and third-party providers. Singapore region only. + # https://modelstudio.console.alibabacloud.com/ap-southeast-1/?tab=doc#/doc/?type=model&url=3028858 + "alibaba-token-plan": [ + "qwen3.7-max", + "qwen3.6-plus", + "qwen3.6-flash", + "deepseek-v4-pro", + "deepseek-v4-flash", + "deepseek-v3.2", + "kimi-k2.6", + "kimi-k2.5", + "glm-5.1", + "glm-5", + "MiniMax-M2.5", + ], # Curated HF model list — only agentic models that map to OpenRouter defaults. "huggingface": [ "moonshotai/Kimi-K2.5", @@ -914,6 +930,7 @@ class ProviderEntry(NamedTuple): ProviderEntry("openai-codex", "OpenAI Codex", "OpenAI Codex"), ProviderEntry("openai-api", "OpenAI API", "OpenAI API (api.openai.com, API key)"), ProviderEntry("alibaba", "Qwen Cloud", "Qwen Cloud / DashScope Coding (Qwen + multi-provider)"), + ProviderEntry("alibaba-token-plan", "Alibaba Token Plan", "Alibaba Cloud Token Plan (Team Edition — Singapore, broad model catalog)"), ProviderEntry("xai-oauth", "xAI Grok OAuth (SuperGrok / Premium+)", "xAI Grok OAuth (SuperGrok / Premium+)"), 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)"), @@ -1010,6 +1027,9 @@ class ProviderEntry(NamedTuple): "aliyun": "alibaba", "qwen": "alibaba", "alibaba-cloud": "alibaba", + "alibaba_token": "alibaba-token-plan", + "alibaba-token": "alibaba-token-plan", + "alibaba_token_plan": "alibaba-token-plan", "qwen-portal": "qwen-oauth", "gemini-cli": "google-gemini-cli", "gemini-oauth": "google-gemini-cli", diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index a19a4584f98cc..69faf30970d90 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -143,6 +143,16 @@ class HermesOverlay: transport="openai_chat", base_url_env_var="ALIBABA_CODING_PLAN_BASE_URL", ), + "alibaba-token-plan": HermesOverlay( + transport="anthropic_messages", + extra_env_vars=("ALIBABA_TOKEN_PLAN_API_KEY",), + base_url_override="https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic", + base_url_env_var="ALIBABA_TOKEN_PLAN_BASE_URL", + ), + "vercel": HermesOverlay( + transport="openai_chat", + is_aggregator=True, + ), "opencode": HermesOverlay( transport="openai_chat", is_aggregator=True, @@ -310,6 +320,9 @@ class ProviderDef: "alibaba_coding": "alibaba-coding-plan", "alibaba-coding": "alibaba-coding-plan", "alibaba_coding_plan": "alibaba-coding-plan", + "alibaba_token": "alibaba-token-plan", + "alibaba-token": "alibaba-token-plan", + "alibaba_token_plan": "alibaba-token-plan", # google-gemini-cli (OAuth + Code Assist) "gemini-cli": "google-gemini-cli", @@ -376,6 +389,7 @@ class ProviderDef: "lmstudio": "LM Studio", "local": "Local endpoint", "bedrock": "AWS Bedrock", + "alibaba-token-plan": "Alibaba Token Plan", "ollama-cloud": "Ollama Cloud", "xai-oauth": "xAI Grok OAuth (SuperGrok / Premium+)", } diff --git a/tests/hermes_cli/test_alibaba_token_plan_provider.py b/tests/hermes_cli/test_alibaba_token_plan_provider.py new file mode 100644 index 0000000000000..34371d6f4a5e1 --- /dev/null +++ b/tests/hermes_cli/test_alibaba_token_plan_provider.py @@ -0,0 +1,45 @@ +"""Regression tests for Alibaba Token Plan provider wiring.""" + +from hermes_cli.auth import PROVIDER_REGISTRY, resolve_provider +from hermes_cli.providers import HERMES_OVERLAYS, TRANSPORT_TO_API_MODE, get_label, get_provider +from hermes_cli.runtime_provider import _detect_api_mode_for_url + + +TOKEN_PLAN_ANTHROPIC_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic" + + +def test_auth_registry_uses_token_plan_anthropic_endpoint(): + cfg = PROVIDER_REGISTRY["alibaba-token-plan"] + + assert cfg.inference_base_url == TOKEN_PLAN_ANTHROPIC_URL + assert cfg.api_key_env_vars == ("ALIBABA_TOKEN_PLAN_API_KEY",) + assert cfg.base_url_env_var == "ALIBABA_TOKEN_PLAN_BASE_URL" + + +def test_provider_overlay_uses_anthropic_messages_transport(): + overlay = HERMES_OVERLAYS["alibaba-token-plan"] + + assert overlay.transport == "anthropic_messages" + assert TRANSPORT_TO_API_MODE[overlay.transport] == "anthropic_messages" + assert overlay.base_url_override == TOKEN_PLAN_ANTHROPIC_URL + assert overlay.extra_env_vars == ("ALIBABA_TOKEN_PLAN_API_KEY",) + + +def test_provider_definition_exposes_base_url_key_and_label(): + provider = get_provider("alibaba_token_plan") + + assert provider is not None + assert provider.id == "alibaba-token-plan" + assert provider.base_url == TOKEN_PLAN_ANTHROPIC_URL + assert provider.api_key_env_vars == ("ALIBABA_TOKEN_PLAN_API_KEY",) + assert get_label("alibaba_token") == "Alibaba Token Plan" + + +def test_token_plan_aliases_resolve_to_canonical_provider(): + assert resolve_provider("alibaba_token") == "alibaba-token-plan" + assert resolve_provider("alibaba-token") == "alibaba-token-plan" + assert resolve_provider("alibaba_token_plan") == "alibaba-token-plan" + + +def test_token_plan_anthropic_url_auto_detects_api_mode(): + assert _detect_api_mode_for_url(TOKEN_PLAN_ANTHROPIC_URL) == "anthropic_messages" diff --git a/tests/hermes_cli/test_model_validation.py b/tests/hermes_cli/test_model_validation.py index 91fc4e50d009b..8bda64331113e 100644 --- a/tests/hermes_cli/test_model_validation.py +++ b/tests/hermes_cli/test_model_validation.py @@ -70,6 +70,12 @@ def test_stepfun_alias_resolved(self): assert provider == "stepfun" assert model == "step-3.5-flash" + def test_alibaba_token_plan_alias_resolved(self): + provider, model = parse_model_input("alibaba-token:qwen3.7-max", "openrouter") + assert provider == "alibaba-token-plan" + assert model == "qwen3.7-max" + assert provider_label(provider) == "Alibaba Token Plan" + def test_no_slash_no_colon_keeps_provider(self): provider, model = parse_model_input("gpt-5.4", "openrouter") assert provider == "openrouter" @@ -606,6 +612,12 @@ def test_zai_known_model_accepted_via_catalog_when_api_down(self): assert result["persist"] is True assert result["recognized"] is True + def test_alibaba_token_plan_known_models_accepted_via_catalog_when_api_down(self): + result = _validate("deepseek-v4-pro", provider="alibaba-token-plan", api_models=None) + assert result["accepted"] is True + assert result["persist"] is True + assert result["recognized"] is True + def test_unknown_provider_soft_accepted_when_api_down(self): # No catalog for unknown providers — soft-accept with a Note. with patch("hermes_cli.models.provider_model_ids", return_value=[]):