feat(delegation): honor delegation.request_overrides on all three resolution branches with explicit-over-runtime merge precedence
Completes the #90953 salvage on post-#98237 main: - New _merge_request_overrides helper defines the precedence contract: explicit delegation.request_overrides merges OVER runtime/parent-derived overrides — explicit top-level keys win; extra_body is deep-merged one level so runtime extra_body keys survive unless redefined. Inputs are copy.deepcopy'd so transport-side mutation can't leak into config or the provider runtime cache. - Direct base_url branch: explicit key now merges over the #98237 provider-alongside-base_url runtime overrides instead of being a separate return shape; max_output_tokens preserved. - Named-provider branch and parent-inherit branch now honor the key too, so delegation.request_overrides never silently no-ops. - _build_child_agent honors override_request_overrides whenever set (previously only when override_provider was set), enabling the inherit branch's merged value to reach the child. - DEFAULT_CONFIG: delegation.request_overrides entry with comment. - Tests: expanded tests/tools/test_delegate_request_overrides.py — deep-copy proofs, explicit-over-runtime precedence on the provider-alongside-base_url path, named-provider branch, inherit branch, and merge-helper unit tests. - Docs: configuration.md delegation section + features/delegation.md document the key, precedence, and example YAML (OpenRouter extra_body.provider.sort).
This commit is contained in:
@@ -2077,6 +2077,15 @@ DEFAULT_CONFIG = {
|
||||
# "codex_responses", or "anthropic_messages". Empty = auto-detect
|
||||
# from URL (e.g. /anthropic suffix → anthropic_messages). Set this
|
||||
# explicitly for non-standard endpoints the heuristic can't detect.
|
||||
# Per-child request settings sent on every delegation API call, on all
|
||||
# three resolution branches (direct base_url, named provider, and
|
||||
# parent-inherit). Top-level keys are API kwargs (e.g. service_tier);
|
||||
# an "extra_body" sub-dict is merged into the request's extra_body —
|
||||
# e.g. {"extra_body": {"provider": {"sort": "throughput"}}} routes
|
||||
# OpenRouter delegation children to the fastest provider. Precedence:
|
||||
# these explicit values merge OVER runtime/parent-derived overrides
|
||||
# (explicit keys win; extra_body deep-merged one level).
|
||||
"request_overrides": {},
|
||||
# When delegate_task narrows child toolsets explicitly, preserve any
|
||||
# MCP toolsets the parent already has enabled. On by default so
|
||||
# narrowing (e.g. toolsets=["web","browser"]) expresses "I want these
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
"""Regression tests for delegation.request_overrides on the direct-endpoint branch.
|
||||
"""Regression tests for the delegation.request_overrides config key.
|
||||
|
||||
The direct base_url branch of _resolve_delegation_credentials (delegation.base_url
|
||||
set, provider=custom) used to drop delegation.request_overrides on the floor —
|
||||
the named-provider branch forwards runtime request_overrides (delegate_tool.py
|
||||
"request_overrides": dict(runtime.get("request_overrides") or {})), but the
|
||||
direct branch returned no key. That made it impossible to give delegation
|
||||
children OpenRouter routing hints (extra_body.provider = {"sort": "throughput"})
|
||||
when delegating straight to openrouter.ai/api/v1 via base_url+api_key.
|
||||
PR #90953 (salvage): ``delegation.request_overrides`` is an explicit dict of
|
||||
per-child request settings that must be honored on EVERY resolution branch of
|
||||
``_resolve_delegation_credentials``:
|
||||
|
||||
1. direct base_url (provider=custom) — the branch the original PR fixed,
|
||||
2. named provider (delegation.provider set, no base_url),
|
||||
3. parent-inherit (neither provider nor base_url).
|
||||
|
||||
Precedence contract (post-#98237): explicit config values merge OVER
|
||||
runtime/parent-derived overrides — top-level explicit keys win; the
|
||||
``extra_body`` sub-dict is deep-merged one level so runtime extra_body keys
|
||||
survive unless the explicit key redefines them. Nested values are deep-copied
|
||||
so transport-side mutation cannot leak back into config.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from tools.delegate_tool import _resolve_delegation_credentials
|
||||
from tools.delegate_tool import (
|
||||
_merge_request_overrides,
|
||||
_resolve_delegation_credentials,
|
||||
)
|
||||
|
||||
|
||||
def _cfg(**overrides):
|
||||
@@ -24,6 +33,20 @@ def _cfg(**overrides):
|
||||
return cfg
|
||||
|
||||
|
||||
def _parent(**attrs):
|
||||
parent = MagicMock()
|
||||
parent._delegate_depth = 0
|
||||
# MagicMock attributes are MagicMocks (non-dict) by default; set real
|
||||
# values for the ones the resolution path inspects.
|
||||
parent.request_overrides = attrs.pop("request_overrides", None)
|
||||
for k, v in attrs.items():
|
||||
setattr(parent, k, v)
|
||||
return parent
|
||||
|
||||
|
||||
# ── Branch 1: direct base_url ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_direct_branch_forwards_request_overrides():
|
||||
"""delegation.request_overrides flows through the direct-endpoint branch."""
|
||||
cfg = _cfg(
|
||||
@@ -35,6 +58,8 @@ def test_direct_branch_forwards_request_overrides():
|
||||
assert creds["request_overrides"] == {
|
||||
"extra_body": {"provider": {"sort": "throughput"}},
|
||||
}
|
||||
# Shape parity with the named-provider branch: max_output_tokens present.
|
||||
assert "max_output_tokens" in creds
|
||||
|
||||
|
||||
def test_direct_branch_absent_request_overrides_stays_none():
|
||||
@@ -50,3 +75,182 @@ def test_direct_branch_non_dict_request_overrides_stays_none():
|
||||
_cfg(request_overrides=bad), parent_agent=None
|
||||
)
|
||||
assert creds["request_overrides"] is None
|
||||
|
||||
|
||||
def test_direct_branch_deep_copies_nested_extra_body():
|
||||
"""Transport-side mutation of the child's overrides must not leak back
|
||||
into the config dict (copy.deepcopy, not a shallow dict())."""
|
||||
source = {"extra_body": {"provider": {"sort": "throughput"}}}
|
||||
cfg = _cfg(request_overrides=source)
|
||||
creds = _resolve_delegation_credentials(cfg, parent_agent=None)
|
||||
creds["request_overrides"]["extra_body"]["provider"]["sort"] = "mutated"
|
||||
creds["request_overrides"]["extra_body"]["injected"] = True
|
||||
assert source == {"extra_body": {"provider": {"sort": "throughput"}}}
|
||||
|
||||
|
||||
@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
|
||||
def test_explicit_merges_over_runtime_on_provider_alongside_base_url(mock_resolve):
|
||||
"""Precedence on the provider-alongside-base_url path (#98237 interplay):
|
||||
explicit delegation.request_overrides merges OVER the named provider's
|
||||
runtime overrides — runtime extra_body keys survive unless redefined,
|
||||
explicit top-level keys win, and max_output_tokens is preserved."""
|
||||
mock_resolve.return_value = {
|
||||
"provider": "custom",
|
||||
"base_url": "https://provider-default.example/v1",
|
||||
"api_key": "provider-key",
|
||||
"api_mode": "chat_completions",
|
||||
"request_overrides": {
|
||||
"service_tier": "default",
|
||||
"extra_body": {"thinking": {"type": "disabled"}, "provider": {"sort": "price"}},
|
||||
},
|
||||
"max_output_tokens": 8192,
|
||||
}
|
||||
cfg = _cfg(
|
||||
provider="mimo",
|
||||
request_overrides={
|
||||
"service_tier": "flex",
|
||||
"extra_body": {"provider": {"sort": "throughput"}},
|
||||
},
|
||||
)
|
||||
creds = _resolve_delegation_credentials(cfg, parent_agent=None)
|
||||
assert creds["request_overrides"] == {
|
||||
# explicit top-level key wins
|
||||
"service_tier": "flex",
|
||||
"extra_body": {
|
||||
# runtime extra_body key survives (not redefined)
|
||||
"thinking": {"type": "disabled"},
|
||||
# explicit extra_body key wins over runtime's
|
||||
"provider": {"sort": "throughput"},
|
||||
},
|
||||
}
|
||||
assert creds["max_output_tokens"] == 8192
|
||||
|
||||
|
||||
# ── Branch 2: named provider (no base_url) ─────────────────────────────────
|
||||
|
||||
|
||||
@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
|
||||
def test_named_provider_branch_honors_explicit_key(mock_resolve):
|
||||
"""The named-provider branch merges the explicit key over the provider's
|
||||
runtime overrides — the config key never silently no-ops."""
|
||||
mock_resolve.return_value = {
|
||||
"provider": "openrouter",
|
||||
"base_url": "https://openrouter.ai/api/v1",
|
||||
"api_key": "runtime-key",
|
||||
"api_mode": "chat_completions",
|
||||
"request_overrides": {"extra_body": {"reasoning": {"enabled": True}}},
|
||||
"max_output_tokens": 4096,
|
||||
}
|
||||
cfg = {
|
||||
"model": "deepseek/deepseek-v4-flash-0731",
|
||||
"provider": "openrouter",
|
||||
"request_overrides": {"extra_body": {"provider": {"sort": "throughput"}}},
|
||||
}
|
||||
creds = _resolve_delegation_credentials(cfg, parent_agent=None)
|
||||
assert creds["request_overrides"] == {
|
||||
"extra_body": {
|
||||
"reasoning": {"enabled": True},
|
||||
"provider": {"sort": "throughput"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
|
||||
def test_named_provider_branch_without_explicit_key_unchanged(mock_resolve):
|
||||
"""Without the config key the named-provider branch behaves as before."""
|
||||
mock_resolve.return_value = {
|
||||
"provider": "openrouter",
|
||||
"base_url": "https://openrouter.ai/api/v1",
|
||||
"api_key": "runtime-key",
|
||||
"api_mode": "chat_completions",
|
||||
"request_overrides": {"extra_body": {"reasoning": {"enabled": True}}},
|
||||
"max_output_tokens": 4096,
|
||||
}
|
||||
cfg = {"model": "m", "provider": "openrouter"}
|
||||
creds = _resolve_delegation_credentials(cfg, parent_agent=None)
|
||||
assert creds["request_overrides"] == {"extra_body": {"reasoning": {"enabled": True}}}
|
||||
|
||||
|
||||
# ── Branch 3: parent-inherit (no provider, no base_url) ────────────────────
|
||||
|
||||
|
||||
def test_inherit_branch_honors_explicit_key_over_parent():
|
||||
"""Pure-inherit setups still apply delegation.request_overrides, merged
|
||||
over the parent agent's own request_overrides."""
|
||||
parent = _parent(
|
||||
request_overrides={
|
||||
"service_tier": "default",
|
||||
"extra_body": {"thinking": {"type": "disabled"}},
|
||||
}
|
||||
)
|
||||
cfg = {
|
||||
"model": "",
|
||||
"provider": "",
|
||||
"request_overrides": {"extra_body": {"provider": {"sort": "throughput"}}},
|
||||
}
|
||||
creds = _resolve_delegation_credentials(cfg, parent)
|
||||
assert creds["request_overrides"] == {
|
||||
"service_tier": "default",
|
||||
"extra_body": {
|
||||
"thinking": {"type": "disabled"},
|
||||
"provider": {"sort": "throughput"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_inherit_branch_without_key_stays_none():
|
||||
"""No explicit key and no parent overrides → None (old contract: the
|
||||
child's construction path falls back to the parent's request_overrides)."""
|
||||
parent = _parent(request_overrides=None)
|
||||
creds = _resolve_delegation_credentials({"model": "", "provider": ""}, parent)
|
||||
assert creds["request_overrides"] is None
|
||||
|
||||
|
||||
def test_inherit_branch_deep_copies_parent_overrides():
|
||||
"""Parent's nested overrides must be deep-copied on the inherit branch."""
|
||||
parent_overrides = {"extra_body": {"thinking": {"type": "disabled"}}}
|
||||
parent = _parent(request_overrides=parent_overrides)
|
||||
cfg = {
|
||||
"model": "",
|
||||
"provider": "",
|
||||
"request_overrides": {"extra_body": {"provider": {"sort": "throughput"}}},
|
||||
}
|
||||
creds = _resolve_delegation_credentials(cfg, parent)
|
||||
creds["request_overrides"]["extra_body"]["thinking"]["type"] = "mutated"
|
||||
assert parent_overrides == {"extra_body": {"thinking": {"type": "disabled"}}}
|
||||
|
||||
|
||||
# ── Merge helper unit tests ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_merge_helper_both_none():
|
||||
assert _merge_request_overrides(None, None) is None
|
||||
assert _merge_request_overrides({}, {}) is None
|
||||
assert _merge_request_overrides("junk", 42) is None
|
||||
|
||||
|
||||
def test_merge_helper_explicit_only():
|
||||
assert _merge_request_overrides(None, {"a": 1}) == {"a": 1}
|
||||
|
||||
|
||||
def test_merge_helper_runtime_only():
|
||||
assert _merge_request_overrides({"a": 1}, None) == {"a": 1}
|
||||
|
||||
|
||||
def test_merge_helper_explicit_top_level_wins():
|
||||
assert _merge_request_overrides({"a": 1, "b": 2}, {"a": 9}) == {"a": 9, "b": 2}
|
||||
|
||||
|
||||
def test_merge_helper_extra_body_one_level_merge():
|
||||
merged = _merge_request_overrides(
|
||||
{"extra_body": {"keep": 1, "clash": "runtime"}},
|
||||
{"extra_body": {"clash": "explicit", "new": 2}},
|
||||
)
|
||||
assert merged == {"extra_body": {"keep": 1, "clash": "explicit", "new": 2}}
|
||||
|
||||
|
||||
def test_merge_helper_non_dict_runtime_extra_body_replaced():
|
||||
merged = _merge_request_overrides(
|
||||
{"extra_body": "junk"}, {"extra_body": {"a": 1}}
|
||||
)
|
||||
assert merged == {"extra_body": {"a": 1}}
|
||||
|
||||
@@ -1994,9 +1994,17 @@ def _build_child_agent(
|
||||
provider_require_parameters=child_provider_require_parameters,
|
||||
provider_data_collection=child_provider_data_collection,
|
||||
request_overrides=(
|
||||
dict(override_request_overrides or {})
|
||||
if override_provider
|
||||
else dict(getattr(parent_agent, "request_overrides", {}) or {})
|
||||
# override_request_overrides is honored whenever set —
|
||||
# including the inherit branch (override_provider=None),
|
||||
# where _resolve_delegation_credentials already merged
|
||||
# delegation.request_overrides OVER the parent's values.
|
||||
dict(override_request_overrides)
|
||||
if override_request_overrides is not None
|
||||
else (
|
||||
{}
|
||||
if override_provider
|
||||
else dict(getattr(parent_agent, "request_overrides", {}) or {})
|
||||
)
|
||||
),
|
||||
openrouter_min_coding_score=child_openrouter_min_coding_score,
|
||||
tool_progress_callback=child_progress_cb,
|
||||
@@ -4456,6 +4464,43 @@ def _resolve_child_credential_pool(
|
||||
return None
|
||||
|
||||
|
||||
def _merge_request_overrides(runtime_overrides, explicit_overrides):
|
||||
"""Merge explicit ``delegation.request_overrides`` over runtime-derived ones.
|
||||
|
||||
Precedence contract: the explicit config key WINS over runtime-derived
|
||||
(provider-catalog or parent-inherited) overrides. Top-level keys from the
|
||||
explicit dict replace same-named runtime keys; the ``extra_body`` sub-dict
|
||||
is deep-merged ONE level — runtime ``extra_body`` keys survive unless the
|
||||
explicit dict redefines that exact key. This keeps provider personality
|
||||
(e.g. ``thinking: {type: disabled}``) intact while letting users layer
|
||||
routing hints (e.g. ``extra_body.provider = {"sort": "throughput"}``) on
|
||||
top.
|
||||
|
||||
Both inputs are deep-copied (``copy.deepcopy``) so transport-side mutation
|
||||
of the child's request kwargs can never leak back into the loaded config
|
||||
dict or the provider runtime cache.
|
||||
|
||||
Returns ``None`` when both sides are empty/non-dict.
|
||||
"""
|
||||
import copy as _copy
|
||||
|
||||
runtime_overrides = runtime_overrides if isinstance(runtime_overrides, dict) else None
|
||||
explicit_overrides = explicit_overrides if isinstance(explicit_overrides, dict) else None
|
||||
if not runtime_overrides and not explicit_overrides:
|
||||
return None
|
||||
merged = _copy.deepcopy(runtime_overrides) if runtime_overrides else {}
|
||||
explicit = _copy.deepcopy(explicit_overrides) if explicit_overrides else {}
|
||||
runtime_extra = merged.get("extra_body")
|
||||
explicit_extra = explicit.pop("extra_body", None)
|
||||
merged.update(explicit)
|
||||
if isinstance(runtime_extra, dict) and isinstance(explicit_extra, dict):
|
||||
runtime_extra.update(explicit_extra)
|
||||
merged["extra_body"] = runtime_extra
|
||||
elif explicit_extra is not None:
|
||||
merged["extra_body"] = explicit_extra
|
||||
return merged or None
|
||||
|
||||
|
||||
def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||
"""Resolve credentials for subagent delegation.
|
||||
|
||||
@@ -4483,6 +4528,18 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||
configured_api_key = str(cfg.get("api_key") or "").strip() or None
|
||||
configured_api_mode = str(cfg.get("api_mode") or "").strip().lower() or None
|
||||
|
||||
# delegation.request_overrides: explicit per-child request settings from
|
||||
# config. Honored on EVERY resolution branch (direct base_url, named
|
||||
# provider, and parent-inherit) so the key never silently no-ops.
|
||||
# Precedence: explicit merges OVER runtime/parent-derived overrides via
|
||||
# _merge_request_overrides (top-level explicit keys win; extra_body is
|
||||
# deep-merged one level). Non-dict values are ignored.
|
||||
explicit_request_overrides = (
|
||||
cfg.get("request_overrides")
|
||||
if isinstance(cfg.get("request_overrides"), dict)
|
||||
else None
|
||||
)
|
||||
|
||||
# Native-SDK providers (Bedrock, Vertex, Google GenAI) speak their own
|
||||
# wire protocol — they cannot be reached via OpenAI chat_completions against
|
||||
# a base_url. For these, always fall through to resolve_runtime_provider()
|
||||
@@ -4494,23 +4551,23 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||
_is_native_sdk_provider = _provider_lower in _NATIVE_SDK_PROVIDERS
|
||||
|
||||
if configured_base_url and not _is_native_sdk_provider:
|
||||
# delegation.request_overrides: same semantics as the named-provider
|
||||
# branch below (runtime.get("request_overrides")) — a dict merged into
|
||||
# the child's API kwargs by the transport's profile path. Keys are
|
||||
# top-level kwargs (e.g. service_tier); an "extra_body" sub-dict is
|
||||
# merged into extra_body. This is how a direct-endpoint delegation
|
||||
# (provider=custom) forwards OpenRouter routing hints such as
|
||||
# extra_body.provider = {"sort": "throughput"} to its children —
|
||||
# the child's CustomProfile does not emit provider preferences, and
|
||||
# the parent-inheritance path is deliberately cleared when
|
||||
# delegation.provider/base_url overrides the parent (see the
|
||||
# delegation.request_overrides: an explicit dict of per-child request
|
||||
# settings merged into the child's API kwargs by the transport's
|
||||
# profile path. Keys are top-level kwargs (e.g. service_tier); an
|
||||
# "extra_body" sub-dict is merged into extra_body. This is how a
|
||||
# direct-endpoint delegation (provider=custom) forwards OpenRouter
|
||||
# routing hints such as extra_body.provider = {"sort": "throughput"}
|
||||
# to its children — the child's CustomProfile does not emit provider
|
||||
# preferences, and the parent-inheritance path is deliberately cleared
|
||||
# when delegation.provider/base_url overrides the parent (see the
|
||||
# provider-preference clearing in _build_child_agent).
|
||||
configured_request_overrides = cfg.get("request_overrides")
|
||||
request_overrides = (
|
||||
dict(configured_request_overrides)
|
||||
if isinstance(configured_request_overrides, dict)
|
||||
else None
|
||||
)
|
||||
#
|
||||
# Precedence: explicit delegation.request_overrides MERGES OVER any
|
||||
# runtime-derived overrides (see _merge_request_overrides) — top-level
|
||||
# explicit keys win; extra_body is deep-merged one level so runtime
|
||||
# extra_body keys survive unless the explicit key redefines them.
|
||||
# (explicit_request_overrides is parsed once at the top of this
|
||||
# function and applied to every branch.)
|
||||
|
||||
# When delegation.api_key is not set, return None so _build_child_agent
|
||||
# falls back to the parent agent's API key via the credential inheritance
|
||||
@@ -4577,6 +4634,12 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||
exc,
|
||||
)
|
||||
|
||||
# Explicit delegation.request_overrides merges OVER the runtime-derived
|
||||
# overrides (explicit wins; extra_body deep-merged one level).
|
||||
request_overrides = _merge_request_overrides(
|
||||
request_overrides, explicit_request_overrides
|
||||
)
|
||||
|
||||
return {
|
||||
"model": configured_model,
|
||||
"provider": provider,
|
||||
@@ -4588,14 +4651,22 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||
}
|
||||
|
||||
if not configured_provider:
|
||||
# No provider override — child inherits everything from parent
|
||||
# No provider override — child inherits everything from parent.
|
||||
# delegation.request_overrides still applies: merge the explicit key
|
||||
# OVER the parent's own request_overrides so the config key works even
|
||||
# in pure-inherit setups (never a silent no-op). None when neither
|
||||
# side has values → _build_child_agent falls back to the parent's
|
||||
# request_overrides unchanged.
|
||||
return {
|
||||
"model": configured_model,
|
||||
"provider": None,
|
||||
"base_url": None,
|
||||
"api_key": None,
|
||||
"api_mode": None,
|
||||
"request_overrides": None,
|
||||
"request_overrides": _merge_request_overrides(
|
||||
getattr(parent_agent, "request_overrides", None),
|
||||
explicit_request_overrides,
|
||||
),
|
||||
"max_output_tokens": None,
|
||||
}
|
||||
|
||||
@@ -4639,7 +4710,13 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||
"base_url": runtime.get("base_url"),
|
||||
"api_key": api_key,
|
||||
"api_mode": runtime.get("api_mode"),
|
||||
"request_overrides": dict(runtime.get("request_overrides") or {}),
|
||||
# Explicit delegation.request_overrides merges OVER the named
|
||||
# provider's runtime overrides (explicit wins; extra_body deep-merged
|
||||
# one level) — same precedence as the direct-base_url branch above.
|
||||
"request_overrides": _merge_request_overrides(
|
||||
runtime.get("request_overrides"), explicit_request_overrides
|
||||
)
|
||||
or {},
|
||||
"max_output_tokens": runtime.get("max_output_tokens"),
|
||||
"command": runtime.get("command"),
|
||||
"args": list(runtime.get("args") or []),
|
||||
|
||||
@@ -2568,6 +2568,10 @@ delegation:
|
||||
# base_url: "http://localhost:1234/v1" # Direct OpenAI-compatible endpoint (takes precedence over provider)
|
||||
# api_key: "local-key" # API key for base_url (falls back to OPENAI_API_KEY)
|
||||
# api_mode: "" # Wire protocol for base_url: "chat_completions", "codex_responses", or "anthropic_messages". Empty = auto-detect from URL (e.g. /anthropic suffix → anthropic_messages). Set explicitly for non-standard endpoints the heuristic can't detect.
|
||||
# request_overrides: # Per-child request settings sent on every subagent API call (all resolution branches).
|
||||
# extra_body: # Merged into the request's extra_body — e.g. OpenRouter routing hints:
|
||||
# provider:
|
||||
# sort: throughput
|
||||
max_concurrent_children: 3 # Parallel children per batch (floor 1, no ceiling). Also via DELEGATION_MAX_CONCURRENT_CHILDREN env var.
|
||||
worktree_isolation: false # Give each child its own git worktree branched from HEAD (local backend + git repos only; inspired by Muse Code). See Subagent Delegation → Worktree Isolation.
|
||||
max_spawn_depth: 1 # Delegation tree depth cap (1-3, clamped). 1 = flat (default): parent spawns leaves that cannot delegate. 2 = orchestrator children can spawn leaf grandchildren. 3 = three levels.
|
||||
@@ -2578,6 +2582,19 @@ delegation:
|
||||
|
||||
**Direct endpoint override:** If you want the obvious custom-endpoint path, set `delegation.base_url`, `delegation.api_key`, and `delegation.model`. That sends subagents directly to that OpenAI-compatible endpoint and takes precedence over `delegation.provider`. If `delegation.api_key` is omitted, Hermes falls back to `OPENAI_API_KEY` only. When `delegation.provider` is set alongside `delegation.base_url`, the explicit endpoint and key still win, but that provider's request settings (`extra_body` overrides and max output tokens from your `custom_providers` entry) are carried into the subagent.
|
||||
|
||||
**Per-child request settings (`request_overrides`):** `delegation.request_overrides` is a dict of request settings sent on every subagent API call. Top-level keys are API kwargs (e.g. `service_tier`); an `extra_body` sub-dict is merged into the request's `extra_body`. It is honored on **all three** resolution branches — direct `base_url`, named `provider`, and pure inherit — so the key always takes effect. Precedence: explicit `request_overrides` values merge **over** any runtime- or parent-derived overrides — top-level explicit keys win, and `extra_body` is deep-merged one level so runtime `extra_body` keys (e.g. a provider's `thinking: {type: disabled}` personality) survive unless your key redefines them. The canonical use case is OpenRouter routing hints for delegation children:
|
||||
|
||||
```yaml
|
||||
delegation:
|
||||
model: "deepseek/deepseek-v4-flash-0731"
|
||||
base_url: "https://openrouter.ai/api/v1"
|
||||
api_key: "sk-or-..."
|
||||
request_overrides:
|
||||
extra_body:
|
||||
provider:
|
||||
sort: throughput # route children to the fastest OpenRouter provider
|
||||
```
|
||||
|
||||
**Wire protocol (`api_mode`):** Hermes auto-detects the wire protocol from `delegation.base_url` (e.g. paths ending in `/anthropic` → `anthropic_messages`; Codex / native Anthropic / Kimi-coding hostnames keep their existing detection). For endpoints the heuristic can't classify — for example Azure AI Foundry, MiniMax, Zhipu GLM, or LiteLLM proxies fronting an Anthropic-shaped backend — set `delegation.api_mode` explicitly to one of `chat_completions`, `codex_responses`, or `anthropic_messages`. Leave it empty (the default) to keep auto-detection.
|
||||
|
||||
The delegation provider uses the same credential resolution as CLI/gateway startup. All configured providers are supported: `openrouter`, `nous`, `copilot`, `zai`, `kimi-coding`, `minimax`, `minimax-cn`. When a provider is set, the system automatically resolves the correct base URL, API key, and API mode — no manual credential wiring needed.
|
||||
|
||||
@@ -188,7 +188,7 @@ delegation:
|
||||
provider: "openrouter" # optional: route children to a different provider
|
||||
```
|
||||
|
||||
Resolution order: `delegation.base_url` (direct endpoint) takes precedence, then `delegation.provider` (full credential bundle resolved via the runtime provider system), and when neither is set children inherit the parent's provider and credentials; `delegation.model` applies in all cases, and when it is empty children inherit the parent's model. Setting `delegation.provider` alongside `delegation.base_url` keeps the explicit endpoint but carries that provider's request overrides and max output tokens into the child.
|
||||
Resolution order: `delegation.base_url` (direct endpoint) takes precedence, then `delegation.provider` (full credential bundle resolved via the runtime provider system), and when neither is set children inherit the parent's provider and credentials; `delegation.model` applies in all cases, and when it is empty children inherit the parent's model. Setting `delegation.provider` alongside `delegation.base_url` keeps the explicit endpoint but carries that provider's request overrides and max output tokens into the child. An explicit `delegation.request_overrides` dict is honored on every branch and merges over those runtime-derived values (see [Configuration](#configuration) below).
|
||||
|
||||
Note that the pin is global: `delegate_task` has no per-task model parameter, so every child in a batch runs on the configured delegation model. For quality-sensitive subtasks that need a stronger model, either leave `delegation.model` unset for that session or hand the task to the [kanban board](kanban.md#per-task-model-override), which does support a per-task model override.
|
||||
|
||||
@@ -512,10 +512,23 @@ delegation:
|
||||
base_url: "http://localhost:1234/v1"
|
||||
api_key: "local-key"
|
||||
# api_mode: "anthropic_messages" # Optional. Wire protocol override for base_url ("chat_completions", "codex_responses", or "anthropic_messages"). Empty = auto-detect from URL (e.g. /anthropic suffix). Set explicitly for endpoints the heuristic can't classify (Azure AI Foundry, MiniMax, Zhipu GLM, LiteLLM proxies, …).
|
||||
|
||||
# Send per-child request settings on every subagent API call — e.g. OpenRouter
|
||||
# routing hints when delegating straight to openrouter.ai via base_url:
|
||||
delegation:
|
||||
model: "deepseek/deepseek-v4-flash-0731"
|
||||
base_url: "https://openrouter.ai/api/v1"
|
||||
api_key: "sk-or-..."
|
||||
request_overrides:
|
||||
extra_body:
|
||||
provider:
|
||||
sort: throughput # children route to the fastest OpenRouter provider
|
||||
```
|
||||
|
||||
When `base_url` points at an Anthropic-compatible endpoint — for example a path ending in `/anthropic`, an Azure Foundry Claude route, or a MiniMax `/anthropic` proxy — `api_mode` is auto-detected as `anthropic_messages` so the subagent uses the right wire format without you setting anything. Set `api_mode` explicitly when the auto-detection guess is wrong (rare).
|
||||
|
||||
`delegation.request_overrides` works on **all three** resolution branches — direct `base_url`, named `provider`, and pure inherit — so it always takes effect. Top-level keys are API kwargs (e.g. `service_tier`); an `extra_body` sub-dict is merged into the request's `extra_body`. Explicit values merge **over** runtime- or parent-derived overrides: explicit top-level keys win, and `extra_body` is deep-merged one level, so a provider's own request personality (e.g. `thinking: {type: disabled}`) survives unless your key redefines it. See [Configuration → Delegation](../configuration.md#delegation) for details.
|
||||
|
||||
:::tip
|
||||
The agent handles delegation automatically based on the task complexity. You don't need to explicitly ask it to delegate — it will do so when it makes sense.
|
||||
:::
|
||||
|
||||
Reference in New Issue
Block a user