fix(cli): honor config base_url mirror for explicit openrouter provider

When config.yaml sets `model.provider: openrouter` together with a
`model.base_url` mirror/proxy, an explicit `--provider openrouter`
request ignored the mirror and sent traffic to the public OpenRouter
endpoint: the config base_url was only trusted for auto/custom, the
credential pool was still consulted (so a pooled key won over the
mirror), and even when the mirror URL was used its host failed the
openrouter.ai match so OPENROUTER_API_KEY was not selected for it.

Trust the config base_url for the explicit openrouter case, treat that
mirror as an OpenRouter context for key selection, and bypass the pool
like the other custom-endpoint cases already do.

Fixes #10622
This commit is contained in:
JackJin
2026-09-11 16:44:48 +08:00
committed by Teknium
parent b146cf1d0e
commit 63f1016bea
3 changed files with 78 additions and 5 deletions
+2 -1
View File
@@ -491,7 +491,8 @@ def _openrouter_should_use_pool(requested_provider, model_cfg, explicit_api_key,
"""OpenRouter pool only for a plain openrouter/auto request with no custom endpoint or override."""
cfg_base_url = str(model_cfg.get("base_url") or "").strip()
env_base_urls = _getenv("OPENAI_BASE_URL", "").strip() or _getenv("OPENROUTER_BASE_URL", "").strip()
has_custom_endpoint = bool(explicit_base_url or env_base_urls or (cfg_base_url and _cfg_provider(model_cfg) in {"auto", "custom"}))
has_custom_endpoint = bool(explicit_base_url or env_base_urls
or (cfg_base_url and _cfg_provider(model_cfg) in {"auto", "custom", "openrouter"}))
return requested_provider in {"openrouter", "auto"} and not has_custom_endpoint and not bool(explicit_api_key or explicit_base_url)
+11 -4
View File
@@ -131,6 +131,8 @@ def _resolve_openrouter_runtime(
use_config_base_url = bool(cfg_base_url.strip()) and not explicit_base_url and (
(requested_norm == "auto" and cfg_provider in ("", "auto"))
or (requested_norm == "custom" and rp._config_base_url_trustworthy_for_bare_custom(cfg_base_url, cfg_provider))
# provider: openrouter + base_url in config.yaml is a deliberate mirror/proxy (#10622).
or (requested_norm == "openrouter" and cfg_provider == "openrouter")
)
base_url = ((explicit_base_url or "").strip() or env_custom_base_url or (cfg_base_url.strip() if use_config_base_url else "")
or env_openrouter_base_url or OPENROUTER_BASE_URL).rstrip("/")
@@ -138,11 +140,16 @@ def _resolve_openrouter_runtime(
# prefer OPENROUTER_API_KEY (issue #289). When hitting a custom endpoint (e.g. Z.ai, local LLM), prefer
# OPENAI_API_KEY so the OpenRouter key doesn't leak to an unrelated provider (issues #420, #560).
is_openrouter_url = base_url_host_matches(base_url, "openrouter.ai")
# Explicitly-configured OpenRouter mirrors (OPENROUTER_BASE_URL + provider=openrouter) still
# count as OpenRouter for key selection.
# Explicitly-configured OpenRouter mirrors (OPENROUTER_BASE_URL, or a config.yaml base_url under
# provider: openrouter) still count as OpenRouter for key selection — otherwise the mirror's host
# fails the openrouter.ai match and the generic custom-endpoint branch never selects
# OPENROUTER_API_KEY for it (#10622).
is_openrouter_context = is_openrouter_url or (
requested_norm == "openrouter" and (env_openrouter_base_url or base_url == env_openrouter_base_url)
and base_url == (env_openrouter_base_url or "").rstrip("/")
requested_norm == "openrouter" and (
(use_config_base_url and cfg_provider == "openrouter")
or ((env_openrouter_base_url or base_url == env_openrouter_base_url)
and base_url == (env_openrouter_base_url or "").rstrip("/"))
)
)
if is_openrouter_context:
candidates = [explicit_api_key, rp._getenv("OPENROUTER_API_KEY"), rp._getenv("OPENAI_API_KEY")]
@@ -868,6 +868,71 @@ def test_explicit_openrouter_skips_openai_base_url(monkeypatch):
assert resolved["api_key"] == "or-test-key"
def test_explicit_openrouter_honors_config_base_url_mirror(monkeypatch):
"""requested='openrouter' + config provider='openrouter' + config base_url must
resolve to that mirror AND still select OPENROUTER_API_KEY for it — a
config-sourced mirror is an OpenRouter context for credential selection just
like the OPENROUTER_BASE_URL env path already is. Regression test for #10622."""
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter")
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {
"provider": "openrouter",
"base_url": "https://openrouter-mirror.example.com/api/v1",
},
)
monkeypatch.setattr(rp, "load_pool", lambda _provider: SimpleNamespace(has_credentials=lambda: False))
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("OPENROUTER_API_KEY", "router-key")
resolved = rp.resolve_runtime_provider(requested="openrouter")
assert resolved["provider"] == "openrouter"
assert resolved["base_url"] == "https://openrouter-mirror.example.com/api/v1"
assert resolved["api_key"] == "router-key"
def test_explicit_openrouter_config_mirror_bypasses_pool(monkeypatch):
"""A config.yaml mirror under provider='openrouter' is a custom endpoint: the
OpenRouter credential pool must be skipped rather than silently routing the
request to openrouter.ai with a pooled key (#10622)."""
class _Entry:
access_token = "pool-key"
source = "manual"
base_url = "https://openrouter.ai/api/v1"
class _Pool:
def has_credentials(self):
return True
def select(self):
return _Entry()
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter")
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {
"provider": "openrouter",
"base_url": "https://openrouter-mirror.example.com/api/v1",
},
)
monkeypatch.setattr(rp, "load_pool", lambda _provider: _Pool())
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("OPENROUTER_API_KEY", "router-key")
resolved = rp.resolve_runtime_provider(requested="openrouter")
assert resolved["base_url"] == "https://openrouter-mirror.example.com/api/v1"
assert resolved["api_key"] == "router-key"
assert resolved.get("credential_pool") is None