feat(delegate): add per-task model/provider routing#36790
Conversation
mxnstrexgl
left a comment
There was a problem hiding this comment.
🤖 Automated PR Review
Security Scan
- ✓ No hardcoded secrets, injection sinks, unsafe deserialization, or dependency red flags found by this automated scan.
Code Quality
- ✓ No blocking code-quality issues found by this automated scan.
Summary
Status: APPROVE — security findings: 0, quality suggestions: 0.
Automated review; raw diff content intentionally omitted.
✅ Rebasse vérifié — prêt à mergerJ'ai cherry-pické cette PR il y a plusieurs jours et je l'utilise en production sur mon fork. Fonctionne parfaitement. Aujourd'hui, j'ai vérifié le rebase sur le
Branche rebasée disponible ici : La PR répond à un vrai besoin (cf. issues #5012, #6306, #10995 toutes marquées comme fixed by this PR). Le code est approuvé par @mxnstrexgl, les tests passent, le rebase est clean. Qu'est-ce qui bloque encore le merge ? |
|
Great work on this — the approach of routing through the /model switch pipeline is architecturally the right call. It means aliases, custom providers, and --provider inline syntax all work without parallel resolution logic. That's cleaner than both the synthetic config-dict approach in #34773 and a custom resolver I was sketching out. I run a production Hermes instance that uses delegate_task daily (deepseek-v4-flash as the daily driver, pro-tier models on explicit request). I'd be happy to pull your branch and test it against real workloads — Matrix-based single-task delegation, mixed-model batch delegation, and the model-only-on-same-provider case. A few questions as I review:
Happy to dig into the branch and report back. |
|
@impiyush Thanks for the thoughtful review and for offering to test this against real workloads.
If you hit anything unexpected, a minimal Also thanks @gskeyzer-web for rebasing and validating this on current |
Allow individual tasks in the array to specify their own and/or , overriding the global delegation config for that task only. When a per-task override is present, is called with a task-scoped config so that base_url, api_key, and api_mode are derived correctly from the per-task provider — not the global delegation config. Tasks without overrides fall back to the pre-resolved dict as before (no regression for existing usage). Changes: - : add and fields to the per-task object inside array - loop: resolve per-task credentials when override is present; otherwise reuse global (zero overhead) Closes NousResearch#35437 Related: NousResearch#34489, NousResearch#31537, NousResearch#36790, NousResearch#30388, NousResearch#37966
|
@jmche Thanks for working on this — I smoke-tested the patch on my self-hosted Hermes instance (v0.16.0). Assuming I am patching it correctly - the batch path works (resolves to 401 auth issue though), but the single-goal path is not applying model overrides. Environment:
Test Results:
The pattern is consistent across three different test models ( This suggests the normalization at the single-goal entry point (where Also worth noting: the batch path had auth failures (401) for all models including Let me know how I can help you with this, or if I am doing something incorrectly on my side. |
🔄 Rebase + extension vérifiés en productionJe confirme les observations de @impiyush. Le bug du top-level Tests réels (production, 5+ jours)
Extension :
|
|
Thank you @gskeyzer-web for confirming my results and pointing me in the right direction. Here are my updated smoke test results for this PR. Retested after correcting my call pattern. The handler code is correct — the earlier "single-goal override not applied" was a red herring on my end. Environment (same as before):
Results:
Root cause of my earlier false report: The calling LLM drops optional top-level params like model/provider from the JSON tool call when they're at the top level of delegate_task(). The batch shape delegate_task(tasks=[{...}]) works because the same model reliably includes model inside dict elements. Not a code bug — a serialization behavior difference. Overall: Code is good and does the job. tasks=[{...}] works reliably for single-task and mixed-batch delegation. The other learning is to always explicitly set the 3 properties in delegate task - goal, model and provider. I did run into an issue when the provider was not explicitly specified for Kimi-K2.6 model and hermes defaulted to using the Kimi-Coding or Moonshot AI api keys, which were not set. This issue has been a blocker for me to run subagents correctly with different models and am glad this PR will fix it. Thanks again @jmche & @gskeyzer-web. LGTM!! |
impiyush
left a comment
There was a problem hiding this comment.
Refer to my latest comment on this PR's conversation.
…ousResearch#36790) Cherry-pick du commit 7d9432d de jmche: - Ajoute les parametres model/provider a delegate_task() - Ajoute les champs model/provider dans tasks[] - Resolution par priorite: tasks[] > top-level > config - Tests unitaires passes Source: NousResearch#36790
Add reasoning_effort parameter to delegate_task() — top-level and per-task in tasks[] array. Supported values: none, minimal, low, medium, high, xhigh (matching parse_reasoning_effort). Bypasses delegation.reasoning_effort config and parent inheritance when explicitly set. Extends PR NousResearch#36790 cherry-pick (6d6988e75 + 9583b8079).
|
Thanks @impiyush and @gskeyzer-web for the production validation and for narrowing down the call-shape issue. I agree with the practical conclusion: for JSON tool calls, the most reliable shape is to put routing fields inside delegate_task(tasks=[{
"goal": "...",
"provider": "...",
"model": "..."
}])To keep this shape from becoming too loose, the docs should recommend structured fields: The handler supports top-level On Given the additional smoke testing and approval, I think this PR is ready from the implementation side. Maintainers: would you like me to rebase/update this branch against current |
Summary
Adds per-call and per-task model/provider routing to
delegate_task.This lets delegated subagents run on different models in the same batch, while preserving the existing fallback behavior when no override is provided.
Fixes #6306
Fixes #10995
Fixes #5012
Related to #7586, #6771, #12715, #20000, #23266, #34773, and #35033. Also addresses the
delegate_taskportion discussed in #18880.Builds on behavior already fixed by merged PRs #17587 and #19623.
Design
Model/provider precedence is:
tasks[].model/tasks[].providermodel/providerdelegation.model/delegation.providerin configThe override resolver reuses the existing
/modelswitch pipeline, so aliases, provider catalogs, custom providers, and--providersyntax stay consistent.Both forms are supported:
For JSON tool calls, the structured
providerfield is preferred. If a structured provider conflicts with an inline--providerinmodel,delegate_taskreturns a tool error instead of silently choosing one.Relation to Other PRs
There are several overlapping PRs in this feature cluster. This PR intentionally includes both top-level and per-task routing because #6306, #10995, and #5012 request single-call shorthand as well as mixed-model batch delegation.
Compared with the narrower per-task-only implementations, this PR also routes explicit overrides through the existing
/modelswitch pipeline. That keeps alias handling, provider catalog lookup, custom providers, and inline--providersyntax consistent with the rest of Hermes instead of adding a parallel resolver.This PR does not expose per-task
base_urlorapi_key; provider credentials still come from configured Hermes providers.Changes
modelandproviderparameters todelegate_task.modelandproviderfields intasks[].modelandproviderthrough the registry handler.Verification
Automated:
Manual Hermes verification with real models:
Parent model:
opencode-go/deepseek-v4-proParallel child models:
gemini/gemini-3.5-flash->CHILD_OK_GEMINIopencode-go/mimo-v2.5-pro->CHILD_OK_MIMOopencode-go/qwen3.6-plus->CHILD_OK_QWENResult:
3/3 completed, total wall time about6.36s.