fix(provider): unify provider/credential resolution — config.yaml as single source, fail-closed on custom intent#35056
Open
banditburai wants to merge 5 commits into
Conversation
b5d5be5 to
f0d1e1d
Compare
…ovider Pure, offline, import-light leaf module: ResolvedProvider value object, canonicalize_provider (single alias table), normalize_base_url (one /v1 normalizer), and the path-scoped api_mode detector / select_api_mode ladder. Foundation for the unified resolver.
…-closed; demote ambient-key fallback (NousResearch#5358 NousResearch#4600 NousResearch#12146) One staged resolver returns the ResolvedProvider object with provenance and memoization (static sources only; pool/OAuth/process bypass). Routes both alias tables through canonicalize_provider (NousResearch#12146). Moves the ambient OPENAI/OPENROUTER short-circuit below the per-provider scan so a configured vendor key wins (NousResearch#5358). Applies the /v1 normalizer once, gated to custom (NousResearch#4600). Fails closed with AuthError(custom_provider_unresolved) when custom intent has no resolvable endpoint and no usable key (preserving the key-present NousResearch#14676 fallback).
…NousResearch#8919) Folds model.api_base into model.base_url at load (permanent accepted alias) and warns once on unknown model.* keys, so config.yaml is the single source of truth for the endpoint and a stale OPENAI_BASE_URL is no longer silently consulted.
…stence (NousResearch#3263); picker aliases via canonicalize_provider (NousResearch#12146) /model classifies probe failures and warns-and-confirms instead of swallowing them, pre-fills custom-endpoint fields, and persists {provider,base_url,api_key} atomically. The model picker's alias table is overlaid on canonicalize_provider so it can no longer drift from runtime resolution (NousResearch#12146).
…read ACP explicit_base_url (NousResearch#13489 NousResearch#5358) The agent carries the resolved ResolvedProvider (with provenance) instead of re-deriving credentials per request. The auxiliary client delegates wholesale to the unified resolver (dropping its duplicate alias table and OPENAI_BASE_URL routing) and reaches named custom providers whose name canonicalizes to custom. ACP sessions thread explicit_base_url + target_model into resolution so a custom provider's base_url is honored and a built-in re-derives its own (NousResearch#13489).
f0d1e1d to
d7da060
Compare
This was referenced Jun 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parallel provider/credential resolution paths (CLI, gateway, ACP, auxiliary) had drifted, and OpenRouter was the terminal fallback in every one — so a configured custom/local provider was silently dropped to OpenRouter or had credentials mixed from another provider. This collapses them into one offline resolver → one
ResolvedProvidervalue object → one alias table → one/v1normalizer → one path-scoped api_mode detector, fail-closed on declared custom intent, with zero network during resolution. Closes 6 issues, supersedes 1 PR.Issues fixed
main.py(switch + classified probe),config.py(model surface)test_probe_classification,test_model_flow_custom_probe,test_config_model_surface/v1on chat completionsprovider_resolution.normalize_base_urlviaruntime_provider._finalize_runtimetest_provider_resolution,test_runtime_provider_resolutionmodel.provider, fall back to OpenRouter whenOPENROUTER_API_KEYsetauth.resolve_provider(ambient short-circuit demoted below per-provider scan)test_provider_resolution_cross_pathapi_basedropped;OPENAI_BASE_URLsilently consultedconfig._normalize_model_api_base,runtime_provider,auxiliary_client(warn)test_config_model_surface,test_clear_stale_base_url,test_auxiliary_custom_runtime_delegationprovider=custom(alias-table drift)provider_resolution.canonicalize_provider(single table),auth.py,models.pypickertest_provider_resolution(alias parity),test_auxiliary_named_custom_providersprovider=customresolves the wrong credentialacp_adapter/session.pythreadsexplicit_base_urlinto resolutiontest_session_explicit_base_url⚠ Behavior changes
Most users unaffected — these matter only if you relied on env-based routing or incidental keys.
OPENAI_BASE_URLno longer routes — ignored for routing, warned when set. [potentially breaking] → move it tomodel.base_url(ormodel.api_base) in config.yaml.OPENAI_API_KEY/OPENROUTER_API_KEYshort-circuit moved below the per-provider scan. [breaking for incidental-key setups] a configured vendor key (e.g.DEEPSEEK_API_KEY) now wins; force OpenRouter withprovider: openrouter.openai-apiis skipped in auto-detect so a bareOPENAI_API_KEYstill falls to the OpenRouter last-resort. [behavior change] → setprovider: openai-apiexplicitly to use direct OpenAI.AuthError(code="custom_provider_unresolved")instead of silently hittingopenrouter.aiwith an empty key. [breaking — now errors] → supply a base_url or key. The established key-present → OpenRouter fallback is unchanged./v1(chat); anthropic endpoints never gain/v1. [low risk]Back-compat
ResolvedProvidersupports dict-style reads (get/[]/in) andas_dict()stays byte-compatible with the legacy runtime dict — existing consumers untouched.model.api_baseis a permanent accepted alias formodel.base_url(folded at load).provider:values (openai-api, openrouter, anthropic, vendors) remain reachable.Security
Preserves host-gated key selection from security advisory GHSA-76xc-57q6-vm5m — env keys only sent to matching hosts; lookalike/path-spoof hosts rejected. The new path-scoped api_mode detector additionally resists query/fragment spoofing the old full-URL detector allowed.
Design
hermes_cli/provider_resolution.py— pure, offline, import-light; the single source of truth.ResolvedProviderfrozen value object resolved once per lifecycle and carried on the agent (provenance:base_url_source/key_source)._parse_api_mode/VALID_API_MODESconsolidated into the leaf (eliminates the 3 previously-divergent copies; locked by an identity test).Resolves upstream:
Closes #3263
Closes #4600
Closes #5358
Closes #8919
Closes #12146
Closes #13489
Supersedes #17072 — strict superset of its LM-Studio runtime slice, without its ~20k LOC of unrelated divergence and with full test coverage.