feat: add Step Plan provider support (salvage #6005)
Adds a first-class 'stepfun' API-key provider surfaced as Step Plan: - Support Step Plan setup for both International and China regions - Discover Step Plan models live from /step_plan/v1/models, with a small coding-focused fallback catalog when discovery is unavailable - Thread StepFun through provider metadata, setup persistence, status and doctor output, auxiliary routing, and model normalization - Add tests for provider resolution, model validation, metadata mapping, and StepFun region/model persistence Based on #6005 by @hengm3467. Co-authored-by: hengm3467 <100685635+hengm3467@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,8 @@ from hermes_cli.auth import (
|
||||
get_auth_status,
|
||||
AuthError,
|
||||
KIMI_CODE_BASE_URL,
|
||||
STEPFUN_STEP_PLAN_INTL_BASE_URL,
|
||||
STEPFUN_STEP_PLAN_CN_BASE_URL,
|
||||
_resolve_kimi_base_url,
|
||||
)
|
||||
from hermes_cli.copilot_auth import _try_gh_cli_token
|
||||
@@ -35,6 +37,7 @@ class TestProviderRegistry:
|
||||
("xai", "xAI", "api_key"),
|
||||
("nvidia", "NVIDIA NIM", "api_key"),
|
||||
("kimi-coding", "Kimi / Moonshot", "api_key"),
|
||||
("stepfun", "StepFun Step Plan", "api_key"),
|
||||
("minimax", "MiniMax", "api_key"),
|
||||
("minimax-cn", "MiniMax (China)", "api_key"),
|
||||
("ai-gateway", "Vercel AI Gateway", "api_key"),
|
||||
@@ -83,6 +86,11 @@ class TestProviderRegistry:
|
||||
assert pconfig.api_key_env_vars == ("MINIMAX_API_KEY",)
|
||||
assert pconfig.base_url_env_var == "MINIMAX_BASE_URL"
|
||||
|
||||
def test_stepfun_env_vars(self):
|
||||
pconfig = PROVIDER_REGISTRY["stepfun"]
|
||||
assert pconfig.api_key_env_vars == ("STEPFUN_API_KEY",)
|
||||
assert pconfig.base_url_env_var == "STEPFUN_BASE_URL"
|
||||
|
||||
def test_minimax_cn_env_vars(self):
|
||||
pconfig = PROVIDER_REGISTRY["minimax-cn"]
|
||||
assert pconfig.api_key_env_vars == ("MINIMAX_CN_API_KEY",)
|
||||
@@ -108,6 +116,7 @@ class TestProviderRegistry:
|
||||
assert PROVIDER_REGISTRY["copilot-acp"].inference_base_url == "acp://copilot"
|
||||
assert PROVIDER_REGISTRY["zai"].inference_base_url == "https://api.z.ai/api/paas/v4"
|
||||
assert PROVIDER_REGISTRY["kimi-coding"].inference_base_url == "https://api.moonshot.ai/v1"
|
||||
assert PROVIDER_REGISTRY["stepfun"].inference_base_url == STEPFUN_STEP_PLAN_INTL_BASE_URL
|
||||
assert PROVIDER_REGISTRY["minimax"].inference_base_url == "https://api.minimax.io/anthropic"
|
||||
assert PROVIDER_REGISTRY["minimax-cn"].inference_base_url == "https://api.minimaxi.com/anthropic"
|
||||
assert PROVIDER_REGISTRY["ai-gateway"].inference_base_url == "https://ai-gateway.vercel.sh/v1"
|
||||
@@ -130,7 +139,8 @@ PROVIDER_ENV_VARS = (
|
||||
"OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN",
|
||||
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||
"GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY",
|
||||
"KIMI_API_KEY", "KIMI_BASE_URL", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
||||
"KIMI_API_KEY", "KIMI_BASE_URL", "STEPFUN_API_KEY", "STEPFUN_BASE_URL",
|
||||
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
||||
"AI_GATEWAY_API_KEY", "AI_GATEWAY_BASE_URL",
|
||||
"KILOCODE_API_KEY", "KILOCODE_BASE_URL",
|
||||
"DASHSCOPE_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY",
|
||||
@@ -156,6 +166,9 @@ class TestResolveProvider:
|
||||
def test_explicit_kimi_coding(self):
|
||||
assert resolve_provider("kimi-coding") == "kimi-coding"
|
||||
|
||||
def test_explicit_stepfun(self):
|
||||
assert resolve_provider("stepfun") == "stepfun"
|
||||
|
||||
def test_explicit_minimax(self):
|
||||
assert resolve_provider("minimax") == "minimax"
|
||||
|
||||
@@ -180,6 +193,9 @@ class TestResolveProvider:
|
||||
def test_alias_moonshot(self):
|
||||
assert resolve_provider("moonshot") == "kimi-coding"
|
||||
|
||||
def test_alias_step(self):
|
||||
assert resolve_provider("step") == "stepfun"
|
||||
|
||||
def test_alias_minimax_underscore(self):
|
||||
assert resolve_provider("minimax_cn") == "minimax-cn"
|
||||
|
||||
@@ -248,6 +264,10 @@ class TestResolveProvider:
|
||||
monkeypatch.setenv("KIMI_API_KEY", "test-kimi-key")
|
||||
assert resolve_provider("auto") == "kimi-coding"
|
||||
|
||||
def test_auto_detects_stepfun_key(self, monkeypatch):
|
||||
monkeypatch.setenv("STEPFUN_API_KEY", "test-stepfun-key")
|
||||
assert resolve_provider("auto") == "stepfun"
|
||||
|
||||
def test_auto_detects_minimax_key(self, monkeypatch):
|
||||
monkeypatch.setenv("MINIMAX_API_KEY", "test-mm-key")
|
||||
assert resolve_provider("auto") == "minimax"
|
||||
@@ -312,6 +332,13 @@ class TestApiKeyProviderStatus:
|
||||
status = get_api_key_provider_status("kimi-coding")
|
||||
assert status["base_url"] == "https://custom.kimi.example/v1"
|
||||
|
||||
def test_stepfun_status_uses_configured_base_url(self, monkeypatch):
|
||||
monkeypatch.setenv("STEPFUN_API_KEY", "stepfun-key")
|
||||
monkeypatch.setenv("STEPFUN_BASE_URL", STEPFUN_STEP_PLAN_CN_BASE_URL)
|
||||
status = get_api_key_provider_status("stepfun")
|
||||
assert status["configured"] is True
|
||||
assert status["base_url"] == STEPFUN_STEP_PLAN_CN_BASE_URL
|
||||
|
||||
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")
|
||||
@@ -429,6 +456,19 @@ class TestResolveApiKeyProviderCredentials:
|
||||
assert creds["api_key"] == "kimi-secret-key"
|
||||
assert creds["base_url"] == "https://api.moonshot.ai/v1"
|
||||
|
||||
def test_resolve_stepfun_with_key(self, monkeypatch):
|
||||
monkeypatch.setenv("STEPFUN_API_KEY", "stepfun-secret-key")
|
||||
creds = resolve_api_key_provider_credentials("stepfun")
|
||||
assert creds["provider"] == "stepfun"
|
||||
assert creds["api_key"] == "stepfun-secret-key"
|
||||
assert creds["base_url"] == STEPFUN_STEP_PLAN_INTL_BASE_URL
|
||||
|
||||
def test_resolve_stepfun_custom_base_url(self, monkeypatch):
|
||||
monkeypatch.setenv("STEPFUN_API_KEY", "stepfun-secret-key")
|
||||
monkeypatch.setenv("STEPFUN_BASE_URL", STEPFUN_STEP_PLAN_CN_BASE_URL)
|
||||
creds = resolve_api_key_provider_credentials("stepfun")
|
||||
assert creds["base_url"] == STEPFUN_STEP_PLAN_CN_BASE_URL
|
||||
|
||||
def test_resolve_minimax_with_key(self, monkeypatch):
|
||||
monkeypatch.setenv("MINIMAX_API_KEY", "mm-secret-key")
|
||||
creds = resolve_api_key_provider_credentials("minimax")
|
||||
@@ -519,6 +559,16 @@ class TestRuntimeProviderResolution:
|
||||
assert result["api_mode"] == "chat_completions"
|
||||
assert result["api_key"] == "kimi-key"
|
||||
|
||||
def test_runtime_stepfun(self, monkeypatch):
|
||||
monkeypatch.setenv("STEPFUN_API_KEY", "stepfun-key")
|
||||
monkeypatch.setenv("STEPFUN_BASE_URL", STEPFUN_STEP_PLAN_CN_BASE_URL)
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
result = resolve_runtime_provider(requested="stepfun")
|
||||
assert result["provider"] == "stepfun"
|
||||
assert result["api_mode"] == "chat_completions"
|
||||
assert result["api_key"] == "stepfun-key"
|
||||
assert result["base_url"] == STEPFUN_STEP_PLAN_CN_BASE_URL
|
||||
|
||||
def test_runtime_minimax(self, monkeypatch):
|
||||
monkeypatch.setenv("MINIMAX_API_KEY", "mm-key")
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
|
||||
@@ -32,6 +32,8 @@ def config_home(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
||||
monkeypatch.delenv("STEPFUN_API_KEY", raising=False)
|
||||
monkeypatch.delenv("STEPFUN_BASE_URL", raising=False)
|
||||
return home
|
||||
|
||||
|
||||
@@ -330,3 +332,33 @@ class TestBaseUrlValidation:
|
||||
|
||||
saved = get_env_value("GLM_BASE_URL") or ""
|
||||
assert saved == "", "Empty input should not save a base URL"
|
||||
|
||||
def test_stepfun_provider_saved_with_selected_region(self, config_home, monkeypatch):
|
||||
from hermes_cli.main import _model_flow_stepfun
|
||||
from hermes_cli.config import load_config, get_env_value
|
||||
|
||||
monkeypatch.setenv("STEPFUN_API_KEY", "stepfun-test-key")
|
||||
|
||||
with patch(
|
||||
"hermes_cli.main._prompt_provider_choice",
|
||||
return_value=1,
|
||||
), patch(
|
||||
"hermes_cli.models.fetch_api_models",
|
||||
return_value=["step-3.5-flash", "step-3-agent-lite"],
|
||||
), patch(
|
||||
"hermes_cli.auth._prompt_model_selection",
|
||||
return_value="step-3-agent-lite",
|
||||
), patch(
|
||||
"hermes_cli.auth.deactivate_provider",
|
||||
):
|
||||
_model_flow_stepfun(load_config(), "old-model")
|
||||
|
||||
import yaml
|
||||
|
||||
config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
|
||||
model = config.get("model")
|
||||
assert isinstance(model, dict)
|
||||
assert model.get("provider") == "stepfun"
|
||||
assert model.get("default") == "step-3-agent-lite"
|
||||
assert model.get("base_url") == "https://api.stepfun.com/step_plan/v1"
|
||||
assert get_env_value("STEPFUN_BASE_URL") == "https://api.stepfun.com/step_plan/v1"
|
||||
|
||||
@@ -63,6 +63,11 @@ class TestParseModelInput:
|
||||
assert provider == "zai"
|
||||
assert model == "glm-5"
|
||||
|
||||
def test_stepfun_alias_resolved(self):
|
||||
provider, model = parse_model_input("step:step-3.5-flash", "openrouter")
|
||||
assert provider == "stepfun"
|
||||
assert model == "step-3.5-flash"
|
||||
|
||||
def test_no_slash_no_colon_keeps_provider(self):
|
||||
provider, model = parse_model_input("gpt-5.4", "openrouter")
|
||||
assert provider == "openrouter"
|
||||
@@ -154,6 +159,7 @@ class TestNormalizeProvider:
|
||||
assert normalize_provider("glm") == "zai"
|
||||
assert normalize_provider("kimi") == "kimi-coding"
|
||||
assert normalize_provider("moonshot") == "kimi-coding"
|
||||
assert normalize_provider("step") == "stepfun"
|
||||
assert normalize_provider("github-copilot") == "copilot"
|
||||
|
||||
def test_case_insensitive(self):
|
||||
@@ -164,6 +170,7 @@ class TestProviderLabel:
|
||||
def test_known_labels_and_auto(self):
|
||||
assert provider_label("anthropic") == "Anthropic"
|
||||
assert provider_label("kimi") == "Kimi / Kimi Coding Plan"
|
||||
assert provider_label("stepfun") == "StepFun Step Plan"
|
||||
assert provider_label("copilot") == "GitHub Copilot"
|
||||
assert provider_label("copilot-acp") == "GitHub Copilot ACP"
|
||||
assert provider_label("auto") == "Auto"
|
||||
@@ -193,6 +200,16 @@ class TestProviderModelIds:
|
||||
def test_zai_returns_glm_models(self):
|
||||
assert "glm-5" in provider_model_ids("zai")
|
||||
|
||||
def test_stepfun_prefers_live_catalog(self):
|
||||
with patch(
|
||||
"hermes_cli.auth.resolve_api_key_provider_credentials",
|
||||
return_value={"api_key": "***", "base_url": "https://api.stepfun.com/step_plan/v1"},
|
||||
), patch(
|
||||
"hermes_cli.models.fetch_api_models",
|
||||
return_value=["step-3.5-flash", "step-3-agent-lite"],
|
||||
):
|
||||
assert provider_model_ids("stepfun") == ["step-3.5-flash", "step-3-agent-lite"]
|
||||
|
||||
def test_copilot_prefers_live_catalog(self):
|
||||
with patch("hermes_cli.auth.resolve_api_key_provider_credentials", return_value={"api_key": "gh-token"}), \
|
||||
patch("hermes_cli.models._fetch_github_models", return_value=["gpt-5.4", "claude-sonnet-4.6"]):
|
||||
|
||||
Reference in New Issue
Block a user