diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index 9115c89f4d..06b3a6dc26 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -115,39 +115,25 @@ class ProviderDef: # -- Aliases: human-friendly / legacy names grouped by canonical (models.dev where possible) id; # ``ALIASES`` is the inverted lookup table. --------------------------------------------------- _ALIAS_GROUPS: Dict[str, Tuple[str, ...]] = { - "openrouter": ("openai",), - "zai": ("glm", "z-ai", "z.ai", "zhipu"), - "xai": ("x-ai", "x.ai", "grok"), + "openrouter": ("openai",), "zai": ("glm", "z-ai", "z.ai", "zhipu"), "xai": ("x-ai", "x.ai", "grok"), "xai-oauth": ("grok-oauth", "xai-oauth", "x-ai-oauth", "xai-grok-oauth"), "nvidia": ("nim", "nvidia-nim", "build-nvidia", "nemotron"), "kimi-for-coding": ("kimi", "kimi-coding", "kimi-coding-cn", "moonshot"), - "stepfun": ("step", "stepfun-coding-plan"), - "minimax-cn": ("minimax-china", "minimax_cn"), - "anthropic": ("claude", "claude-code"), - "github-copilot": ("copilot", "github"), - "copilot-acp": ("github-copilot-acp",), - "vercel": ("ai-gateway", "aigateway", "vercel-ai-gateway"), - "opencode": ("opencode-zen", "zen"), - "opencode-go": ("go", "opencode-go-sub"), - "opencode-free": ("free", "opencode_free"), - "kilo": ("kilocode", "kilo-code", "kilo-gateway"), - "deepseek": ("deep-seek",), - "alibaba": ("dashscope", "aliyun", "qwen", "alibaba-cloud"), + "stepfun": ("step", "stepfun-coding-plan"), "minimax-cn": ("minimax-china", "minimax_cn"), + "anthropic": ("claude", "claude-code"), "github-copilot": ("copilot", "github"), + "copilot-acp": ("github-copilot-acp",), "vercel": ("ai-gateway", "aigateway", "vercel-ai-gateway"), + "opencode": ("opencode-zen", "zen"), "opencode-go": ("go", "opencode-go-sub"), + "opencode-free": ("free", "opencode_free"), "kilo": ("kilocode", "kilo-code", "kilo-gateway"), + "deepseek": ("deep-seek",), "alibaba": ("dashscope", "aliyun", "qwen", "alibaba-cloud"), "alibaba-coding-plan": ("alibaba_coding", "alibaba-coding", "alibaba_coding_plan"), - "huggingface": ("hf", "hugging-face", "huggingface-hub"), - "novita": ("novita-ai", "novitaai"), - "xiaomi": ("mimo", "xiaomi-mimo"), - "tencent-tokenhub": ("tencent", "tokenhub", "tencent-cloud", "tencentmaas"), + "huggingface": ("hf", "hugging-face", "huggingface-hub"), "novita": ("novita-ai", "novitaai"), + "xiaomi": ("mimo", "xiaomi-mimo"), "tencent-tokenhub": ("tencent", "tokenhub", "tencent-cloud", "tencentmaas"), "tencent-tokenplan": ("tokenplan", "tencent-lkeap"), - "bedrock": ("aws", "aws-bedrock", "amazon-bedrock", "amazon"), - "arcee": ("arcee-ai", "arceeai"), - "gmi": ("gmi-cloud", "gmicloud"), - "fireworks": ("fireworks-ai", "fw"), - "upstage": ("solar",), + "bedrock": ("aws", "aws-bedrock", "amazon-bedrock", "amazon"), "arcee": ("arcee-ai", "arceeai"), + "gmi": ("gmi-cloud", "gmicloud"), "fireworks": ("fireworks-ai", "fw"), "upstage": ("solar",), "actual": ("actual-computer", "actualcomputer", "aci"), "nebius-token-factory": ("nebius", "nebius-tokenfactory", "nebius-tf", "token-factory", "tokenfactory"), - "lmstudio": ("lmstudio", "lm-studio", "lm_studio"), - "custom": ("ollama",), + "lmstudio": ("lmstudio", "lm-studio", "lm_studio"), "custom": ("ollama",), "local": ("vllm", "llamacpp", "llama.cpp", "llama-cpp"), } ALIASES: Dict[str, str] = {alias: canon for canon, aliases in _ALIAS_GROUPS.items() for alias in aliases} @@ -156,35 +142,20 @@ ALIASES: Dict[str, str] = {alias: canon for canon, aliases in _ALIAS_GROUPS.item # -- Display labels for providers not in the models.dev catalog --------------- _LABEL_OVERRIDES: Dict[str, str] = { - "moa": "Mixture of Agents", - "nous": "Nous Portal", - "openai-codex": "ChatGPT or Codex Subscription", - "copilot-acp": "GitHub Copilot ACP", - "stepfun": "StepFun Step Plan", - "xiaomi": "Xiaomi MiMo", - "gmi": "GMI Cloud", - "upstage": "Upstage Solar", - "actual": "Actual Computer", - "tencent-tokenhub": "Tencent TokenHub", - "nebius-token-factory": "Nebius Token Factory", - "tencent-tokenplan": "Tencent TokenPlan", - "lmstudio": "LM Studio", - "local": "Local endpoint", - "bedrock": "AWS Bedrock", - "vertex": "Google Vertex AI", - "ollama-cloud": "Ollama Cloud", - "xai-oauth": "xAI Grok OAuth (SuperGrok / Premium+)", - "opencode-free": "OpenCode Free", + "moa": "Mixture of Agents", "nous": "Nous Portal", "openai-codex": "ChatGPT or Codex Subscription", + "copilot-acp": "GitHub Copilot ACP", "stepfun": "StepFun Step Plan", "xiaomi": "Xiaomi MiMo", "gmi": "GMI Cloud", + "upstage": "Upstage Solar", "actual": "Actual Computer", "tencent-tokenhub": "Tencent TokenHub", + "nebius-token-factory": "Nebius Token Factory", "tencent-tokenplan": "Tencent TokenPlan", "lmstudio": "LM Studio", + "local": "Local endpoint", "bedrock": "AWS Bedrock", "vertex": "Google Vertex AI", "ollama-cloud": "Ollama Cloud", + "xai-oauth": "xAI Grok OAuth (SuperGrok / Premium+)", "opencode-free": "OpenCode Free", } # -- Transport → API mode mapping --------------------------------------------- TRANSPORT_TO_API_MODE: Dict[str, str] = { - "openai_chat": "chat_completions", - "anthropic_messages": "anthropic_messages", - "codex_responses": "codex_responses", - "bedrock_converse": "bedrock_converse", + "openai_chat": "chat_completions", "anthropic_messages": "anthropic_messages", + "codex_responses": "codex_responses", "bedrock_converse": "bedrock_converse", } diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index 47c5cd23ee..3784c69d68 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -594,14 +594,20 @@ def _explicit_anthropic(requested_provider, model_cfg, api_key, base_url, target return _runtime("anthropic", "anthropic_messages", base_url, api_key, source="explicit", requested_provider=requested_provider) +def _creds_fallback(api_key, explicit_base_url, base_url, expiry, expiry_key, resolve): + """When no explicit key was given, take api_key / expiry / base_url from stored credentials + (an explicit --base-url still wins over the stored one).""" + if api_key: + return api_key, base_url, expiry + creds = resolve() + return creds.get("api_key", ""), explicit_base_url or creds.get("base_url", "").rstrip("/") or base_url, creds.get(expiry_key) + + def _explicit_codex(requested_provider, model_cfg, api_key, explicit_base_url, target_model): - base_url = explicit_base_url or DEFAULT_CODEX_BASE_URL - last_refresh = None - if not api_key: - creds = resolve_codex_runtime_credentials() - api_key = creds.get("api_key", "") - last_refresh = creds.get("last_refresh") - base_url = explicit_base_url or creds.get("base_url", "").rstrip("/") or base_url + api_key, base_url, last_refresh = _creds_fallback( + api_key, explicit_base_url, explicit_base_url or DEFAULT_CODEX_BASE_URL, None, "last_refresh", + resolve_codex_runtime_credentials, + ) return _runtime( "openai-codex", "codex_responses", base_url, api_key, source="explicit", last_refresh=last_refresh, requested_provider=requested_provider, @@ -620,12 +626,10 @@ def _explicit_nous(requested_provider, model_cfg, api_key, explicit_base_url, ta api_key = api_key or ( str(state.get("agent_key") or "").strip() if _agent_key_is_usable(state, _nous_min_key_ttl()) else "" ) - expires_at = state.get("agent_key_expires_at") or state.get("expires_at") - if not api_key: - creds = _resolve_nous_creds() - api_key = creds.get("api_key", "") - expires_at = creds.get("expires_at") - base_url = explicit_base_url or creds.get("base_url", "").rstrip("/") or base_url + api_key, base_url, expires_at = _creds_fallback( + api_key, explicit_base_url, base_url, state.get("agent_key_expires_at") or state.get("expires_at"), "expires_at", + _resolve_nous_creds, + ) return _runtime( "nous", nous_api_mode(_effective_model(model_cfg, target_model)), base_url, api_key, source="explicit", expires_at=expires_at, requested_provider=requested_provider, @@ -638,6 +642,13 @@ def _explicit_azure_foundry(requested_provider, model_cfg, api_key, base_url, ta ) +def _actual_local_key(provider: str, api_key: str, base_url: str) -> str: + """Actual Computer's loopback daemon speaks a no-auth local API — substitute the placeholder key.""" + if provider == "actual" and not api_key and is_actual_local_base_url(base_url): + return ACTUAL_LOCAL_NOAUTH_PLACEHOLDER + return api_key + + def _explicit_api_key_provider(provider, pconfig, requested_provider, model_cfg, api_key, base_url, target_model): if not base_url: if provider in {"kimi-coding", "kimi-coding-cn"}: @@ -657,8 +668,7 @@ def _explicit_api_key_provider(provider, pconfig, requested_provider, model_cfg, api_mode = _api_key_provider_api_mode( provider, model_cfg, api_key, base_url, target_model or model_cfg.get("default", ""), opencode_by_model=False ) - if provider == "actual" and not api_key and is_actual_local_base_url(base_url): - api_key = ACTUAL_LOCAL_NOAUTH_PLACEHOLDER + api_key = _actual_local_key(provider, api_key, base_url) return _runtime(provider, api_mode, base_url.rstrip("/"), api_key, source="explicit", requested_provider=requested_provider) @@ -816,9 +826,7 @@ def _api_key_provider_runtime(provider, pconfig, requested_provider, model_cfg, provider, model_cfg, creds.get("api_key", ""), base_url, target_model or model_cfg.get("default", ""), opencode_by_model=True ) base_url = _finalize_base_url(provider, api_mode, base_url) - api_key = creds.get("api_key", "") - if provider == "actual" and not api_key and is_actual_local_base_url(base_url): - api_key = ACTUAL_LOCAL_NOAUTH_PLACEHOLDER + api_key = _actual_local_key(provider, creds.get("api_key", ""), base_url) return _runtime(provider, api_mode, base_url, api_key, source=creds.get("source", "env"), requested_provider=requested_provider) @@ -963,37 +971,33 @@ def resolve_runtime_provider( if runtime: return runtime provider = resolve_provider(requested_provider, explicit_api_key=explicit_api_key, explicit_base_url=explicit_base_url) + return next(r for r in _provider_rungs(provider, requested_provider, explicit_api_key, explicit_base_url, target_model) if r) + + +def _provider_rungs(provider, requested_provider, explicit_api_key, explicit_base_url, target_model): + """Rungs 5-8 of the ladder, yielded lazily so each is evaluated only when the previous one + returned nothing; the last rung (OpenRouter / bare-custom fallback) always yields a runtime.""" model_cfg = _get_model_config() - runtime = _opencode_free_runtime(provider, requested_provider, model_cfg, target_model) - if runtime is not None: - return runtime - runtime = _resolve_explicit_runtime( + yield _opencode_free_runtime(provider, requested_provider, model_cfg, target_model) + yield _resolve_explicit_runtime( provider=provider, requested_provider=requested_provider, model_cfg=model_cfg, explicit_api_key=explicit_api_key, explicit_base_url=explicit_base_url, target_model=target_model, ) - if runtime: - return runtime - runtime = _resolve_from_pool(provider, requested_provider, model_cfg, explicit_api_key, explicit_base_url, target_model) - if runtime: - return runtime + yield _resolve_from_pool(provider, requested_provider, model_cfg, explicit_api_key, explicit_base_url, target_model) if provider in _OAUTH_RUNTIME_PROVIDERS: - runtime = _resolve_oauth_runtime(provider, requested_provider, model_cfg, target_model) - if runtime: - return runtime + yield _resolve_oauth_runtime(provider, requested_provider, model_cfg, target_model) if provider == "minimax-oauth": - runtime = _minimax_oauth_runtime(provider, requested_provider) - if runtime: - return runtime + yield _minimax_oauth_runtime(provider, requested_provider) if _is_external_process_provider(provider): - return _resolve_external_process_runtime(provider, requested_provider) + yield _resolve_external_process_runtime(provider, requested_provider) if provider == "anthropic": - return _anthropic_env_runtime(requested_provider, model_cfg) + yield _anthropic_env_runtime(requested_provider, model_cfg) if provider == "bedrock": - return _resolve_bedrock_runtime(requested_provider, model_cfg, target_model) + yield _resolve_bedrock_runtime(requested_provider, model_cfg, target_model) pconfig = PROVIDER_REGISTRY.get(provider) if pconfig and pconfig.auth_type == "api_key": - return _api_key_provider_runtime(provider, pconfig, requested_provider, model_cfg, target_model) - return _openrouter_fallback(requested_provider, explicit_api_key, explicit_base_url) + yield _api_key_provider_runtime(provider, pconfig, requested_provider, model_cfg, target_model) + yield _openrouter_fallback(requested_provider, explicit_api_key, explicit_base_url) def format_runtime_provider_error(error: Exception) -> str: