-
Notifications
You must be signed in to change notification settings - Fork 51.9k
fix(model-switch): resolve provider-module plugin providers in switch path #34368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -390,6 +390,13 @@ class ProviderDef: | |
| "bedrock_converse": "bedrock_converse", | ||
| } | ||
|
|
||
| # Inverse of TRANSPORT_TO_API_MODE — provider-module plugins declare api_mode | ||
| # directly (ProviderProfile.api_mode), so map it back to the ProviderDef | ||
| # transport vocabulary. Unknown api_modes fall back to "openai_chat". | ||
| _API_MODE_TO_TRANSPORT: Dict[str, str] = { | ||
| v: k for k, v in TRANSPORT_TO_API_MODE.items() | ||
| } | ||
|
|
||
|
|
||
| # -- Helper functions --------------------------------------------------------- | ||
|
|
||
|
|
@@ -683,6 +690,18 @@ def resolve_provider_full( | |
| if pdef is not None: | ||
| return pdef | ||
|
|
||
| # 1b. Provider-module plugins (plugins/model-providers/<name>/). | ||
| # These are the same profiles that hermes_cli.auth.PROVIDER_REGISTRY | ||
| # auto-extends from at import time, so the model-switch path must see | ||
| # them too — otherwise a provider that resolves fine at startup (e.g. | ||
| # a local Anthropic proxy declared only as a plugin profile) is | ||
| # rejected with "Unknown provider" when the user tries to /model into | ||
| # it. resolve_provider_full and runtime startup resolution were | ||
| # consulting two different registries; this reunifies them. | ||
| plugin_pdef = _resolve_plugin_provider(canonical, name) | ||
| if plugin_pdef is not None: | ||
| return plugin_pdef | ||
|
|
||
| # 2. User-defined providers from config | ||
| if user_providers: | ||
| # Try canonical name | ||
|
|
@@ -716,3 +735,56 @@ def resolve_provider_full( | |
| pass | ||
|
|
||
| return None | ||
|
|
||
|
|
||
| def _resolve_plugin_provider(canonical: str, original: str) -> Optional[ProviderDef]: | ||
| """Resolve a provider declared as a provider-module plugin profile. | ||
|
|
||
| Provider plugins live in ``plugins/model-providers/<name>/`` (bundled) or | ||
| ``$HERMES_HOME/plugins/model-providers/<name>/`` (user). They register a | ||
| ``ProviderProfile`` carrying name/aliases/base_url/env_vars/api_mode. | ||
| ``hermes_cli.auth.PROVIDER_REGISTRY`` already auto-extends from this same | ||
| source, which is why such providers resolve correctly during runtime | ||
| startup. The model-switch resolver historically did not, producing the | ||
| "Unknown provider" failure for plugin-only providers. | ||
|
|
||
| Translates a ``ProviderProfile`` into a ``ProviderDef`` so the rest of the | ||
| switch path is unchanged. ``api_mode`` maps back to ``transport`` via the | ||
| inverse of ``TRANSPORT_TO_API_MODE`` (default ``openai_chat``). | ||
| """ | ||
| try: | ||
| from providers import get_provider_profile | ||
| except Exception: | ||
| return None | ||
|
|
||
| profile = None | ||
| try: | ||
| profile = get_provider_profile(canonical) or get_provider_profile( | ||
| (original or "").strip().lower() | ||
| ) | ||
| except Exception: | ||
| profile = None | ||
| if profile is None: | ||
| return None | ||
|
|
||
| api_mode = getattr(profile, "api_mode", "") or "chat_completions" | ||
| transport = _API_MODE_TO_TRANSPORT.get(api_mode, "openai_chat") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This mapped transport is not consumed by |
||
|
|
||
| env_vars = tuple(getattr(profile, "env_vars", ()) or ()) | ||
| api_key_env_vars = tuple( | ||
| v for v in env_vars if not v.endswith("_BASE_URL") and not v.endswith("_URL") | ||
| ) or env_vars | ||
| base_url_env_var = next( | ||
| (v for v in env_vars if v.endswith("_BASE_URL") or v.endswith("_URL")), "" | ||
| ) | ||
|
|
||
| return ProviderDef( | ||
| id=getattr(profile, "name", canonical), | ||
| name=getattr(profile, "display_name", "") or getattr(profile, "name", canonical), | ||
| transport=transport, | ||
| api_key_env_vars=api_key_env_vars, | ||
| base_url=getattr(profile, "base_url", "") or "", | ||
| base_url_env_var=base_url_env_var, | ||
| auth_type=getattr(profile, "auth_type", "api_key") or "api_key", | ||
| source="plugin", | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fallback is before the existing
providers:andcustom_providers:paths. A plugin named the same as a configured endpoint will now shadow that user override; keep config resolution ahead of the plugin fallback.