diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 00685436d..4a812a64b 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -182,6 +182,14 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = { api_key_env_vars=("GOOGLE_API_KEY", "GEMINI_API_KEY"), base_url_env_var="GEMINI_BASE_URL", ), + "ppq": ProviderConfig( + id="ppq", + name="PPQ (PayPerQ)", + auth_type="api_key", + inference_base_url="https://api.ppq.ai", + api_key_env_vars=("PPQ_API_KEY",), + base_url_env_var="PPQ_BASE_URL", + ), "zai": ProviderConfig( id="zai", name="Z.AI / GLM", @@ -1090,6 +1098,7 @@ def resolve_provider( # Normalize provider aliases _PROVIDER_ALIASES = { "glm": "zai", "z-ai": "zai", "z.ai": "zai", "zhipu": "zai", + "payperq": "ppq", "ppq.ai": "ppq", "google": "gemini", "google-gemini": "gemini", "google-ai-studio": "gemini", "x-ai": "xai", "x.ai": "xai", "grok": "xai", "kimi": "kimi-coding", "kimi-for-coding": "kimi-coding", "moonshot": "kimi-coding", diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 3a902ffdf..159a264a1 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -177,6 +177,40 @@ _PROVIDER_MODELS: dict[str, list[str]] = { "gemini-2.5-pro", "grok-code-fast-1", ], + "ppq": [ + # Curated PPQ list — mirrors the OpenRouter selection using PPQ's native IDs. + # PPQ uses shorter IDs for some providers (e.g. claude-opus-4.7 vs anthropic/claude-opus-4.7). + # Verified against live /v1/models catalog (329 models as of 2026-04-18). + "claude-opus-4.7", + "anthropic/claude-opus-4.6", + "claude-sonnet-4.6", + "qwen/qwen3.6-plus", + "anthropic/claude-sonnet-4.5", + "claude-haiku-4.5", + "gpt-5.4", + "gpt-5.4-mini", + "gpt-5.4-pro", + "gpt-5.4-nano", + "gpt-5.3-codex", + "google/gemini-3-pro-image-preview", + "gemini-3-flash-preview", + "google/gemini-3.1-pro-preview", + "google/gemini-3.1-flash-lite-preview", + "xiaomi/mimo-v2-pro", + "z-ai/glm-5.1", + "z-ai/glm-5-turbo", + "z-ai/glm-5v-turbo", + "moonshotai/kimi-k2.5", + "minimax/minimax-m2.7", + "minimax/minimax-m2.5", + "qwen/qwen3.5-plus-02-15", + "qwen/qwen3.5-35b-a3b", + "stepfun/step-3.5-flash", + "x-ai/grok-4.20", + "nvidia/nemotron-3-super-120b-a12b", + "arcee-ai/trinity-large-thinking", + "arcee-ai/trinity-large-preview", + ], "gemini": [ "gemini-3.1-pro-preview", "gemini-3-pro-preview", @@ -725,6 +759,7 @@ CANONICAL_PROVIDERS: list[ProviderEntry] = [ ProviderEntry("huggingface", "Hugging Face", "Hugging Face Inference Providers (20+ open models)"), ProviderEntry("gemini", "Google AI Studio", "Google AI Studio (Gemini models — native Gemini API)"), ProviderEntry("google-gemini-cli", "Google Gemini (OAuth)", "Google Gemini via OAuth + Code Assist (free tier supported; no API key needed)"), + ProviderEntry("ppq", "PPQ (PayPerQ)", "PPQ (PayPerQ multi-provider gateway)"), ProviderEntry("deepseek", "DeepSeek", "DeepSeek (DeepSeek-V3, R1, coder — direct API)"), ProviderEntry("xai", "xAI", "xAI (Grok models — direct API)"), ProviderEntry("zai", "Z.AI / GLM", "Z.AI / GLM (Zhipu AI direct API)"), @@ -752,6 +787,8 @@ _PROVIDER_ALIASES = { "z-ai": "zai", "z.ai": "zai", "zhipu": "zai", + "payperq": "ppq", + "ppq.ai": "ppq", "github": "copilot", "github-copilot": "copilot", "github-models": "copilot", @@ -1748,6 +1785,20 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False) return live except Exception: pass + if normalized == "ppq": + # Try live PPQ /v1/models endpoint + try: + from hermes_cli.auth import resolve_api_key_provider_credentials + + creds = resolve_api_key_provider_credentials("ppq") + api_key = str(creds.get("api_key") or "").strip() + base_url = str(creds.get("base_url") or "").strip() + if api_key and base_url: + live = fetch_api_models(api_key, base_url) + if live: + return live + except Exception: + pass if normalized == "anthropic": live = _fetch_anthropic_models() if live: diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index f65ceac7a..dafa6c8a8 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -90,6 +90,13 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = { extra_env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"), base_url_env_var="GLM_BASE_URL", ), + "ppq": HermesOverlay( + transport="openai_chat", + is_aggregator=True, + extra_env_vars=("PPQ_API_KEY",), + base_url_override="https://api.ppq.ai", + base_url_env_var="PPQ_BASE_URL", + ), "kimi-for-coding": HermesOverlay( transport="openai_chat", base_url_env_var="KIMI_BASE_URL", @@ -203,6 +210,10 @@ ALIASES: Dict[str, str] = { "z.ai": "zai", "zhipu": "zai", + # ppq + "payperq": "ppq", + "ppq.ai": "ppq", + # xai "x-ai": "xai", "x.ai": "xai", @@ -313,6 +324,7 @@ _LABEL_OVERRIDES: Dict[str, str] = { "copilot-acp": "GitHub Copilot ACP", "stepfun": "StepFun Step Plan", "xiaomi": "Xiaomi MiMo", + "ppq": "PPQ (PayPerQ)", "local": "Local endpoint", "bedrock": "AWS Bedrock", "ollama-cloud": "Ollama Cloud", diff --git a/tests/hermes_cli/test_api_key_providers.py b/tests/hermes_cli/test_api_key_providers.py index e8f181fa4..b6a853963 100644 --- a/tests/hermes_cli/test_api_key_providers.py +++ b/tests/hermes_cli/test_api_key_providers.py @@ -33,6 +33,7 @@ class TestProviderRegistry: ("copilot-acp", "GitHub Copilot ACP", "external_process"), ("copilot", "GitHub Copilot", "api_key"), ("huggingface", "Hugging Face", "api_key"), + ("ppq", "PPQ (PayPerQ)", "api_key"), ("zai", "Z.AI / GLM", "api_key"), ("xai", "xAI", "api_key"), ("nvidia", "NVIDIA NIM", "api_key"), @@ -67,6 +68,12 @@ class TestProviderRegistry: assert pconfig.base_url_env_var == "NVIDIA_BASE_URL" assert pconfig.inference_base_url == "https://integrate.api.nvidia.com/v1" + def test_ppq_env_vars(self): + pconfig = PROVIDER_REGISTRY["ppq"] + assert pconfig.api_key_env_vars == ("PPQ_API_KEY",) + assert pconfig.base_url_env_var == "PPQ_BASE_URL" + assert pconfig.inference_base_url == "https://api.ppq.ai" + def test_copilot_env_vars(self): pconfig = PROVIDER_REGISTRY["copilot"] assert pconfig.api_key_env_vars == ("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN") @@ -138,6 +145,7 @@ class TestProviderRegistry: PROVIDER_ENV_VARS = ( "OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", + "PPQ_API_KEY", "GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY", "KIMI_API_KEY", "KIMI_BASE_URL", "STEPFUN_API_KEY", "STEPFUN_BASE_URL", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", diff --git a/tests/hermes_cli/test_model_validation.py b/tests/hermes_cli/test_model_validation.py index 80c7d2502..db8b77d53 100644 --- a/tests/hermes_cli/test_model_validation.py +++ b/tests/hermes_cli/test_model_validation.py @@ -157,6 +157,8 @@ class TestNormalizeProvider: def test_known_aliases(self): assert normalize_provider("glm") == "zai" + assert normalize_provider("payperq") == "ppq" + assert normalize_provider("ppq.ai") == "ppq" assert normalize_provider("kimi") == "kimi-coding" assert normalize_provider("moonshot") == "kimi-coding" assert normalize_provider("step") == "stepfun" @@ -173,6 +175,7 @@ class TestProviderLabel: assert provider_label("stepfun") == "StepFun Step Plan" assert provider_label("copilot") == "GitHub Copilot" assert provider_label("copilot-acp") == "GitHub Copilot ACP" + assert provider_label("ppq") == "PPQ (PayPerQ)" assert provider_label("auto") == "Auto" def test_unknown_provider_preserves_original_name(self): @@ -210,6 +213,23 @@ class TestProviderModelIds: ): assert provider_model_ids("stepfun") == ["step-3.5-flash", "step-3-agent-lite"] + def test_ppq_prefers_live_catalog(self): + with patch("hermes_cli.auth.resolve_api_key_provider_credentials", return_value={ + "api_key": "***", "base_url": "https://api.ppq.ai", + }), patch("hermes_cli.models.fetch_api_models", return_value=[ + "claude-sonnet-4.6", "openai/gpt-5.4", + ]): + assert provider_model_ids("ppq") == ["claude-sonnet-4.6", "openai/gpt-5.4"] + + def test_ppq_falls_back_to_curated(self): + """When live fetch fails, PPQ falls back to the curated _PROVIDER_MODELS list.""" + with patch("hermes_cli.auth.resolve_api_key_provider_credentials", return_value={ + "api_key": "", "base_url": "https://api.ppq.ai", + }): + ids = provider_model_ids("ppq") + assert len(ids) > 0 + assert "claude-opus-4.7" in ids + 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"]):