feat(providers): ProviderProfile ABC + plugins/model-providers/ (salvage of #14424 + pluggable migration)#20324
Merged
Merged
Conversation
…els, transport single-path Introduces providers/ package — single source of truth for every inference provider. Adding a simple api-key provider now requires one providers/<name>.py file with zero edits anywhere else. What this PR ships: - providers/ package (ProviderProfile ABC + 33 profiles across 4 api_modes) - ProviderProfile declarative fields: name, api_mode, aliases, display_name, env_vars, base_url, models_url, auth_type, fallback_models, hostname, default_headers, fixed_temperature, default_max_tokens, default_aux_model - 4 overridable hooks: prepare_messages, build_extra_body, build_api_kwargs_extras, fetch_models - chat_completions.build_kwargs: profile path via _build_kwargs_from_profile, legacy flag path retained for lmstudio/tencent-tokenhub (which have session-aware reasoning probing that doesn't map cleanly to hooks yet) - run_agent.py: profile path for all registered providers; legacy path variable scoping fixed (all flags defined before branching) - Auto-wires: auth.PROVIDER_REGISTRY, models.CANONICAL_PROVIDERS, doctor health checks, config.OPTIONAL_ENV_VARS, model_metadata._URL_TO_PROVIDER - GeminiProfile: thinking_config translation (native + openai-compat nested) - New tests/providers/ (79 tests covering profile declarations, transport parity, hook overrides, e2e kwargs assembly) Deltas vs original PR (salvaged onto current main): - Added profiles: alibaba-coding-plan, azure-foundry, minimax-oauth (were added to main since original PR) - Skipped profiles: lmstudio, tencent-tokenhub stay on legacy path (their reasoning_effort probing has no clean hook equivalent yet) - Removed lmstudio alias from custom profile (it's a separate provider now) - Skipped openrouter/custom from PROVIDER_REGISTRY auto-extension (resolve_provider special-cases them; adding breaks runtime resolution) - runtime_provider: profile.api_mode only as fallback when URL detection finds nothing (was breaking minimax /v1 override) - Preserved main's legacy-path improvements: deepseek reasoning_content preserve, gemini Gemma skip, OpenRouter response caching, Anthropic 1M beta recovery, etc. - Kept agent/copilot_acp_client.py in place (rejected PR's relocation — main has 7 fixes landed since; relocation would revert them) - _API_KEY_PROVIDER_AUX_MODELS alias kept for backward compat with existing test imports Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Closes #14418
Collaborator
…providers/ Every provider profile is now a self-contained plugin under plugins/model-providers/<name>/, mirroring the plugins/platforms/ pattern established for IRC and Teams. The ProviderProfile ABC stays in providers/; the per-provider profile data moves out. - plugins/model-providers/<name>/__init__.py calls register_provider() - plugins/model-providers/<name>/plugin.yaml declares kind: model-provider - providers/__init__.py._discover_providers() lazily scans bundled plugins then $HERMES_HOME/plugins/model-providers/<name>/ (user override path) - User plugins with the same name override bundled ones (last-writer-wins in register_provider) - Legacy providers/<name>.py layout still supported for back-compat with out-of-tree editable installs - Hermes PluginManager: new kind=model-provider; skipped like memory plugins (providers/ discovery owns them); standalone plugins with register_provider+ProviderProfile in their __init__.py auto-coerce to this kind (same heuristic as memory providers) - skip_names extended to include 'model-providers' so the general PluginManager doesn't double-scan the category - 4 new tests in tests/providers/test_plugin_discovery.py covering bundled discovery, user override, and general-loader isolation - Docs updated: website/docs/developer-guide/adding-providers.md, provider-runtime.md, providers/README.md, plugins/model-providers/README.md No API break: auth.py / config.py / doctor.py / models.py / runtime_provider.py / model_metadata.py / auxiliary_client.py / chat_completions.py / run_agent.py all still consume providers via get_provider_profile() / list_providers() — they just now see plugin-discovered entries instead of pkgutil-iterated ones. Third parties can now drop a single directory into ~/.hermes/plugins/model-providers/<name>/ to add or override an inference provider without touching the repo.
1 task
15 tasks
4 tasks
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.
Provider declarations become one plugin each. Adding a simple api-key provider now requires a single
plugins/model-providers/<name>/directory (2 files:__init__.py+plugin.yaml) and zero edits anywhere else. Bundled plugins can be overridden from$HERMES_HOME/plugins/model-providers/<name>/without editing the repo.Salvaged from @kshitijk4poor's PR #14424 onto current main, then migrated to the same plugin pattern used for IRC and Teams gateway adapters.
Commits in this PR
feat: provider modules — ProviderProfile ABC, 33 providers, fetch_models, transport single-path— the salvaged feat: provider modules — ProviderProfile ABC, 29 providers, fetch_models, transport single-path #14424 refactor, updated for current mainfeat(providers): make all 33 providers pluggable under plugins/model-providers/— moves every profile to a bundled plugin dir, adds user-override path, auto-coerces user-installed plugins tokind: model-providerdocs(providers): add model-provider-plugin authoring guide + fix stale refs— new/docs/developer-guide/model-provider-pluginpage (authoring guide: layout, fields, hooks, user overrides, testing, pip distribution); stale comment cleanup in main.py/doctor.py/auth.py/models.py; updates to AGENTS.md, user-guide/features/plugins.md (plugin types: 3→4), adding-providers.md + provider-runtime.md cross-referencesWhat changes for plugin authors
Before:
After:
Or for a user override (no repo edit):
User plugins win over bundled plugins on name collision (last-writer-wins in
register_provider()).Changes (commit 2 of 2)
plugins/model-providers/— new category, 28 bundled plugin dirs (covering 33 profiles — minimax, kimi, opencode, gemini each ship two profiles in one plugin)providers/__init__.py—_discover_providers()now scans bundled + user plugin dirs first, then falls back to legacyproviders/*.pyproviders/— now onlybase.py(ABC),__init__.py(registry),README.mdhermes_cli/plugins.py— addskind: model-providerto_VALID_PLUGIN_KINDS; generalPluginManagerskips imports of these (providers/ discovery owns them); heuristic auto-coerces user-installedstandaloneplugins whose__init__.pycallsregister_providerwith aProviderProfiletokind: model-providerskip_namesextended soplugins/model-providers/isn't double-scanned by the general managertests/providers/test_plugin_discovery.py— bundled discovery, user override, general-loader isolationDocs
plugins/model-providers/README.md— plugin contract + authoring guideproviders/README.md— rewritten, points at plugin dirwebsite/docs/developer-guide/adding-providers.md— fast path now describes plugin layoutwebsite/docs/developer-guide/provider-runtime.md— references both pathsChanges (commit 1 of 2 — salvage)
Original #14424 changes, same as documented before:
providers/— ProviderProfile ABC + registry + 33 profilesagent/transports/chat_completions.py— profile path via_build_kwargs_from_profile()for registered providers; legacy flag path retained for lmstudio / tencent-tokenhubrun_agent.py— profile path integrated; legacy-path variables scoped correctlyhermes_cli/{auth,config,doctor,main,models,runtime_provider}.py— auto-extend fromlist_providers()agent/{auxiliary_client,model_metadata}.py— auto-wirealibaba-coding-plan,azure-foundry,minimax-oauthSalvage notes (from commit 1)
agent/copilot_acp_client.pyleft in place (PR's relocation would have reverted 7 landed fixes)lmstudio/tencent-tokenhubstay on legacy flag path — reasoning-effort logic has no clean profile-hook equivalent yetlmstudioaliases fromcustomprofileopenrouter/customfromPROVIDER_REGISTRYauto-extension (resolve_provider()special-cases them)runtime_provider.py:profile.api_modeis a fallback, not an override (PR's version broke minimax/v1URL)GeminiProfile.build_extra_bodytranslatesthinking_config(native + OpenAI-compat nested)Validation
tests/providers/tests/run_agent/test_provider_parity.pytests/hermes_cli/test_runtime_provider_resolution.pytests/hermes_cli/test_plugin*+tests/test_plugin_skills.pytests/hermes_cli/(full dir)tests/run_agent/(full dir)tests/agent/(full dir)All failing tests are reproduced on
origin/mainwithout this PR. Not regressions.E2E smoke test confirms:
plugins/model-providers/PROVIDER_REGISTRY,CANONICAL_PROVIDERS,OPTIONAL_ENV_VARS, doctor,_URL_TO_PROVIDERopenrouter/customcorrectly absent fromPROVIDER_REGISTRY(behavior parity)lmstudio/tencent-tokenhubcorrectly fall through to legacy paththinking_configproduces correct native + OpenAI-compat nested shapes$HERMES_HOME/plugins/model-providers/gmi/replaces bundledgmiprofileCloses #14418. Original PR #14424 by @kshitijk4poor — salvage preserves their authorship on commit 1 via rebase-merge.