mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-14 04:02:26 +00:00
feat(providers): add PPQ (PayPerQ) as aggregator provider
Add PPQ (PayPerQ) as a first-class provider with aggregator support, following the same pattern as OpenRouter. - ProviderConfig in auth.py with PPQ_API_KEY / PPQ_BASE_URL env vars - HermesOverlay in providers.py with is_aggregator=True - Dynamic model fetching via /v1/models (no static model list) - Aliases: payperq, ppq.ai -> ppq - Label override: PPQ (PayPerQ) - Full test coverage: registry, aliases, labels, live catalog, fallback
This commit is contained in:
parent
9de555f3e3
commit
df3b55e4f0
5 changed files with 100 additions and 0 deletions
|
|
@ -159,6 +159,14 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
|
||||||
api_key_env_vars=("GOOGLE_API_KEY", "GEMINI_API_KEY"),
|
api_key_env_vars=("GOOGLE_API_KEY", "GEMINI_API_KEY"),
|
||||||
base_url_env_var="GEMINI_BASE_URL",
|
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(
|
"zai": ProviderConfig(
|
||||||
id="zai",
|
id="zai",
|
||||||
name="Z.AI / GLM",
|
name="Z.AI / GLM",
|
||||||
|
|
@ -1028,6 +1036,7 @@ def resolve_provider(
|
||||||
# Normalize provider aliases
|
# Normalize provider aliases
|
||||||
_PROVIDER_ALIASES = {
|
_PROVIDER_ALIASES = {
|
||||||
"glm": "zai", "z-ai": "zai", "z.ai": "zai", "zhipu": "zai",
|
"glm": "zai", "z-ai": "zai", "z.ai": "zai", "zhipu": "zai",
|
||||||
|
"payperq": "ppq", "ppq.ai": "ppq",
|
||||||
"google": "gemini", "google-gemini": "gemini", "google-ai-studio": "gemini",
|
"google": "gemini", "google-gemini": "gemini", "google-ai-studio": "gemini",
|
||||||
"x-ai": "xai", "x.ai": "xai", "grok": "xai",
|
"x-ai": "xai", "x.ai": "xai", "grok": "xai",
|
||||||
"kimi": "kimi-coding", "kimi-for-coding": "kimi-coding", "moonshot": "kimi-coding",
|
"kimi": "kimi-coding", "kimi-for-coding": "kimi-coding", "moonshot": "kimi-coding",
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,40 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
||||||
"gemini-2.5-pro",
|
"gemini-2.5-pro",
|
||||||
"grok-code-fast-1",
|
"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": [
|
||||||
"gemini-3.1-pro-preview",
|
"gemini-3.1-pro-preview",
|
||||||
"gemini-3-pro-preview",
|
"gemini-3-pro-preview",
|
||||||
|
|
@ -710,6 +744,7 @@ CANONICAL_PROVIDERS: list[ProviderEntry] = [
|
||||||
ProviderEntry("huggingface", "Hugging Face", "Hugging Face Inference Providers (20+ open models)"),
|
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("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("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("deepseek", "DeepSeek", "DeepSeek (DeepSeek-V3, R1, coder — direct API)"),
|
||||||
ProviderEntry("xai", "xAI", "xAI (Grok models — direct API)"),
|
ProviderEntry("xai", "xAI", "xAI (Grok models — direct API)"),
|
||||||
ProviderEntry("zai", "Z.AI / GLM", "Z.AI / GLM (Zhipu AI direct API)"),
|
ProviderEntry("zai", "Z.AI / GLM", "Z.AI / GLM (Zhipu AI direct API)"),
|
||||||
|
|
@ -737,6 +772,8 @@ _PROVIDER_ALIASES = {
|
||||||
"z-ai": "zai",
|
"z-ai": "zai",
|
||||||
"z.ai": "zai",
|
"z.ai": "zai",
|
||||||
"zhipu": "zai",
|
"zhipu": "zai",
|
||||||
|
"payperq": "ppq",
|
||||||
|
"ppq.ai": "ppq",
|
||||||
"github": "copilot",
|
"github": "copilot",
|
||||||
"github-copilot": "copilot",
|
"github-copilot": "copilot",
|
||||||
"github-models": "copilot",
|
"github-models": "copilot",
|
||||||
|
|
@ -1733,6 +1770,20 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
|
||||||
return live
|
return live
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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":
|
if normalized == "anthropic":
|
||||||
live = _fetch_anthropic_models()
|
live = _fetch_anthropic_models()
|
||||||
if live:
|
if live:
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,13 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = {
|
||||||
extra_env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"),
|
extra_env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"),
|
||||||
base_url_env_var="GLM_BASE_URL",
|
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(
|
"kimi-for-coding": HermesOverlay(
|
||||||
transport="openai_chat",
|
transport="openai_chat",
|
||||||
base_url_env_var="KIMI_BASE_URL",
|
base_url_env_var="KIMI_BASE_URL",
|
||||||
|
|
@ -203,6 +210,10 @@ ALIASES: Dict[str, str] = {
|
||||||
"z.ai": "zai",
|
"z.ai": "zai",
|
||||||
"zhipu": "zai",
|
"zhipu": "zai",
|
||||||
|
|
||||||
|
# ppq
|
||||||
|
"payperq": "ppq",
|
||||||
|
"ppq.ai": "ppq",
|
||||||
|
|
||||||
# xai
|
# xai
|
||||||
"x-ai": "xai",
|
"x-ai": "xai",
|
||||||
"x.ai": "xai",
|
"x.ai": "xai",
|
||||||
|
|
@ -313,6 +324,7 @@ _LABEL_OVERRIDES: Dict[str, str] = {
|
||||||
"copilot-acp": "GitHub Copilot ACP",
|
"copilot-acp": "GitHub Copilot ACP",
|
||||||
"stepfun": "StepFun Step Plan",
|
"stepfun": "StepFun Step Plan",
|
||||||
"xiaomi": "Xiaomi MiMo",
|
"xiaomi": "Xiaomi MiMo",
|
||||||
|
"ppq": "PPQ (PayPerQ)",
|
||||||
"local": "Local endpoint",
|
"local": "Local endpoint",
|
||||||
"bedrock": "AWS Bedrock",
|
"bedrock": "AWS Bedrock",
|
||||||
"ollama-cloud": "Ollama Cloud",
|
"ollama-cloud": "Ollama Cloud",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ class TestProviderRegistry:
|
||||||
("copilot-acp", "GitHub Copilot ACP", "external_process"),
|
("copilot-acp", "GitHub Copilot ACP", "external_process"),
|
||||||
("copilot", "GitHub Copilot", "api_key"),
|
("copilot", "GitHub Copilot", "api_key"),
|
||||||
("huggingface", "Hugging Face", "api_key"),
|
("huggingface", "Hugging Face", "api_key"),
|
||||||
|
("ppq", "PPQ (PayPerQ)", "api_key"),
|
||||||
("zai", "Z.AI / GLM", "api_key"),
|
("zai", "Z.AI / GLM", "api_key"),
|
||||||
("xai", "xAI", "api_key"),
|
("xai", "xAI", "api_key"),
|
||||||
("nvidia", "NVIDIA NIM", "api_key"),
|
("nvidia", "NVIDIA NIM", "api_key"),
|
||||||
|
|
@ -67,6 +68,12 @@ class TestProviderRegistry:
|
||||||
assert pconfig.base_url_env_var == "NVIDIA_BASE_URL"
|
assert pconfig.base_url_env_var == "NVIDIA_BASE_URL"
|
||||||
assert pconfig.inference_base_url == "https://integrate.api.nvidia.com/v1"
|
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):
|
def test_copilot_env_vars(self):
|
||||||
pconfig = PROVIDER_REGISTRY["copilot"]
|
pconfig = PROVIDER_REGISTRY["copilot"]
|
||||||
assert pconfig.api_key_env_vars == ("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN")
|
assert pconfig.api_key_env_vars == ("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN")
|
||||||
|
|
@ -138,6 +145,7 @@ class TestProviderRegistry:
|
||||||
PROVIDER_ENV_VARS = (
|
PROVIDER_ENV_VARS = (
|
||||||
"OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN",
|
"OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN",
|
||||||
"CLAUDE_CODE_OAUTH_TOKEN",
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
|
"PPQ_API_KEY",
|
||||||
"GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY",
|
"GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY",
|
||||||
"KIMI_API_KEY", "KIMI_BASE_URL", "STEPFUN_API_KEY", "STEPFUN_BASE_URL",
|
"KIMI_API_KEY", "KIMI_BASE_URL", "STEPFUN_API_KEY", "STEPFUN_BASE_URL",
|
||||||
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,8 @@ class TestNormalizeProvider:
|
||||||
|
|
||||||
def test_known_aliases(self):
|
def test_known_aliases(self):
|
||||||
assert normalize_provider("glm") == "zai"
|
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("kimi") == "kimi-coding"
|
||||||
assert normalize_provider("moonshot") == "kimi-coding"
|
assert normalize_provider("moonshot") == "kimi-coding"
|
||||||
assert normalize_provider("step") == "stepfun"
|
assert normalize_provider("step") == "stepfun"
|
||||||
|
|
@ -173,6 +175,7 @@ class TestProviderLabel:
|
||||||
assert provider_label("stepfun") == "StepFun Step Plan"
|
assert provider_label("stepfun") == "StepFun Step Plan"
|
||||||
assert provider_label("copilot") == "GitHub Copilot"
|
assert provider_label("copilot") == "GitHub Copilot"
|
||||||
assert provider_label("copilot-acp") == "GitHub Copilot ACP"
|
assert provider_label("copilot-acp") == "GitHub Copilot ACP"
|
||||||
|
assert provider_label("ppq") == "PPQ (PayPerQ)"
|
||||||
assert provider_label("auto") == "Auto"
|
assert provider_label("auto") == "Auto"
|
||||||
|
|
||||||
def test_unknown_provider_preserves_original_name(self):
|
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"]
|
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):
|
def test_copilot_prefers_live_catalog(self):
|
||||||
with patch("hermes_cli.auth.resolve_api_key_provider_credentials", return_value={"api_key": "gh-token"}), \
|
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"]):
|
patch("hermes_cli.models._fetch_github_models", return_value=["gpt-5.4", "claude-sonnet-4.6"]):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue