diff --git a/README.md b/README.md
index 622910b3a..e7f3ca301 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
**The self-improving AI agent built by [Nous Research](https://nousresearch.com).** It's the only agent with a built-in learning loop — it creates skills from experience, improves them during use, nudges itself to persist knowledge, searches its own past conversations, and builds a deepening model of who you are across sessions. Run it on a $5 VPS, a GPU cluster, or serverless infrastructure that costs nearly nothing when idle. It's not tied to your laptop — talk to it from Telegram while it works on a cloud VM.
-Use any model you want — [Nous Portal](https://portal.nousresearch.com), [OpenRouter](https://openrouter.ai) (200+ models), [NVIDIA NIM](https://build.nvidia.com) (Nemotron), [Xiaomi MiMo](https://platform.xiaomimimo.com), [z.ai/GLM](https://z.ai), [Kimi/Moonshot](https://platform.moonshot.ai), [MiniMax](https://www.minimax.io), [Hugging Face](https://huggingface.co), OpenAI, or your own endpoint. Switch with `hermes model` — no code changes, no lock-in.
+Use any model you want — [Nous Portal](https://portal.nousresearch.com), [OpenRouter](https://openrouter.ai) (200+ models), [Volcengine](https://www.volcengine.com/product/ark), [BytePlus](https://www.byteplus.com/en/product/modelark), [NVIDIA NIM](https://build.nvidia.com) (Nemotron), [Xiaomi MiMo](https://platform.xiaomimimo.com), [z.ai/GLM](https://z.ai), [Kimi/Moonshot](https://platform.moonshot.ai), [MiniMax](https://www.minimax.io), [Hugging Face](https://huggingface.co), OpenAI, or your own endpoint. Switch with `hermes model` — no code changes, no lock-in.
| A real terminal interface | Full TUI with multiline editing, slash-command autocomplete, conversation history, interrupt-and-redirect, and streaming tool output. |
diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py
index 4f8c9a0a4..32bed28e0 100644
--- a/agent/auxiliary_client.py
+++ b/agent/auxiliary_client.py
@@ -47,6 +47,7 @@ from openai import OpenAI
from agent.credential_pool import load_pool
from hermes_cli.config import get_hermes_home
+from hermes_cli.provider_contracts import PROVIDER_AUX_MODELS
from hermes_constants import OPENROUTER_BASE_URL
from utils import base_url_host_matches, base_url_hostname, normalize_proxy_env_vars
@@ -74,6 +75,10 @@ _PROVIDER_ALIASES = {
"minimax_cn": "minimax-cn",
"claude": "anthropic",
"claude-code": "anthropic",
+ "volcengine-coding-plan": "volcengine",
+ "volcengine_coding_plan": "volcengine",
+ "byteplus-coding-plan": "byteplus",
+ "byteplus_coding_plan": "byteplus",
}
@@ -144,6 +149,8 @@ _API_KEY_PROVIDER_AUX_MODELS: Dict[str, str] = {
"opencode-go": "glm-5",
"kilocode": "google/gemini-3-flash-preview",
"ollama-cloud": "nemotron-3-nano:30b",
+ "volcengine": PROVIDER_AUX_MODELS["volcengine"],
+ "byteplus": PROVIDER_AUX_MODELS["byteplus"],
}
# Vision-specific model overrides for direct providers.
diff --git a/agent/model_metadata.py b/agent/model_metadata.py
index 152e536fd..7dd9fd4e3 100644
--- a/agent/model_metadata.py
+++ b/agent/model_metadata.py
@@ -14,8 +14,8 @@ from urllib.parse import urlparse
import requests
import yaml
+from hermes_cli.provider_contracts import model_context_window
from utils import base_url_host_matches, base_url_hostname
-
from hermes_constants import OPENROUTER_MODELS_URL
logger = logging.getLogger(__name__)
@@ -30,6 +30,10 @@ _PROVIDER_PREFIXES: frozenset[str] = frozenset({
"qwen-oauth",
"xiaomi",
"arcee",
+ "volcengine",
+ "volcengine-coding-plan",
+ "byteplus",
+ "byteplus-coding-plan",
"custom", "local",
# Common aliases
"google", "google-gemini", "google-ai-studio",
@@ -257,6 +261,8 @@ _URL_TO_PROVIDER: Dict[str, str] = {
"api.xiaomimimo.com": "xiaomi",
"xiaomimimo.com": "xiaomi",
"ollama.com": "ollama-cloud",
+ "ark.cn-beijing.volces.com": "volcengine",
+ "ark.ap-southeast.bytepluses.com": "byteplus",
}
@@ -1119,12 +1125,20 @@ def get_model_context_length(
ctx = _resolve_nous_context_length(model)
if ctx:
return ctx
+ if effective_provider in {"volcengine", "byteplus"}:
+ ctx = model_context_window(model)
+ if ctx:
+ return ctx
if effective_provider:
from agent.models_dev import lookup_models_dev_context
ctx = lookup_models_dev_context(effective_provider, model)
if ctx:
return ctx
+ ctx = model_context_window(model)
+ if ctx:
+ return ctx
+
# 6. OpenRouter live API metadata (provider-unaware fallback)
metadata = fetch_model_metadata()
if model in metadata:
diff --git a/gateway/run.py b/gateway/run.py
index 617a38418..84795a337 100644
--- a/gateway/run.py
+++ b/gateway/run.py
@@ -5690,6 +5690,7 @@ class GatewayRunner:
from hermes_cli.models import (
list_available_providers,
normalize_provider,
+ provider_for_base_url,
_PROVIDER_LABELS,
)
@@ -5718,7 +5719,10 @@ class GatewayRunner:
# Detect custom endpoint from config base_url
if current_provider == "openrouter":
_cfg_base = model_cfg.get("base_url", "") if isinstance(model_cfg, dict) else ""
- if _cfg_base and "openrouter.ai" not in _cfg_base:
+ inferred_provider = provider_for_base_url(_cfg_base)
+ if inferred_provider:
+ current_provider = inferred_provider
+ elif _cfg_base and "openrouter.ai" not in _cfg_base:
current_provider = "custom"
current_label = _PROVIDER_LABELS.get(current_provider, current_provider)
diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py
index 3fab36a2c..c90dd62ec 100644
--- a/hermes_cli/auth.py
+++ b/hermes_cli/auth.py
@@ -39,6 +39,13 @@ import httpx
import yaml
from hermes_cli.config import get_hermes_home, get_config_path, read_raw_config
+from hermes_cli.provider_contracts import (
+ VOLCENGINE_PROVIDER,
+ BYTEPLUS_PROVIDER,
+ VOLCENGINE_STANDARD_BASE_URL,
+ BYTEPLUS_STANDARD_BASE_URL,
+ base_url_for_provider_model,
+)
from hermes_constants import OPENROUTER_BASE_URL
logger = logging.getLogger(__name__)
@@ -307,6 +314,20 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
api_key_env_vars=("XIAOMI_API_KEY",),
base_url_env_var="XIAOMI_BASE_URL",
),
+ "volcengine": ProviderConfig(
+ id=VOLCENGINE_PROVIDER,
+ name="Volcengine",
+ auth_type="api_key",
+ inference_base_url=VOLCENGINE_STANDARD_BASE_URL,
+ api_key_env_vars=("VOLCENGINE_API_KEY",),
+ ),
+ "byteplus": ProviderConfig(
+ id=BYTEPLUS_PROVIDER,
+ name="BytePlus",
+ auth_type="api_key",
+ inference_base_url=BYTEPLUS_STANDARD_BASE_URL,
+ api_key_env_vars=("BYTEPLUS_API_KEY",),
+ ),
"ollama-cloud": ProviderConfig(
id="ollama-cloud",
name="Ollama Cloud",
@@ -1015,6 +1036,10 @@ def resolve_provider(
"hf": "huggingface", "hugging-face": "huggingface", "huggingface-hub": "huggingface",
"mimo": "xiaomi", "xiaomi-mimo": "xiaomi",
"aws": "bedrock", "aws-bedrock": "bedrock", "amazon-bedrock": "bedrock", "amazon": "bedrock",
+ "volcengine-coding-plan": "volcengine",
+ "volcengine_coding_plan": "volcengine",
+ "byteplus-coding-plan": "byteplus",
+ "byteplus_coding_plan": "byteplus",
"go": "opencode-go", "opencode-go-sub": "opencode-go",
"kilo": "kilocode", "kilo-code": "kilocode", "kilo-gateway": "kilocode",
# Local server aliases — route through the generic custom provider
@@ -1157,6 +1182,21 @@ def _qwen_cli_auth_path() -> Path:
return Path.home() / ".qwen" / "oauth_creds.json"
+def _current_model_for_provider(provider_id: str) -> str:
+ """Return the currently configured model when it belongs to the provider."""
+ try:
+ config = read_raw_config()
+ except Exception:
+ return ""
+
+ model_cfg = config.get("model")
+ if isinstance(model_cfg, dict):
+ configured_provider = str(model_cfg.get("provider") or "").strip().lower()
+ if configured_provider == provider_id:
+ return str(model_cfg.get("default") or model_cfg.get("model") or "").strip()
+ return ""
+
+
def _read_qwen_cli_tokens() -> Dict[str, Any]:
auth_path = _qwen_cli_auth_path()
if not auth_path.exists():
@@ -2555,7 +2595,11 @@ def get_api_key_provider_status(provider_id: str) -> Dict[str, Any]:
if pconfig.base_url_env_var:
env_url = os.getenv(pconfig.base_url_env_var, "").strip()
- if provider_id in ("kimi-coding", "kimi-coding-cn"):
+ active_model = _current_model_for_provider(provider_id)
+
+ if provider_id in {VOLCENGINE_PROVIDER, BYTEPLUS_PROVIDER}:
+ base_url = base_url_for_provider_model(provider_id, active_model) or pconfig.inference_base_url
+ elif provider_id in ("kimi-coding", "kimi-coding-cn"):
base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url)
elif env_url:
base_url = env_url
@@ -2650,7 +2694,11 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:
if pconfig.base_url_env_var:
env_url = os.getenv(pconfig.base_url_env_var, "").strip()
- if provider_id in ("kimi-coding", "kimi-coding-cn"):
+ active_model = _current_model_for_provider(provider_id)
+
+ if provider_id in {VOLCENGINE_PROVIDER, BYTEPLUS_PROVIDER}:
+ base_url = base_url_for_provider_model(provider_id, active_model) or pconfig.inference_base_url
+ elif provider_id in ("kimi-coding", "kimi-coding-cn"):
base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url)
elif provider_id == "zai":
base_url = _resolve_zai_base_url(api_key, pconfig.inference_base_url, env_url)
diff --git a/hermes_cli/main.py b/hermes_cli/main.py
index 404e59089..f1d4be64d 100644
--- a/hermes_cli/main.py
+++ b/hermes_cli/main.py
@@ -1570,6 +1570,8 @@ def select_provider_and_model(args=None):
_model_flow_stepfun(config, current_model)
elif selected_provider == "bedrock":
_model_flow_bedrock(config, current_model)
+ elif selected_provider in ("volcengine", "byteplus"):
+ _model_flow_contract_provider(config, selected_provider, current_model)
elif selected_provider in (
"gemini",
"deepseek",
@@ -1954,7 +1956,7 @@ def _aux_flow_custom_endpoint(task: str, task_cfg: dict) -> None:
print(f"{display_name}: custom ({short_url})" + (f" · {model}" if model else ""))
-def _prompt_provider_choice(choices, *, default=0):
+def _prompt_provider_choice(choices, *, default=0, title="Select provider:"):
"""Show provider selection menu with curses arrow-key navigation.
Falls back to a numbered list when curses is unavailable (e.g. piped
@@ -1963,8 +1965,7 @@ def _prompt_provider_choice(choices, *, default=0):
"""
try:
from hermes_cli.setup import _curses_prompt_choice
-
- idx = _curses_prompt_choice("Select provider:", choices, default)
+ idx = _curses_prompt_choice(title, choices, default)
if idx >= 0:
print()
return idx
@@ -1972,7 +1973,7 @@ def _prompt_provider_choice(choices, *, default=0):
pass
# Fallback: numbered list
- print("Select provider:")
+ print(title)
for i, c in enumerate(choices, 1):
marker = "→" if i - 1 == default else " "
print(f" {marker} {i}. {c}")
@@ -2944,6 +2945,10 @@ def _model_flow_named_custom(config, provider_info):
# Curated model lists for direct API-key providers — single source in models.py
from hermes_cli.models import _PROVIDER_MODELS
+from hermes_cli.provider_contracts import (
+ base_url_for_provider_model,
+ provider_models,
+)
def _current_reasoning_effort(config) -> str:
@@ -4033,6 +4038,70 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
print("No change.")
+def _model_flow_contract_provider(config, provider_id, current_model=""):
+ """Provider flow for Volcengine / BytePlus contract-backed catalogs."""
+ from hermes_cli.auth import (
+ PROVIDER_REGISTRY,
+ _prompt_model_selection,
+ _save_model_choice,
+ deactivate_provider,
+ )
+ from hermes_cli.config import get_env_value, load_config, save_config, save_env_value
+
+ pconfig = PROVIDER_REGISTRY[provider_id]
+ key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
+ existing_key = ""
+ for env_var in pconfig.api_key_env_vars:
+ existing_key = get_env_value(env_var) or os.getenv(env_var, "")
+ if existing_key:
+ break
+
+ if not existing_key:
+ print(f"No {pconfig.name} API key configured.")
+ if key_env:
+ try:
+ import getpass
+
+ new_key = getpass.getpass(f"{key_env} (or Enter to cancel): ").strip()
+ except (KeyboardInterrupt, EOFError):
+ print()
+ return
+ if not new_key:
+ print("Cancelled.")
+ return
+ save_env_value(key_env, new_key)
+ print("API key saved.")
+ print()
+ else:
+ print(f" {pconfig.name} API key: {existing_key[:8]}... ✓")
+ print()
+
+ model_list = provider_models(provider_id)
+ if not model_list:
+ print(f"No curated model catalog found for {pconfig.name}.")
+ return
+
+ selected = _prompt_model_selection(model_list, current_model=current_model)
+ if not selected:
+ print("No change.")
+ return
+
+ _save_model_choice(selected)
+
+ cfg = load_config()
+ model = cfg.get("model")
+ if not isinstance(model, dict):
+ model = {"default": model} if model else {}
+ cfg["model"] = model
+ model["provider"] = provider_id
+ model["base_url"] = base_url_for_provider_model(provider_id, selected)
+ model.pop("api_mode", None)
+ save_config(cfg)
+ deactivate_provider()
+
+ print(f"Default model set to: {selected} (via {pconfig.name})")
+
+
def _run_anthropic_oauth_flow(save_env_value):
"""Run the Claude OAuth setup-token flow. Returns True if credentials were saved."""
from agent.anthropic_adapter import (
diff --git a/hermes_cli/model_normalize.py b/hermes_cli/model_normalize.py
index 76dace065..836bfbfbe 100644
--- a/hermes_cli/model_normalize.py
+++ b/hermes_cli/model_normalize.py
@@ -97,6 +97,8 @@ _MATCHING_PREFIX_STRIP_PROVIDERS: frozenset[str] = frozenset({
"xiaomi",
"arcee",
"ollama-cloud",
+ "volcengine",
+ "byteplus",
"custom",
})
@@ -423,4 +425,3 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str:
# ---------------------------------------------------------------------------
# Batch / convenience helpers
# ---------------------------------------------------------------------------
-
diff --git a/hermes_cli/models.py b/hermes_cli/models.py
index 4b3493506..528341034 100644
--- a/hermes_cli/models.py
+++ b/hermes_cli/models.py
@@ -22,6 +22,12 @@ from hermes_cli import __version__ as _HERMES_VERSION
# Check (error 1010) don't reject the default ``Python-urllib/*`` signature.
_HERMES_USER_AGENT = f"hermes-cli/{_HERMES_VERSION}"
+from hermes_cli.provider_contracts import (
+ BYTEPLUS_PROVIDER,
+ VOLCENGINE_PROVIDER,
+ provider_models,
+)
+
COPILOT_BASE_URL = "https://api.githubcopilot.com"
COPILOT_MODELS_URL = f"{COPILOT_BASE_URL}/models"
COPILOT_EDITOR_VERSION = "vscode/1.104.1"
@@ -356,6 +362,8 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
"us.meta.llama4-maverick-17b-instruct-v1:0",
"us.meta.llama4-scout-17b-instruct-v1:0",
],
+ VOLCENGINE_PROVIDER: provider_models(VOLCENGINE_PROVIDER),
+ BYTEPLUS_PROVIDER: provider_models(BYTEPLUS_PROVIDER),
}
# Vercel AI Gateway: derive the bare-model-id catalog from the curated
@@ -690,6 +698,8 @@ CANONICAL_PROVIDERS: list[ProviderEntry] = [
ProviderEntry("ai-gateway", "Vercel AI Gateway", "Vercel AI Gateway (200+ models, $5 free credit, no markup)"),
ProviderEntry("anthropic", "Anthropic", "Anthropic (Claude models — API key or Claude Code)"),
ProviderEntry("openai-codex", "OpenAI Codex", "OpenAI Codex"),
+ ProviderEntry(VOLCENGINE_PROVIDER, "Volcengine", "Volcengine (standard + Coding Plan catalogs)"),
+ ProviderEntry(BYTEPLUS_PROVIDER, "BytePlus", "BytePlus (standard + Coding Plan catalogs)"),
ProviderEntry("xiaomi", "Xiaomi MiMo", "Xiaomi MiMo (MiMo-V2 models — pro, omni, flash)"),
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)"),
@@ -719,7 +729,6 @@ CANONICAL_PROVIDERS: list[ProviderEntry] = [
_PROVIDER_LABELS = {p.slug: p.label for p in CANONICAL_PROVIDERS}
_PROVIDER_LABELS["custom"] = "Custom endpoint" # special case: not a named provider
-
_PROVIDER_ALIASES = {
"glm": "zai",
"z-ai": "zai",
@@ -782,6 +791,10 @@ _PROVIDER_ALIASES = {
"nemotron": "nvidia",
"ollama": "custom", # bare "ollama" = local; use "ollama-cloud" for cloud
"ollama_cloud": "ollama-cloud",
+ "volcengine-coding-plan": VOLCENGINE_PROVIDER,
+ "volcengine_coding_plan": VOLCENGINE_PROVIDER,
+ "byteplus-coding-plan": BYTEPLUS_PROVIDER,
+ "byteplus_coding_plan": BYTEPLUS_PROVIDER,
}
@@ -1242,7 +1255,6 @@ def list_available_providers() -> list[dict[str, str]]:
"""
# Derive display order from canonical list + custom
provider_order = [p.slug for p in CANONICAL_PROVIDERS] + ["custom"]
-
# Build reverse alias map
aliases_for: dict[str, list[str]] = {}
for alias, canonical in _PROVIDER_ALIASES.items():
@@ -1258,7 +1270,7 @@ def list_available_providers() -> list[dict[str, str]]:
from hermes_cli.auth import get_auth_status, has_usable_secret
if pid == "custom":
custom_base_url = _get_custom_base_url() or ""
- has_creds = bool(custom_base_url.strip())
+ has_creds = bool(custom_base_url.strip()) and provider_for_base_url(custom_base_url) is None
elif pid == "openrouter":
has_creds = has_usable_secret(os.getenv("OPENROUTER_API_KEY", ""))
else:
@@ -1324,6 +1336,44 @@ def _get_custom_base_url() -> str:
return ""
+def provider_for_base_url(base_url: str) -> Optional[str]:
+ """Return a known built-in provider for a configured base URL, if any."""
+ normalized = str(base_url or "").strip().rstrip("/")
+ if not normalized or "openrouter.ai" in normalized.lower():
+ return None
+
+ url_lower = normalized.lower()
+ host_to_provider = {
+ "ark.cn-beijing.volces.com": VOLCENGINE_PROVIDER,
+ "ark.ap-southeast.bytepluses.com": BYTEPLUS_PROVIDER,
+ "api.z.ai": "zai",
+ "api.moonshot.ai": "kimi-coding",
+ "api.kimi.com": "kimi-coding",
+ "api.minimax.io": "minimax",
+ "api.minimaxi.com": "minimax-cn",
+ "dashscope.aliyuncs.com": "alibaba",
+ "dashscope-intl.aliyuncs.com": "alibaba",
+ "portal.qwen.ai": "qwen-oauth",
+ "router.huggingface.co": "huggingface",
+ "generativelanguage.googleapis.com": "gemini",
+ "api.deepseek.com": "deepseek",
+ "api.githubcopilot.com": "copilot",
+ "models.github.ai": "copilot",
+ "opencode.ai": "opencode-go",
+ "api.x.ai": "xai",
+ "api.xiaomimimo.com": "xiaomi",
+ "xiaomimimo.com": "xiaomi",
+ "api.anthropic.com": "anthropic",
+ "inference-api.nousresearch.com": "nous",
+ }
+ for host, provider_id in host_to_provider.items():
+ if host in url_lower:
+ canonical = normalize_provider(provider_id)
+ if canonical in _PROVIDER_LABELS and canonical != "custom":
+ return canonical
+ return None
+
+
def curated_models_for_provider(
provider: Optional[str],
*,
diff --git a/hermes_cli/provider_contracts.py b/hermes_cli/provider_contracts.py
new file mode 100644
index 000000000..189ac5826
--- /dev/null
+++ b/hermes_cli/provider_contracts.py
@@ -0,0 +1,139 @@
+"""Source-of-truth contracts for built-in providers without models.dev catalogs."""
+
+from __future__ import annotations
+
+from typing import Dict, List, Tuple
+
+VOLCENGINE_PROVIDER = "volcengine"
+BYTEPLUS_PROVIDER = "byteplus"
+
+VOLCENGINE_STANDARD_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
+VOLCENGINE_CODING_PLAN_BASE_URL = "https://ark.cn-beijing.volces.com/api/coding/v3"
+BYTEPLUS_STANDARD_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3"
+BYTEPLUS_CODING_PLAN_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/coding/v3"
+
+VOLCENGINE_STANDARD_MODELS: Tuple[str, ...] = (
+ "doubao-seed-2-0-pro-260215",
+ "doubao-seed-2-0-lite-260215",
+ "doubao-seed-2-0-mini-260215",
+ "doubao-seed-2-0-code-preview-260215",
+ "kimi-k2-5-260127",
+ "glm-4-7-251222",
+ "deepseek-v3-2-251201",
+)
+
+VOLCENGINE_CODING_PLAN_MODELS: Tuple[str, ...] = (
+ "doubao-seed-2.0-code",
+ "doubao-seed-2.0-pro",
+ "doubao-seed-2.0-lite",
+ "doubao-seed-code",
+ "minimax-m2.5",
+ "glm-4.7",
+ "deepseek-v3.2",
+ "kimi-k2.5",
+)
+
+BYTEPLUS_STANDARD_MODELS: Tuple[str, ...] = (
+ "seed-2-0-pro-260328",
+ "seed-2-0-lite-260228",
+ "seed-2-0-mini-260215",
+ "kimi-k2-5-260127",
+ "glm-4-7-251222",
+)
+
+BYTEPLUS_CODING_PLAN_MODELS: Tuple[str, ...] = (
+ "dola-seed-2.0-pro",
+ "dola-seed-2.0-lite",
+ "bytedance-seed-code",
+ "glm-4.7",
+ "kimi-k2.5",
+ "gpt-oss-120b",
+)
+
+VOLCENGINE_STANDARD_MODEL_REFS: Tuple[str, ...] = tuple(
+ f"{VOLCENGINE_PROVIDER}/{model_id}" for model_id in VOLCENGINE_STANDARD_MODELS
+)
+VOLCENGINE_CODING_PLAN_MODEL_REFS: Tuple[str, ...] = tuple(
+ f"{VOLCENGINE_PROVIDER}-coding-plan/{model_id}" for model_id in VOLCENGINE_CODING_PLAN_MODELS
+)
+BYTEPLUS_STANDARD_MODEL_REFS: Tuple[str, ...] = tuple(
+ f"{BYTEPLUS_PROVIDER}/{model_id}" for model_id in BYTEPLUS_STANDARD_MODELS
+)
+BYTEPLUS_CODING_PLAN_MODEL_REFS: Tuple[str, ...] = tuple(
+ f"{BYTEPLUS_PROVIDER}-coding-plan/{model_id}" for model_id in BYTEPLUS_CODING_PLAN_MODELS
+)
+
+PROVIDER_MODEL_CATALOGS: Dict[str, Tuple[str, ...]] = {
+ VOLCENGINE_PROVIDER: VOLCENGINE_STANDARD_MODEL_REFS + VOLCENGINE_CODING_PLAN_MODEL_REFS,
+ BYTEPLUS_PROVIDER: BYTEPLUS_STANDARD_MODEL_REFS + BYTEPLUS_CODING_PLAN_MODEL_REFS,
+}
+
+PROVIDER_AUX_MODELS: Dict[str, str] = {
+ VOLCENGINE_PROVIDER: "volcengine/doubao-seed-2-0-lite-260215",
+ BYTEPLUS_PROVIDER: "byteplus/seed-2-0-lite-260228",
+}
+
+MODEL_CONTEXT_WINDOWS: Dict[str, int] = {
+ "doubao-seed-2-0-pro-260215": 256000,
+ "doubao-seed-2-0-lite-260215": 256000,
+ "doubao-seed-2-0-mini-260215": 256000,
+ "doubao-seed-2-0-code-preview-260215": 256000,
+ "kimi-k2-5-260127": 256000,
+ "glm-4-7-251222": 200000,
+ "deepseek-v3-2-251201": 128000,
+ "doubao-seed-2.0-code": 256000,
+ "doubao-seed-2.0-pro": 256000,
+ "doubao-seed-2.0-lite": 256000,
+ "doubao-seed-code": 256000,
+ "minimax-m2.5": 200000,
+ "glm-4.7": 200000,
+ "deepseek-v3.2": 128000,
+ "kimi-k2.5": 256000,
+ "seed-2-0-pro-260328": 256000,
+ "seed-2-0-lite-260228": 256000,
+ "seed-2-0-mini-260215": 256000,
+}
+
+
+def provider_models(provider_id: str) -> List[str]:
+ """Return the full user-facing model catalog for a provider."""
+ return list(PROVIDER_MODEL_CATALOGS.get(provider_id, ()))
+
+
+def _bare_model_name(model_name: str) -> str:
+ value = (model_name or "").strip()
+ if not value:
+ return ""
+ if "/" in value:
+ return value.split("/", 1)[1].strip()
+ return value
+
+
+def is_coding_plan_model(provider_id: str, model_name: str) -> bool:
+ """Return True when a model belongs to the coding-plan catalog."""
+ raw = (model_name or "").strip()
+ bare = _bare_model_name(raw)
+ if provider_id == VOLCENGINE_PROVIDER:
+ return raw in VOLCENGINE_CODING_PLAN_MODEL_REFS or bare in VOLCENGINE_CODING_PLAN_MODELS
+ if provider_id == BYTEPLUS_PROVIDER:
+ return raw in BYTEPLUS_CODING_PLAN_MODEL_REFS or bare in BYTEPLUS_CODING_PLAN_MODELS
+ return False
+
+
+def base_url_for_provider_model(provider_id: str, model_name: str) -> str:
+ """Resolve the source-of-truth base URL for a provider+model pair."""
+ if provider_id == VOLCENGINE_PROVIDER:
+ if is_coding_plan_model(provider_id, model_name):
+ return VOLCENGINE_CODING_PLAN_BASE_URL
+ return VOLCENGINE_STANDARD_BASE_URL
+ if provider_id == BYTEPLUS_PROVIDER:
+ if is_coding_plan_model(provider_id, model_name):
+ return BYTEPLUS_CODING_PLAN_BASE_URL
+ return BYTEPLUS_STANDARD_BASE_URL
+ return ""
+
+
+def model_context_window(model_name: str) -> int | None:
+ """Return a known context window for a model, if specified by the contract."""
+ bare = _bare_model_name(model_name)
+ return MODEL_CONTEXT_WINDOWS.get(bare)
diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py
index e842086a4..d6312766f 100644
--- a/hermes_cli/providers.py
+++ b/hermes_cli/providers.py
@@ -23,6 +23,12 @@ import logging
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
+from hermes_cli.provider_contracts import (
+ BYTEPLUS_PROVIDER,
+ BYTEPLUS_STANDARD_BASE_URL,
+ VOLCENGINE_PROVIDER,
+ VOLCENGINE_STANDARD_BASE_URL,
+)
from utils import base_url_host_matches, base_url_hostname
logger = logging.getLogger(__name__)
@@ -163,6 +169,16 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = {
transport="openai_chat",
base_url_env_var="OLLAMA_BASE_URL",
),
+ VOLCENGINE_PROVIDER: HermesOverlay(
+ transport="openai_chat",
+ extra_env_vars=("VOLCENGINE_API_KEY",),
+ base_url_override=VOLCENGINE_STANDARD_BASE_URL,
+ ),
+ BYTEPLUS_PROVIDER: HermesOverlay(
+ transport="openai_chat",
+ extra_env_vars=("BYTEPLUS_API_KEY",),
+ base_url_override=BYTEPLUS_STANDARD_BASE_URL,
+ ),
}
@@ -273,6 +289,10 @@ ALIASES: Dict[str, str] = {
# xiaomi
"mimo": "xiaomi",
"xiaomi-mimo": "xiaomi",
+ "volcengine-coding-plan": VOLCENGINE_PROVIDER,
+ "volcengine_coding_plan": VOLCENGINE_PROVIDER,
+ "byteplus-coding-plan": BYTEPLUS_PROVIDER,
+ "byteplus_coding_plan": BYTEPLUS_PROVIDER,
# bedrock
"aws": "bedrock",
@@ -306,6 +326,8 @@ _LABEL_OVERRIDES: Dict[str, str] = {
"copilot-acp": "GitHub Copilot ACP",
"stepfun": "StepFun Step Plan",
"xiaomi": "Xiaomi MiMo",
+ VOLCENGINE_PROVIDER: "Volcengine",
+ BYTEPLUS_PROVIDER: "BytePlus",
"local": "Local endpoint",
"bedrock": "AWS Bedrock",
"ollama-cloud": "Ollama Cloud",
diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py
index 4c775b8a6..9a13d307f 100644
--- a/tests/agent/test_auxiliary_client.py
+++ b/tests/agent/test_auxiliary_client.py
@@ -782,6 +782,44 @@ def test_resolve_api_key_provider_skips_unconfigured_anthropic(monkeypatch):
# ---------------------------------------------------------------------------
+class TestModelDefaultElimination:
+ """_resolve_api_key_provider must skip providers without known aux models."""
+
+ def test_unknown_provider_skipped(self, monkeypatch):
+ """Providers not in _API_KEY_PROVIDER_AUX_MODELS are skipped, not sent model='default'."""
+ from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS
+
+ # Verify our known providers have entries
+ assert "gemini" in _API_KEY_PROVIDER_AUX_MODELS
+ assert "kimi-coding" in _API_KEY_PROVIDER_AUX_MODELS
+
+ # A random provider_id not in the dict should return None
+ assert _API_KEY_PROVIDER_AUX_MODELS.get("totally-unknown-provider") is None
+
+ def test_known_provider_gets_real_model(self):
+ """Known providers get a real model name, not 'default'."""
+ from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS
+
+ for provider_id, model in _API_KEY_PROVIDER_AUX_MODELS.items():
+ assert model != "default", f"{provider_id} should not map to 'default'"
+ assert isinstance(model, str) and model.strip(), \
+ f"{provider_id} should have a non-empty model string"
+
+ def test_contract_providers_have_aux_models(self):
+ from agent.auxiliary_client import _API_KEY_PROVIDER_AUX_MODELS
+
+ assert _API_KEY_PROVIDER_AUX_MODELS["volcengine"] == "volcengine/doubao-seed-2-0-lite-260215"
+ assert _API_KEY_PROVIDER_AUX_MODELS["byteplus"] == "byteplus/seed-2-0-lite-260228"
+
+
+class TestContractProviderAliases:
+ def test_coding_plan_aliases_normalize_to_canonical_provider(self):
+ from agent.auxiliary_client import _normalize_aux_provider
+
+ assert _normalize_aux_provider("volcengine-coding-plan") == "volcengine"
+ assert _normalize_aux_provider("byteplus-coding-plan") == "byteplus"
+
+
# ---------------------------------------------------------------------------
# _try_payment_fallback reason parameter (#7512 bug 3)
# ---------------------------------------------------------------------------
diff --git a/tests/agent/test_model_metadata.py b/tests/agent/test_model_metadata.py
index 45e716022..eb3e15a1f 100644
--- a/tests/agent/test_model_metadata.py
+++ b/tests/agent/test_model_metadata.py
@@ -222,6 +222,22 @@ class TestGetModelContextLength:
mock_fetch.return_value = {}
assert get_model_context_length("unknown/never-heard-of-this") == CONTEXT_PROBE_TIERS[0]
+ @patch("agent.model_metadata.fetch_model_metadata")
+ def test_volcengine_contract_model_uses_contract_context_length(self, mock_fetch):
+ mock_fetch.return_value = {}
+ assert get_model_context_length(
+ "volcengine/doubao-seed-2-0-pro-260215",
+ provider="volcengine",
+ ) == 256000
+
+ @patch("agent.model_metadata.fetch_model_metadata")
+ def test_byteplus_contract_model_infers_provider_from_url(self, mock_fetch):
+ mock_fetch.return_value = {}
+ assert get_model_context_length(
+ "byteplus-coding-plan/kimi-k2.5",
+ base_url="https://ark.ap-southeast.bytepluses.com/api/coding/v3",
+ ) == 256000
+
@patch("agent.model_metadata.fetch_model_metadata")
def test_partial_match_in_defaults(self, mock_fetch):
mock_fetch.return_value = {}
diff --git a/tests/hermes_cli/test_api_key_providers.py b/tests/hermes_cli/test_api_key_providers.py
index e8f181fa4..271aa3d47 100644
--- a/tests/hermes_cli/test_api_key_providers.py
+++ b/tests/hermes_cli/test_api_key_providers.py
@@ -42,6 +42,8 @@ class TestProviderRegistry:
("minimax-cn", "MiniMax (China)", "api_key"),
("ai-gateway", "Vercel AI Gateway", "api_key"),
("kilocode", "Kilo Code", "api_key"),
+ ("volcengine", "Volcengine", "api_key"),
+ ("byteplus", "BytePlus", "api_key"),
])
def test_provider_registered(self, provider_id, name, auth_type):
assert provider_id in PROVIDER_REGISTRY
@@ -111,6 +113,16 @@ class TestProviderRegistry:
assert pconfig.api_key_env_vars == ("HF_TOKEN",)
assert pconfig.base_url_env_var == "HF_BASE_URL"
+ def test_volcengine_env_vars(self):
+ pconfig = PROVIDER_REGISTRY["volcengine"]
+ assert pconfig.api_key_env_vars == ("VOLCENGINE_API_KEY",)
+ assert pconfig.base_url_env_var == ""
+
+ def test_byteplus_env_vars(self):
+ pconfig = PROVIDER_REGISTRY["byteplus"]
+ assert pconfig.api_key_env_vars == ("BYTEPLUS_API_KEY",)
+ assert pconfig.base_url_env_var == ""
+
def test_base_urls(self):
assert PROVIDER_REGISTRY["copilot"].inference_base_url == "https://api.githubcopilot.com"
assert PROVIDER_REGISTRY["copilot-acp"].inference_base_url == "acp://copilot"
@@ -122,6 +134,8 @@ class TestProviderRegistry:
assert PROVIDER_REGISTRY["ai-gateway"].inference_base_url == "https://ai-gateway.vercel.sh/v1"
assert PROVIDER_REGISTRY["kilocode"].inference_base_url == "https://api.kilo.ai/api/gateway"
assert PROVIDER_REGISTRY["huggingface"].inference_base_url == "https://router.huggingface.co/v1"
+ assert PROVIDER_REGISTRY["volcengine"].inference_base_url == "https://ark.cn-beijing.volces.com/api/v3"
+ assert PROVIDER_REGISTRY["byteplus"].inference_base_url == "https://ark.ap-southeast.bytepluses.com/api/v3"
def test_oauth_providers_unchanged(self):
"""Ensure we didn't break the existing OAuth providers."""
@@ -147,6 +161,7 @@ PROVIDER_ENV_VARS = (
"NOUS_API_KEY", "GITHUB_TOKEN", "GH_TOKEN",
"OPENAI_BASE_URL", "HERMES_COPILOT_ACP_COMMAND", "COPILOT_CLI_PATH",
"HERMES_COPILOT_ACP_ARGS", "COPILOT_ACP_BASE_URL",
+ "VOLCENGINE_API_KEY", "BYTEPLUS_API_KEY",
)
@@ -232,6 +247,14 @@ class TestResolveProvider:
assert resolve_provider("github-copilot-acp") == "copilot-acp"
assert resolve_provider("copilot-acp-agent") == "copilot-acp"
+ def test_alias_volcengine_coding_plan(self):
+ assert resolve_provider("volcengine-coding-plan") == "volcengine"
+ assert resolve_provider("volcengine_coding_plan") == "volcengine"
+
+ def test_alias_byteplus_coding_plan(self):
+ assert resolve_provider("byteplus-coding-plan") == "byteplus"
+ assert resolve_provider("byteplus_coding_plan") == "byteplus"
+
def test_explicit_huggingface(self):
assert resolve_provider("huggingface") == "huggingface"
@@ -339,6 +362,23 @@ class TestApiKeyProviderStatus:
assert status["configured"] is True
assert status["base_url"] == STEPFUN_STEP_PLAN_CN_BASE_URL
+ def test_volcengine_status_uses_coding_plan_base_url(self, monkeypatch):
+ monkeypatch.setenv("VOLCENGINE_API_KEY", "volc-test-key")
+ monkeypatch.setattr(
+ "hermes_cli.auth.read_raw_config",
+ lambda: {
+ "model": {
+ "provider": "volcengine",
+ "default": "volcengine-coding-plan/doubao-seed-2.0-code",
+ }
+ },
+ )
+
+ status = get_api_key_provider_status("volcengine")
+
+ assert status["configured"] is True
+ assert status["base_url"] == "https://ark.cn-beijing.volces.com/api/coding/v3"
+
def test_copilot_status_uses_gh_cli_token(self, monkeypatch):
monkeypatch.setattr("hermes_cli.copilot_auth._try_gh_cli_token", lambda: "gho_gh_cli_token")
status = get_api_key_provider_status("copilot")
@@ -394,6 +434,25 @@ class TestResolveApiKeyProviderCredentials:
assert creds["base_url"] == "https://api.z.ai/api/paas/v4"
assert creds["source"] == "GLM_API_KEY"
+ def test_resolve_byteplus_with_coding_plan_model_uses_coding_base_url(self, monkeypatch):
+ monkeypatch.setenv("BYTEPLUS_API_KEY", "byteplus-secret-key")
+ monkeypatch.setattr(
+ "hermes_cli.auth.read_raw_config",
+ lambda: {
+ "model": {
+ "provider": "byteplus",
+ "default": "byteplus-coding-plan/dola-seed-2.0-pro",
+ }
+ },
+ )
+
+ creds = resolve_api_key_provider_credentials("byteplus")
+
+ assert creds["provider"] == "byteplus"
+ assert creds["api_key"] == "byteplus-secret-key"
+ assert creds["base_url"] == "https://ark.ap-southeast.bytepluses.com/api/coding/v3"
+ assert creds["source"] == "BYTEPLUS_API_KEY"
+
def test_resolve_copilot_with_github_token(self, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "gh-env-secret")
creds = resolve_api_key_provider_credentials("copilot")
diff --git a/tests/hermes_cli/test_model_normalize.py b/tests/hermes_cli/test_model_normalize.py
index 6de69ab30..75c8c5caa 100644
--- a/tests/hermes_cli/test_model_normalize.py
+++ b/tests/hermes_cli/test_model_normalize.py
@@ -179,6 +179,19 @@ class TestIssue6211NativeProviderPrefixNormalization:
assert normalize_model_for_provider(model, target_provider) == expected
+class TestContractProviderPrefixNormalization:
+ @pytest.mark.parametrize("model,target_provider,expected", [
+ ("volcengine/doubao-seed-2-0-pro-260215", "volcengine", "doubao-seed-2-0-pro-260215"),
+ ("volcengine-coding-plan/doubao-seed-2.0-code", "volcengine", "doubao-seed-2.0-code"),
+ ("byteplus/seed-2-0-pro-260328", "byteplus", "seed-2-0-pro-260328"),
+ ("byteplus-coding-plan/dola-seed-2.0-pro", "byteplus", "dola-seed-2.0-pro"),
+ ])
+ def test_contract_provider_prefixes_strip_to_native_model(
+ self, model, target_provider, expected
+ ):
+ assert normalize_model_for_provider(model, target_provider) == expected
+
+
# ── detect_vendor ──────────────────────────────────────────────────────
class TestDetectVendor:
diff --git a/tests/hermes_cli/test_model_provider_persistence.py b/tests/hermes_cli/test_model_provider_persistence.py
index 067483680..d22ab777d 100644
--- a/tests/hermes_cli/test_model_provider_persistence.py
+++ b/tests/hermes_cli/test_model_provider_persistence.py
@@ -102,6 +102,31 @@ class TestProviderPersistsAfterModelSave:
)
assert model.get("default") == "kimi-k2.5"
+ def test_volcengine_contract_provider_persists_coding_plan_model(self, config_home, monkeypatch):
+ """Volcengine should persist a prefixed coding-plan model and matching base URL."""
+ monkeypatch.setenv("VOLCENGINE_API_KEY", "volc-test-key")
+
+ from hermes_cli.main import _model_flow_contract_provider
+ from hermes_cli.config import load_config
+
+ with patch(
+ "hermes_cli.auth._prompt_model_selection",
+ return_value="volcengine-coding-plan/doubao-seed-2.0-code",
+ ), patch(
+ "hermes_cli.auth.deactivate_provider",
+ ):
+ _model_flow_contract_provider(load_config(), "volcengine", "old-model")
+
+ import yaml
+
+ config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
+ model = config.get("model")
+ assert isinstance(model, dict), f"model should be dict, got {type(model)}"
+ assert model.get("provider") == "volcengine"
+ assert model.get("default") == "volcengine-coding-plan/doubao-seed-2.0-code"
+ assert model.get("base_url") == "https://ark.cn-beijing.volces.com/api/coding/v3"
+ assert "api_mode" not in model
+
def test_copilot_provider_saved_when_selected(self, config_home):
"""_model_flow_copilot should persist provider/base_url/model together."""
from hermes_cli.main import _model_flow_copilot
diff --git a/tests/hermes_cli/test_models.py b/tests/hermes_cli/test_models.py
index b493fd2b6..e5b0e9c0d 100644
--- a/tests/hermes_cli/test_models.py
+++ b/tests/hermes_cli/test_models.py
@@ -6,6 +6,7 @@ from hermes_cli.models import (
OPENROUTER_MODELS, fetch_openrouter_models, model_ids, detect_provider_for_model,
is_nous_free_tier, partition_nous_models_by_tier,
check_nous_free_tier, _FREE_TIER_CACHE_TTL,
+ list_available_providers, provider_for_base_url,
)
import hermes_cli.models as _models_mod
@@ -291,6 +292,41 @@ class TestDetectProviderForModel:
assert result is not None
assert result[0] not in ("nous",) # nous has claude models but shouldn't be suggested
+ def test_volcengine_coding_plan_model_detected(self):
+ result = detect_provider_for_model(
+ "volcengine-coding-plan/doubao-seed-2.0-code",
+ "openrouter",
+ )
+ assert result == ("volcengine", "volcengine-coding-plan/doubao-seed-2.0-code")
+
+ def test_byteplus_standard_model_detected(self):
+ result = detect_provider_for_model(
+ "byteplus/seed-2-0-pro-260328",
+ "openrouter",
+ )
+ assert result == ("byteplus", "byteplus/seed-2-0-pro-260328")
+
+
+class TestConfiguredBaseUrlProviderDetection:
+ def test_provider_for_base_url_detects_volcengine(self):
+ assert provider_for_base_url("https://ark.cn-beijing.volces.com/api/v3") == "volcengine"
+
+ def test_provider_for_base_url_detects_byteplus_coding(self):
+ assert provider_for_base_url("https://ark.ap-southeast.bytepluses.com/api/coding/v3") == "byteplus"
+
+ def test_known_builtin_endpoint_is_not_listed_as_custom(self, monkeypatch):
+ monkeypatch.setattr("hermes_cli.models._get_custom_base_url", lambda: "https://ark.cn-beijing.volces.com/api/v3")
+ monkeypatch.setattr(
+ "hermes_cli.auth.get_auth_status",
+ lambda pid: {"configured": pid == "volcengine", "logged_in": pid == "volcengine"},
+ )
+ monkeypatch.setattr("hermes_cli.auth.has_usable_secret", lambda value: False)
+
+ providers = {p["id"]: p for p in list_available_providers()}
+
+ assert providers["volcengine"]["authenticated"] is True
+ assert providers["custom"]["authenticated"] is False
+
class TestIsNousFreeTier:
"""Tests for is_nous_free_tier — account tier detection."""
diff --git a/website/docs/integrations/providers.md b/website/docs/integrations/providers.md
index 013c6a3e3..3375cfd9d 100644
--- a/website/docs/integrations/providers.md
+++ b/website/docs/integrations/providers.md
@@ -30,6 +30,8 @@ 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`) |
+| **Volcengine** | `hermes model` or `VOLCENGINE_API_KEY` in `~/.hermes/.env` (provider: `volcengine`) |
+| **BytePlus** | `hermes model` or `BYTEPLUS_API_KEY` in `~/.hermes/.env` (provider: `byteplus`) |
| **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`) |
@@ -274,17 +276,59 @@ hermes chat --provider xiaomi --model mimo-v2-pro
# Arcee AI (Trinity models)
hermes chat --provider arcee --model trinity-large-thinking
# Requires: ARCEEAI_API_KEY in ~/.hermes/.env
+
+# Volcengine
+hermes chat --provider volcengine --model volcengine/doubao-seed-2-0-pro-260215
+# Requires: VOLCENGINE_API_KEY in ~/.hermes/.env
+
+# Volcengine Coding Plan catalog (same provider, same API key)
+hermes chat --provider volcengine --model volcengine-coding-plan/doubao-seed-2.0-code
+
+# BytePlus
+hermes chat --provider byteplus --model byteplus/seed-2-0-pro-260328
+# Requires: BYTEPLUS_API_KEY in ~/.hermes/.env
+
+# BytePlus Coding Plan catalog (same provider, same API key)
+hermes chat --provider byteplus --model byteplus-coding-plan/dola-seed-2.0-pro
```
Or set the provider permanently in `config.yaml`:
```yaml
model:
- provider: "zai" # or: kimi-coding, kimi-coding-cn, minimax, minimax-cn, alibaba, xiaomi, arcee
+ provider: "zai" # or: kimi-coding, kimi-coding-cn, minimax, minimax-cn, alibaba, xiaomi, arcee, volcengine, byteplus
default: "glm-5"
```
Base URLs can be overridden with `GLM_BASE_URL`, `KIMI_BASE_URL`, `MINIMAX_BASE_URL`, `MINIMAX_CN_BASE_URL`, `DASHSCOPE_BASE_URL`, or `XIAOMI_BASE_URL` environment variables.
+### Volcengine and BytePlus Contract Catalogs
+
+Hermes exposes **two** built-in providers for these integrations:
+
+- `volcengine`
+- `byteplus`
+
+Each provider includes both its standard catalog and its Coding Plan catalog. The selected model ID determines the runtime base URL automatically:
+
+- `volcengine/...` -> `https://ark.cn-beijing.volces.com/api/v3`
+- `volcengine-coding-plan/...` -> `https://ark.cn-beijing.volces.com/api/coding/v3`
+- `byteplus/...` -> `https://ark.ap-southeast.bytepluses.com/api/v3`
+- `byteplus-coding-plan/...` -> `https://ark.ap-southeast.bytepluses.com/api/coding/v3`
+
+In `hermes model`, the setup flow is:
+
+1. Enter API key
+2. Select a model
+
+If you pick a `volcengine-coding-plan/...` or `byteplus-coding-plan/...` model, Hermes automatically uses the corresponding coding-plan base URL.
+
+The API key is shared per provider:
+
+- `VOLCENGINE_API_KEY` works for both `volcengine/...` and `volcengine-coding-plan/...`
+- `BYTEPLUS_API_KEY` works for both `byteplus/...` and `byteplus-coding-plan/...`
+
+Use `hermes model` to pick from the built-in curated catalogs. Hermes saves the canonical prefixed model ID in `config.yaml`, so standard and Coding Plan variants remain unambiguous.
+
:::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.
:::