fix(runtime): fail fast for bare custom credentials

This commit is contained in:
KoNit-K
2026-09-15 16:50:50 +08:00
committed by Teknium
parent c1bbcf9712
commit 879d65ec78
2 changed files with 69 additions and 1 deletions
+25 -1
View File
@@ -860,7 +860,31 @@ def resolve_runtime_provider(*, requested: Optional[str] = None, explicit_api_ke
OpenCode Zen/Go where different models route through different API surfaces)."""
requested_provider = resolve_requested_provider(requested)
_raise_if_provider_disabled(requested_provider)
return next(r for r in _ladder_rungs(requested_provider, explicit_api_key, explicit_base_url, target_model) if r)
runtime = next(r for r in _ladder_rungs(requested_provider, explicit_api_key, explicit_base_url, target_model) if r)
_raise_for_credentialless_bare_custom(requested_provider, runtime)
return runtime
def _raise_for_credentialless_bare_custom(requested_provider: str, runtime: Dict[str, Any]) -> None:
"""Reject a stale bare ``custom`` placeholder before agent construction.
Named custom providers and local OpenAI-compatible servers retain their existing resolution
paths. A bare placeholder that reaches a remote endpoint without a credential, however, would
otherwise fail later with the unrelated ``No LLM provider configured`` diagnostic.
"""
if requested_provider != "custom":
return
api_key = runtime.get("api_key")
if callable(api_key) or has_usable_secret(api_key):
return
if _loopback_hostname(base_url_hostname(str(runtime.get("base_url") or ""))):
return
raise AuthError(
"provider 'custom' resolved without usable credentials. If this is a named custom provider, "
"use its real name (see providers: in config.yaml).",
provider=requested_provider,
code="missing_api_key",
)
def _ladder_rungs(requested_provider, explicit_api_key, explicit_base_url, target_model):
@@ -758,6 +758,50 @@ def test_bare_custom_resolves_providers_dict_entry_named_custom(monkeypatch):
assert resolved["requested_provider"] == "custom"
def test_bare_custom_without_credentials_for_remote_endpoint_fails_fast(monkeypatch):
"""A stale bare placeholder must name the bad request at resolution time."""
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setattr(
rp,
"load_config",
lambda: {
"providers": {
"volcano": {
"api": "https://ark.example.com/v1",
"key_env": "ARK_API_KEY",
}
}
},
)
monkeypatch.setattr(rp, "_get_model_config", lambda: {"provider": "volcano"})
with pytest.raises(rp.AuthError, match="provider 'custom'.*credentials.*real name") as error:
rp.resolve_runtime_provider(requested="custom")
assert error.value.provider == "custom"
assert error.value.code == "missing_api_key"
def test_bare_custom_without_credentials_keeps_loopback_noauth(monkeypatch):
monkeypatch.setattr(
rp,
"load_config",
lambda: {
"providers": {
"custom": {
"api": "http://localhost:11434/v1",
}
}
},
)
resolved = rp.resolve_runtime_provider(requested="custom")
assert resolved["base_url"] == "http://localhost:11434/v1"
assert resolved["api_key"] == "no-key-required"
def test_named_custom_provider_same_url_uses_matching_key_env_and_api_mode(monkeypatch):