From 16e7f225f60033a403c70a7537838af9589ff560 Mon Sep 17 00:00:00 2001 From: Syed Abdur Rehman Date: Tue, 14 Jul 2026 20:22:25 +0530 Subject: [PATCH 1/2] feat(agent): resolve built-in providers..extra_body once at setup Addresses @teknium1 hermes-sweeper review on #21554. - Resolve first-class / built-in provider extra_body overrides once during agent setup into request_overrides (transport already merges those). - Canonicalize aliases: providers.dashscope.extra_body applies when the session provider is alibaba (and vice versa). Exact session key wins over canonical/alias siblings. - Explicit schema: URL-bearing providers.* entries stay named custom endpoints; partial entries (extra_body only, no api/base_url/url) are the built-in override contract. - Preserve existing custom-provider merge + caller precedence. - Docs + alias/precedence/custom-path tests. --- agent/agent_init.py | 144 +++++++++++++++- .../agent/test_builtin_provider_extra_body.py | 156 ++++++++++++++++++ website/docs/integrations/providers.md | 31 ++++ 3 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 tests/agent/test_builtin_provider_extra_body.py diff --git a/agent/agent_init.py b/agent/agent_init.py index 0c700c279b980..417d6a8b3f061 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -254,23 +254,149 @@ def _custom_provider_extra_body_for_agent( return fallback -def _merge_custom_provider_extra_body(agent, custom_providers: List[Dict[str, Any]]) -> None: +def _providers_entry_ci( + providers_cfg: Dict[str, Any], key: str +) -> Optional[Dict[str, Any]]: + """Return ``providers.`` with case-insensitive key match.""" + if not isinstance(providers_cfg, dict) or not key: + return None + if key in providers_cfg and isinstance(providers_cfg[key], dict): + return providers_cfg[key] + key_l = key.lower() + for raw_key, entry in providers_cfg.items(): + if str(raw_key).strip().lower() == key_l and isinstance(entry, dict): + return entry + return None + + +def _provider_lookup_keys(provider: str) -> List[str]: + """Ordered keys to probe under ``providers:`` for a session provider. + + Prefer the session's exact provider string first, then the canonical + profile name, then registered aliases. That way a user who pins + ``providers.dashscope.extra_body`` still wins after Hermes resolves the + session provider to the canonical ``alibaba`` profile (and vice versa). + """ + provider_norm = (provider or "").strip().lower() + if not provider_norm or provider_norm == "custom" or provider_norm.startswith("custom:"): + return [] + + keys: List[str] = [provider_norm] + try: + from providers import get_provider_profile + + profile = get_provider_profile(provider_norm) + except Exception: + profile = None + + if profile is not None: + canonical = (profile.name or "").strip().lower() + if canonical and canonical not in keys: + keys.append(canonical) + for alias in getattr(profile, "aliases", ()) or (): + alias_norm = str(alias or "").strip().lower() + if alias_norm and alias_norm not in keys: + keys.append(alias_norm) + return keys + + +def _builtin_provider_extra_body_for_agent( + *, + provider: str, + providers_cfg: Any, +) -> Optional[Dict[str, Any]]: + """Read ``providers..extra_body`` for a first-class / built-in provider. + + Schema contract (distinct from named custom endpoints): + - Keys under ``providers:`` that include a base URL (``api`` / ``base_url`` / + ``url``) are **named custom endpoints** and are handled by + :func:`_custom_provider_extra_body_for_agent` via + ``get_compatible_custom_providers``. + - Keys that match a built-in profile name or alias may carry a partial + entry with only ``extra_body`` (no URL required). Those are resolved + here once at agent setup and merged into ``request_overrides``. + + Lookup order: exact session provider string → canonical profile name → + aliases. First hit with a non-empty ``extra_body`` dict wins. + """ + if not isinstance(providers_cfg, dict): + return None + + for key in _provider_lookup_keys(provider): + entry = _providers_entry_ci(providers_cfg, key) + if not isinstance(entry, dict): + continue + # Named custom endpoints with a URL are owned by the custom path — + # skip them here so we never double-apply or steal a custom entry + # that simply shares a name with a built-in profile. + has_url = False + for url_key in ("base_url", "url", "api"): + raw_url = entry.get(url_key) + if isinstance(raw_url, str) and raw_url.strip(): + has_url = True + break + if has_url: + continue + extra_body = entry.get("extra_body") + if isinstance(extra_body, dict) and extra_body: + return dict(extra_body) + return None + + +def _apply_extra_body_to_request_overrides( + agent, extra_body: Dict[str, Any] +) -> None: + """Merge *extra_body* under agent.request_overrides; caller keys win.""" + overrides = dict(getattr(agent, "request_overrides", {}) or {}) + merged_extra_body = dict(extra_body) + existing_extra_body = overrides.get("extra_body") + if isinstance(existing_extra_body, dict): + merged_extra_body.update(existing_extra_body) + overrides["extra_body"] = merged_extra_body + agent.request_overrides = overrides + + +def _merge_custom_provider_extra_body( + agent, + custom_providers: List[Dict[str, Any]], + agent_cfg: Optional[Dict[str, Any]] = None, +) -> None: + """Resolve provider ``extra_body`` once at agent setup into request_overrides. + + Order: + 1. Named custom endpoints (``custom`` / ``custom:``) via *custom_providers*. + 2. Else first-class / built-in profile overrides from + ``providers..extra_body`` (no URL required). + + Existing ``request_overrides.extra_body`` always wins on key conflict. + """ extra_body = _custom_provider_extra_body_for_agent( provider=agent.provider, model=agent.model, base_url=agent.base_url, custom_providers=custom_providers, ) + + if not extra_body: + providers_cfg = None + if isinstance(agent_cfg, dict): + providers_cfg = agent_cfg.get("providers") + else: + try: + from hermes_cli.config import load_config + + providers_cfg = load_config().get("providers") + except Exception: + providers_cfg = None + extra_body = _builtin_provider_extra_body_for_agent( + provider=getattr(agent, "provider", "") or "", + providers_cfg=providers_cfg, + ) + if not extra_body: return - overrides = dict(getattr(agent, "request_overrides", {}) or {}) - merged_extra_body = dict(extra_body) - existing_extra_body = overrides.get("extra_body") - if isinstance(existing_extra_body, dict): - merged_extra_body.update(existing_extra_body) - overrides["extra_body"] = merged_extra_body - agent.request_overrides = overrides + _apply_extra_body_to_request_overrides(agent, extra_body) def init_agent( @@ -1707,7 +1833,7 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None: # Store for reuse by _check_compression_model_feasibility (auxiliary # compression model context-length detection needs the same list). agent._custom_providers = _custom_providers - _merge_custom_provider_extra_body(agent, _custom_providers) + _merge_custom_provider_extra_body(agent, _custom_providers, agent_cfg=_agent_cfg) # Check custom_providers per-model context_length if _config_context_length is None and _custom_providers: diff --git a/tests/agent/test_builtin_provider_extra_body.py b/tests/agent/test_builtin_provider_extra_body.py new file mode 100644 index 0000000000000..2df28029d4782 --- /dev/null +++ b/tests/agent/test_builtin_provider_extra_body.py @@ -0,0 +1,156 @@ +"""Built-in / first-class provider ``providers..extra_body`` resolution. + +Addresses hermes-sweeper review on #21554: +- Resolve once at agent setup into ``request_overrides`` (not per transport call) +- Canonicalize aliases (``dashscope`` → ``alibaba``) so either config key works +- Preserve named custom-endpoint behavior (URL-bearing providers entries) +""" + +from __future__ import annotations + +from types import SimpleNamespace + +from agent.agent_init import ( + _builtin_provider_extra_body_for_agent, + _merge_custom_provider_extra_body, + _provider_lookup_keys, +) + + +def test_provider_lookup_keys_prefer_session_then_canonical_then_aliases(): + keys = _provider_lookup_keys("dashscope") + assert keys[0] == "dashscope" + assert "alibaba" in keys + # Other documented aliases should be present after canonical + assert "alibaba-cloud" in keys or "qwen-dashscope" in keys + + +def test_provider_lookup_keys_skip_custom(): + assert _provider_lookup_keys("custom") == [] + assert _provider_lookup_keys("custom:foo") == [] + + +def test_builtin_extra_body_via_alias_key(): + """Config under providers.dashscope applies when session provider is alibaba.""" + got = _builtin_provider_extra_body_for_agent( + provider="alibaba", + providers_cfg={ + "dashscope": {"extra_body": {"enable_thinking": False}}, + }, + ) + assert got == {"enable_thinking": False} + + +def test_builtin_extra_body_via_canonical_key_when_session_is_alias(): + got = _builtin_provider_extra_body_for_agent( + provider="dashscope", + providers_cfg={ + "alibaba": {"extra_body": {"enable_thinking": False}}, + }, + ) + assert got == {"enable_thinking": False} + + +def test_exact_session_key_wins_over_canonical(): + """If both alias and canonical keys exist, session string match wins.""" + got = _builtin_provider_extra_body_for_agent( + provider="dashscope", + providers_cfg={ + "dashscope": {"extra_body": {"source": "alias"}}, + "alibaba": {"extra_body": {"source": "canonical"}}, + }, + ) + assert got == {"source": "alias"} + + +def test_url_bearing_providers_entry_skipped_by_builtin_path(): + """Named custom endpoints keep the custom path; builtin must not steal them.""" + got = _builtin_provider_extra_body_for_agent( + provider="alibaba", + providers_cfg={ + "dashscope": { + "api": "https://example.test/v1", + "extra_body": {"enable_thinking": False}, + }, + }, + ) + assert got is None + + +def test_merge_builtin_into_request_overrides(): + agent = SimpleNamespace( + provider="alibaba", + model="qwen-plus", + base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + request_overrides={}, + ) + _merge_custom_provider_extra_body( + agent, + custom_providers=[], + agent_cfg={ + "providers": { + "dashscope": {"extra_body": {"enable_thinking": False}}, + } + }, + ) + assert agent.request_overrides == { + "extra_body": {"enable_thinking": False}, + } + + +def test_merge_caller_extra_body_wins_over_builtin(): + agent = SimpleNamespace( + provider="alibaba", + model="qwen-plus", + base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + request_overrides={ + "extra_body": {"enable_thinking": True, "caller_only": 1}, + }, + ) + _merge_custom_provider_extra_body( + agent, + custom_providers=[], + agent_cfg={ + "providers": { + "alibaba": { + "extra_body": { + "enable_thinking": False, + "from_config": True, + } + } + } + }, + ) + assert agent.request_overrides["extra_body"] == { + "enable_thinking": True, # caller wins + "from_config": True, + "caller_only": 1, + } + + +def test_custom_endpoint_path_still_preferred_over_builtin(): + """When provider is custom, builtin lookup must not run.""" + agent = SimpleNamespace( + provider="custom", + model="google/gemma-4-31b-it", + base_url="https://example.test/v1", + request_overrides={}, + ) + _merge_custom_provider_extra_body( + agent, + custom_providers=[ + { + "name": "gemma", + "base_url": "https://example.test/v1", + "model": "google/gemma-4-31b-it", + "extra_body": {"from_custom": True}, + } + ], + agent_cfg={ + "providers": { + # Would match if wrongly applied to custom sessions + "alibaba": {"extra_body": {"from_builtin": True}}, + } + }, + ) + assert agent.request_overrides == {"extra_body": {"from_custom": True}} diff --git a/website/docs/integrations/providers.md b/website/docs/integrations/providers.md index d07d27e22b341..4252fadd2d6ec 100644 --- a/website/docs/integrations/providers.md +++ b/website/docs/integrations/providers.md @@ -1229,6 +1229,37 @@ extra_body: enable_thinking: false ``` +### Built-in provider overrides (`providers..extra_body`) + +Named custom endpoints above require a URL. First-class / built-in providers +(the profiles under `plugins/model-providers/`, e.g. `alibaba` / `dashscope`, +`openai`, `openrouter`) can also pin request body fields without declaring a +URL — put a **partial** entry under the keyed `providers:` schema: + +```yaml +providers: + # Either the canonical profile name … + alibaba: + extra_body: + enable_thinking: false + # … or any registered alias works (dashscope → alibaba): + # dashscope: + # extra_body: + # enable_thinking: false +``` + +Hermes resolves this **once at agent setup** into `request_overrides.extra_body` +(same merge path the chat-completions transport already uses). Lookup order: + +1. Exact session provider string +2. Canonical profile name +3. Other registered aliases + +Per-call `request_overrides.extra_body` still wins on key conflict. Entries +that include `api` / `base_url` / `url` remain **named custom endpoints** and +are handled by the custom-provider path above — they are not double-applied +here. + The `hermes model` → Custom Endpoint wizard now prompts for `api_mode` explicitly and persists your answer to `config.yaml`. URL-based auto-detection (e.g. `/anthropic` paths → `anthropic_messages`) still happens as a fallback when the field is left blank. **Native vision for custom-provider models.** If your custom endpoint serves a vision-capable model that isn't in models.dev, set `model.supports_vision: true` so Hermes routes attached images natively (as `image_url` parts) instead of pre-processing them through `vision_analyze`. Single knob — no need to also set `agent.image_input_mode: native`. From 3730be7a25f3625d947bd16be7afd50edf9d69ff Mon Sep 17 00:00:00 2001 From: Syed Abdur Rehman Ali Date: Fri, 31 Jul 2026 00:08:32 +0530 Subject: [PATCH 2/2] ci: retry transient setup-uv fetch failure