fix(model-picker): support custom_providers list schema in /model picker#7261
Closed
akhater wants to merge 12 commits into
Closed
fix(model-picker): support custom_providers list schema in /model picker#7261akhater wants to merge 12 commits into
akhater wants to merge 12 commits into
Conversation
Add common data formats (JSON, YAML, CSV, XML, HTML) and source code files (Python, JavaScript, TypeScript, Shell, SQL) to the gateway document upload allowlist. Users working with agents via Telegram, Discord, and other messaging platforms frequently need to share configuration files, data exports, and code snippets as file uploads. The current allowlist is limited to office documents and plain text, forcing users to rename files or paste content inline as a workaround.
The Telegram /model slash command picker read provider info from the
`providers:` dict schema only, so user-defined endpoints configured via
the `custom_providers:` list (the format written by `hermes model`) were
invisible in the picker and clicking them failed with "Unknown provider
'custom'".
Changes:
- list_authenticated_providers() accepts a custom_providers list and
collapses all entries sharing the same base_url into a single "custom"
provider that exposes every configured model as a button.
- gateway/run.py reads cfg.get("custom_providers") and forwards it to
both list_authenticated_providers() call sites.
- switch_model() PATH A synthesizes a ProviderDef on the fly when
--provider custom is passed and the runtime is already on a custom
endpoint, so picker-triggered switches reuse the active base_url /
api_key instead of failing provider resolution.
Result: with multiple models configured under custom_providers:, the
/model picker shows one "Custom endpoint" provider with a button per
model, and clicking a button switches cleanly without re-authentication.
…ay file uploads
The /model picker (list_authenticated_providers) walks every provider whose env var is set and adds it to the picker regardless of whether there are any models to show. This breaks down when an API key is set for a non-LLM feature — e.g. setting GROQ_API_KEY to enable Groq Whisper STT makes Groq appear in the LLM picker as "0 models", which users can't click and which clutters the list. Skip any provider whose curated model list is empty. User-defined and custom_providers entries are unaffected since they already gate on having at least one model configured.
Some Ollama-served models (MiniMax M2.7, Kimi K2.5) emit tool calls in Anthropic's XML format (<invoke name="..."><parameter name="...">) instead of OpenAI structured tool_calls. The main turn loop reads response.choices[0].message directly and never reached the XML, so tools like send_message silently failed — the model "thought" it called them. Add a three-gated fallback parser between content normalization and the plugin hook (run_agent.py ~8685). Gates: 1. Structural: skipped entirely for codex_responses and anthropic_messages 2. Empty: only runs when tool_calls is None/empty 3. Substring: only when "<invoke" appears in the text content Parses each <invoke name="..."><parameter name="...">...</parameter></invoke> block into a SimpleNamespace matching the shape the downstream dispatcher (_execute_tool_calls) expects: .id, .function.name, .function.arguments (JSON string). Strips the raw XML from visible content afterward. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-mapping _get_platform_tools() uses a reverse-mapping loop to infer which toolsets are enabled when no explicit platform_toolsets config exists. The loop iterated only over CONFIGURABLE_TOOLSETS, silently dropping any toolset not listed there — including "messaging", which is the toolset containing send_message. send_message is in _HERMES_CORE_TOOLS and is fully present in the hermes-telegram composite toolset, but because "messaging" was not in CONFIGURABLE_TOOLSETS, it was never added to the enabled set. The check_fn was never even evaluated — the tool was excluded before runtime. Fix: after the CONFIGURABLE_TOOLSETS loop, also iterate over all toolsets defined in TOOLSETS that are neither configurable nor platform defaults. Any whose tools are fully covered by the base composite toolset are added to enabled_toolsets. Adds a logger.debug line for visibility. This only affects the else-branch (no explicit saved config), so profiles that have run `hermes tools` and saved explicit toolset lists are unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolved conflict in hermes_cli/model_switch.py: - PATH A: kept our "custom" provider special-case + wired upstream's new custom_providers param into resolve_provider_full() else branch - Section 4: kept our collapse-to-single-slug approach for custom_providers list (all entries appear as one "custom" entry in the picker) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drop our collapsed-to-single-"custom" hack in favour of upstream's design: each custom_providers entry gets its own slug via custom_provider_slug(). resolve_provider_full() already handles these slugs natively so PATH A no longer needs a special case. Retain the if total == 0: continue guard (our NousResearch#7267 fix) so providers with no models (e.g. Groq keyed for STT only) stay hidden. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each custom_providers entry declares one model under a named provider. Entries sharing the same name collapse into a single provider row in the /model picker — e.g. four Ollama Cloud models appear as one row. Entries with distinct names produce separate rows (Ollama Cloud vs Moonshot). This aligns with upstream's custom_provider_slug() convention while fixing the UX regression where every entry became its own provider row. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6 tasks
Contributor
Author
|
Closing — upstream now has full custom_providers support natively via resolve_custom_provider() and custom_provider_slug() in providers.py, and list_authenticated_providers() / switch_model() both accept custom_providers as a parameter. The bug this PR fixed (custom_providers entries invisible in the /model picker) is resolved upstream through a different architecture (per-slug resolution instead of collapsed "custom" provider). No longer needed. |
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.
Summary
The Telegram
/modelslash command picker only reads user-defined endpoints from theproviders:dict schema, so endpoints configured viacustom_providers:(the list schema written byhermes model) are invisible in the picker. Clicking any model there also fails withUnknown provider 'custom'becauseswitch_modelcan't resolve--provider customagainst the registry.Changes
hermes_cli/model_switch.pylist_authenticated_providers()accepts a newcustom_providerslist arg and collapses all entries sharing a base_url into a singlecustomprovider exposing every configured model.switch_model()PATH A synthesizes aProviderDefon the fly when--provider customis passed while already running on a custom endpoint, reusing the activebase_url/api_keyso the in-place model swap just works.gateway/run.py—_handle_model_commandnow readscfg.get("custom_providers")and forwards it to bothlist_authenticated_providers()call sites.Result
With multiple models configured under
custom_providers:,/modelshows a single Custom endpoint provider with one button per model. Clicking a button switches cleanly without re-authentication.Test plan
/modelon a profile with 4 entries undercustom_providers:shows one provider with 4 model buttons/model <name> --provider customstill worksproviders:dict schema are unaffected