fix(aux-client): resolve named-provider credential when base_url overrides endpoint (#16290)#16308
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes auxiliary client credential resolution when a named provider is configured with a custom base_url but no explicit api_key, ensuring the named provider’s credential pool is consulted instead of silently falling back to OPENAI_API_KEY / "no-key-required".
Changes:
- Update
_resolve_task_provider_model()to attempt a credential lookup for the configured named provider whencfg_base_urlis set andapi_keyis blank. - Add regression tests covering named provider +
base_urlcredential resolution, precedence of explicit config keys, and behavior for unknown/custom providers.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
agent/auxiliary_client.py |
Adds conditional credential lookup for named providers when a task-level base_url override is present. |
tests/agent/test_aux_vision_named_provider_key.py |
Introduces regression tests for the named-provider + custom base_url credential resolution behavior. |
Comments suppressed due to low confidence (1)
tests/agent/test_aux_vision_named_provider_key.py:137
- The new tests cover API-key providers and unknown/custom providers, but they don't cover the case where
cfg_provideris inPROVIDER_REGISTRYyet not an API-key provider (e.g.nous,openai-codex,copilot-acp). Adding a test that setsproviderto one of these withbase_urlset and asserts_resolve_task_provider_model()does not raise (and leavesapi_keyasNone) would protect against regressions in the new lookup logic.
def test_fix_applies_to_non_vision_tasks_too(self, monkeypatch):
"""Same fix applies to compression/review/other auxiliary tasks."""
from agent.auxiliary_client import _resolve_task_provider_model
monkeypatch.setattr(
"agent.auxiliary_client._get_auxiliary_task_config",
lambda task: _make_task_config("deepseek", "https://api.deepseek.com/v1") if task == "compression" else {},
)
fake_creds = {"api_key": "deepseek-key-456"}
with patch(
"hermes_cli.auth.resolve_api_key_provider_credentials",
return_value=fake_creds,
):
provider, model, base_url, api_key, api_mode = _resolve_task_provider_model("compression")
assert provider == "custom"
assert base_url == "https://api.deepseek.com/v1"
assert api_key == "deepseek-key-456"
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| creds = resolve_api_key_provider_credentials(cfg_provider) | ||
| resolved_key = str(creds.get("api_key", "")).strip() or None |
There was a problem hiding this comment.
cfg_provider in PROVIDER_REGISTRY is not sufficient before calling resolve_api_key_provider_credentials(): that function raises AuthError for providers whose auth_type is not api_key (e.g. nous, openai-codex, copilot-acp). With auxiliary.<task>.base_url set and api_key blank, this code path can raise and break auxiliary routing. Gate the call on PROVIDER_REGISTRY[cfg_provider].auth_type == "api_key" and/or catch AuthError (or a broader exception) so non-api-key providers simply leave resolved_key as None.
| creds = resolve_api_key_provider_credentials(cfg_provider) | |
| resolved_key = str(creds.get("api_key", "")).strip() or None | |
| provider_config = PROVIDER_REGISTRY[cfg_provider] | |
| if getattr(provider_config, "auth_type", None) == "api_key": | |
| try: | |
| creds = resolve_api_key_provider_credentials(cfg_provider) | |
| resolved_key = str(creds.get("api_key", "")).strip() or None | |
| except Exception: | |
| pass |
| if cfg_provider in PROVIDER_REGISTRY: | ||
| creds = resolve_api_key_provider_credentials(cfg_provider) |
There was a problem hiding this comment.
This lookup is case-sensitive (if cfg_provider in PROVIDER_REGISTRY:), but cfg_provider is taken verbatim from config (only .strip()). If a user sets provider: ZAI (or any non-lowercase variant), downstream resolve_provider_client() would normalize it, but this pre-lookup will fail and resolved_key stays None, reintroducing the 401. Consider normalizing to lowercase for the registry membership check and credential resolution call.
| if cfg_provider in PROVIDER_REGISTRY: | |
| creds = resolve_api_key_provider_credentials(cfg_provider) | |
| registry_provider = cfg_provider.lower() | |
| if registry_provider in PROVIDER_REGISTRY: | |
| creds = resolve_api_key_provider_credentials(registry_provider) |
|
@copilot Both findings addressed in commit Finding 1 (case-sensitivity, line 2693): Finding 2 (auth_type guard, line 2694): Gated the Two new tests: |
…rides endpoint (NousResearch#16290) When auxiliary.vision.provider is set to a named provider (e.g. zai) and auxiliary.vision.base_url is also set, _resolve_task_provider_model() returned "custom" with an empty api_key, causing resolve_provider_client() to fall through to OPENAI_API_KEY or "no-key-required" and receive a 401. Consult the provider's PROVIDER_REGISTRY credential pool when cfg_base_url is set and cfg_api_key is empty and cfg_provider is a known named provider. The explicit api_key in config.yaml always takes precedence; "custom" and "auto" provider names are excluded from the lookup. The fix applies to all auxiliary tasks (vision, compression, review, etc.) via the shared helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oviders Addresses two Copilot inline findings on NousResearch#16308: 1. cfg_provider is read verbatim from config (only .strip()), so a user writing `ZAI` or `Zai` caused a silent miss in PROVIDER_REGISTRY and left resolved_key=None. Normalise to lowercase before the lookup. 2. resolve_api_key_provider_credentials() raises AuthError for providers whose auth_type != "api_key" (nous, openai-codex, copilot-acp, bedrock). Gate the call on pconfig.auth_type == "api_key" so non-api-key providers silently return api_key=None instead of raising. Two new tests added: - test_uppercase_provider_name_normalizes_to_registry_key - test_non_api_key_provider_does_not_raise Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1a3623a to
df26e00
Compare
|
Closing to keep the queue clean — happy to reopen if this is still useful. |
Summary
auxiliary.vision.provider: zai(or any PROVIDER_REGISTRY provider) is set alongsideauxiliary.vision.base_url, the credential pool forzaiwas never consulted — the api_key silently fell through toOPENAI_API_KEYor\"no-key-required\", causing a401at the API._resolve_task_provider_model()now looks up the named provider's credentials whencfg_base_urlis set andcfg_api_keyis empty.api_keyinconfig.yamlalways takes precedence.provider: customandprovider: autoare excluded from the lookup.The bug
_resolve_task_provider_model()returns"custom"as the provider name whenevercfg_base_urlis set, discardingcfg_provider. Downstream,resolve_provider_client("custom", ..., explicit_api_key="")has no knowledge of the original"zai"intent and falls through toOPENAI_API_KEYor"no-key-required"— producing a401even whenZAI_API_KEYis correctly configured in.env.The fix
In the config-reading branch of
_resolve_task_provider_model(), whencfg_base_urlis set andcfg_api_keyis blank, attempt a one-shotresolve_api_key_provider_credentials(cfg_provider)lookup for the named provider. The resolved key is then forwarded asexplicit_api_keyintoresolve_provider_client("custom", ...), which uses it directly for the custom endpoint.The change is isolated to the config-read path (the
if task:block) and applies uniformly to all auxiliary task types (vision, compression, review, etc.).Test plan
_resolve_task_provider_model("vision")withprovider=zai, base_url=..., api_key=""returnsapi_key=None→ 401api_key="zai-test-key-123"(the env-var value)test_named_provider_with_base_url_resolves_env_keyfails; restored → 6 tests passapi_keyin config is never overwritten (test_explicit_api_key_in_config_takes_precedence)Nonewithout raising (test_unknown_provider_in_registry_leaves_key_none)provider: customskips the registry lookup (test_custom_provider_name_skips_registry_lookup)Nonegracefully (test_missing_credentials_do_not_raise)test_fix_applies_to_non_vision_tasks_too)tests/agent/test_auxiliary_client.py,test_auxiliary_config_bridge.py,test_auxiliary_named_custom_providers.py,test_auxiliary_main_first.py,test_vision_resolved_args.py,test_minimax_auxiliary_url.py)Related
🤖 Generated with Claude Code