diff --git a/run_agent.py b/run_agent.py index 3a75d040d3d2..b5e8f21ce36e 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4952,6 +4952,8 @@ def _invoke_tool(self, function_name: str, function_args: dict, effective_task_i toolsets=function_args.get("toolsets"), tasks=function_args.get("tasks"), max_iterations=function_args.get("max_iterations"), + model=function_args.get("model"), + provider=function_args.get("provider"), parent_agent=self, ) else: @@ -5302,6 +5304,8 @@ def _execute_tool_calls_sequential(self, assistant_message, messages: list, effe toolsets=function_args.get("toolsets"), tasks=tasks_arg, max_iterations=function_args.get("max_iterations"), + model=function_args.get("model"), + provider=function_args.get("provider"), parent_agent=self, ) _delegate_result = function_result diff --git a/skills/autonomous-ai-agents/model-routing-template/SKILL.md b/skills/autonomous-ai-agents/model-routing-template/SKILL.md new file mode 100644 index 000000000000..446076fd3f0e --- /dev/null +++ b/skills/autonomous-ai-agents/model-routing-template/SKILL.md @@ -0,0 +1,164 @@ +--- +name: model-routing-template +description: Generic template for strategic model delegation. Customize the provider catalog for your setup, then use the decision framework to route tasks to the right model based on cost, capability, and complexity. +version: 1.0.0 +author: Agatha (Hermes Agent) +license: MIT +metadata: + hermes: + tags: [delegation, model-selection, cost-optimization, subagents, template] + related_skills: [autonomous-ai-agents, plan] +--- + +# Model Routing Strategy (Template) + +Framework for delegating subagent tasks to the right model. Fill in your +provider catalog in `references/providers.yaml`, then follow the decision +tree to route tasks based on cost, capability, and complexity. + +## Quick Start + +1. Copy `references/providers-example.yaml` to `references/providers.yaml` +2. Edit it with YOUR providers, models, costs, and roles +3. Follow the Decision Framework below when delegating + +## Your Provider Catalog + +Edit `references/providers.yaml` to define your available models. +See `references/providers-example.yaml` for a fully worked example. + +## Decision Framework + +### Step 1: Should I delegate at all? + +**Handle yourself (no delegation) when:** +- Routine tasks you can do well (file edits, simple commands, config changes) +- Tasks requiring your personality/style (chatting with the user, channel responses) +- Anything under 3 tool calls +- Quick lookups and formatting + +**Delegate when:** +- The task needs a genuinely different capability (deep reasoning, second opinion, vision) +- You're going in circles and need fresh perspective +- Parallel work would save real time (batch mode) +- The user explicitly asks for a specific model + +### Step 2: Which model? + +Follow the tiers defined in your `providers.yaml`. General principles: + +**TIER 0 — Handle yourself** +- Default for everything. Don't over-delegate. + +**TIER 1 — Cheap/Free (your bulk workhorses)** +- Models that cost nothing or nearly nothing +- Use for: subagent tasks, parallel execution, exploration, tight-scope execution +- Burn freely — these exist to be used at scale + +**TIER 2 — Standard (your reliable specialists)** +- Capable models at moderate cost +- Use for: second opinions, code review, architecture, tasks that need more brain +- Each call is a conscious spend — use when Tier 1 isn't enough + +**TIER 3 — Expensive (frontier models, last resort)** +- The most capable but most costly models +- Use ONLY when: + - You've exhausted cheaper options and still can't crack it + - Going in circles on a genuinely hard problem + - The user explicitly requests it + - Decisions with serious consequences (production, security, money) +- NEVER self-select without first trying cheaper options + +### Step 3: How many? + +**Parallel patterns:** +- **Junior army** — many cheap models with tight plans, running in parallel +- **Scout party** — send cheap models first, expensive models as safety net +- **Crowd wisdom** — multiple standard models for diverse perspectives +- **Rule of thumb**: N cheap opinions < 1 expensive opinion (if the cheap ones can deliver) + +### Step 4: When the user is engaged + +**User is in the conversation and invested:** +- Present the tradeoff, don't just spend. It's their budget. +- Before escalating beyond standard tier, offer the choice: + "I've got inputs from X and Y. I could add Z for another perspective, + or if this warrants it we could go to [expensive model]. Your call." +- The user may know context you don't. + +**User is NOT around (autonomous mode):** +- Follow the escalation ladder strictly. No asking. +- Start cheap, escalate only when genuinely stuck. +- Never skip steps. + +## Escalation Ladder + +Adapt this to your provider catalog. General pattern: + +``` +1. Try yourself (free) +2. Cheap model for the task type (~0.1x) +3. Different cheap perspective (~0.1x) +4. Standard model — second opinion (1x) +5. Different standard model (1x) +6. Specialist model (1x) +7. Multiple models in parallel (1x each) +8. Frontier/expensive model (3x+) + └── ONLY after all above failed +``` + +## Cost-Effective Patterns + +### The "Many for One" Pattern +Instead of one expensive call, get multiple diverse opinions at standard cost: +``` +3x standard model = 1x expensive model +But you get 3 different perspectives instead of 1. +``` + +### The "Free Stack" Pattern +Maximize throughput at minimal cost: +``` +1 heavy analysis (cheap tier, deep thinker) +1-2 explorers (cheap tier, fast scouts) +N junior executors (cheapest tier, tight plans) +``` + +### The "Scout Party" Pattern +Send expendable models first, safety net last: +``` +2+ cheapest models → investigate (might succeed) +1 slightly better model → safety net (only if needed) +``` +If the cheap models already returned good intel, skip the safety net. + +## Roles (define in your catalog) + +Each model should have a clear role. Common patterns: + +| Role | Description | Example | +|------|-------------|---------| +| Coordinator | You yourself — orchestration, chat, routing | The agent's default model | +| Heavy Hitter | Strong reasoning + coding, near-frontier | GLM-5, GPT-5.4 | +| Explorer | Fast scouting, broad searches, codebase recon | GLM-4.7-flash, Gemini Flash | +| Fixer | Tight plan, narrow scope, parallel execution | GLM-4.6, GPT-4.1 | +| Designer | Frontend/UI specialist, polished output | Claude Sonnet | +| Coder | Pure coding specialist, architecture reviews | GPT-5.3-Codex | +| Safety Net | Last-resort scout when others fail | Claude Haiku | +| Frontier | Nuclear option, absolute last resort | Claude Opus | + +## User Override Phrases + +Define natural-language triggers in your catalog. Examples: +- "use opus" / "use sonnet" — explicit model selection +- "get a second opinion" — standard tier model +- "think with me" — escalate beyond cheap tier +- "burn the budget" — clearance for expensive model + +## Notes + +- Cost multipliers are often per-request, not per-token. Check your provider. +- Some providers have monthly quotas, others are per-token. Track accordingly. +- The cheapest model that CAN do the task IS the right choice. +- "Expensive" doesn't mean "better for everything" — match capability to task. +- When in doubt, start at Tier 1 and escalate only with evidence. diff --git a/skills/autonomous-ai-agents/model-routing-template/references/providers-example.yaml b/skills/autonomous-ai-agents/model-routing-template/references/providers-example.yaml new file mode 100644 index 000000000000..3114fbf8583e --- /dev/null +++ b/skills/autonomous-ai-agents/model-routing-template/references/providers-example.yaml @@ -0,0 +1,127 @@ +# Provider Catalog — EXAMPLE +# Copy this to providers.yaml and customize for your setup. +# +# This example shows a multi-provider setup with cost tiers. +# Adjust model names, costs, and roles to match YOUR configuration. + +providers: + # ── Provider: copilot (GitHub Copilot) ────────────────── + # Billing: monthly quota with per-request multipliers + copilot: + billing: "monthly quota" + models: + - id: "gpt-4.1" + cost: 0x + tier: FREE + role: "Fixer" + notes: "Grunt work, simple codegen" + - id: "gpt-4o" + cost: 0x + tier: FREE + role: "Fixer" + notes: "General tasks" + - id: "gpt-5-mini" + cost: 0x + tier: FREE + role: "Fixer" + notes: "Quick tasks" + - id: "claude-haiku-4.5" + cost: 0.33x + tier: CHEAP + role: "Safety Net" + notes: "Scout — send after 2+ cheaper models" + - id: "gpt-5.4" + cost: 1x + tier: STANDARD + role: "Heavy Hitter" + notes: "Price/value frontier champion" + - id: "claude-sonnet-4.6" + cost: 1x + tier: STANDARD + role: "Designer" + notes: "Second opinion + frontend design specialist" + - id: "gpt-5.3-codex" + cost: 1x + tier: STANDARD + role: "Coder" + notes: "Coding specialist, architecture" + - id: "claude-opus-4.8" + cost: 3x + tier: EXPENSIVE + role: "Frontier" + notes: "Last resort. Never self-select without trying cheaper options first." + - id: "claude-opus-4.8-fast" + cost: 30x + tier: NUCLEAR + role: "Frontier" + notes: "NEVER USE. Blocked by policy." + + # ── Provider: zai (Z.AI / GLM) ───────────────────────── + # Billing: subscription with generous limits (~0.1x equivalent) + zai: + billing: "subscription (effectively unlimited)" + models: + - id: "glm-5-turbo" + cost: ~0.1x + tier: CHEAP + role: "Coordinator" + notes: "The agent itself. Built for orchestration." + - id: "glm-5" + cost: ~0.25x + tier: CHEAP + role: "Heavy Hitter" + notes: "Near-frontier. Strong coder AND deep thinker." + - id: "glm-4.7-flash" + cost: ~0.1x + tier: CHEAP + role: "Explorer" + notes: "Fast scouting, codebase recon" + - id: "glm-4.6" + cost: ~0.1x + tier: CHEAP + role: "Fixer" + notes: "Tight-plan execution. 20 parallel, no token limits. NOT open-ended." + - id: "glm-4.6v" + cost: ~0.1x + tier: CHEAP + role: "Fixer" + notes: "Vision variant — image analysis" + +# ── User Override Phrases ──────────────────────────────── +# Map natural language to routing actions. +overrides: + "use opus": { provider: "copilot", model: "claude-opus-4.8" } + "use sonnet": { provider: "copilot", model: "claude-sonnet-4.6" } + "second opinion": { provider: "copilot", model: "claude-sonnet-4.6" } + "coding specialist": { provider: "copilot", model: "gpt-5.3-codex" } + "think with me": { action: "escalate", min_tier: "STANDARD" } + "burn the budget": { action: "escalate", min_tier: "EXPENSIVE" } + +# ── Patterns ───────────────────────────────────────────── +# Named delegation patterns for common scenarios. +patterns: + free-stack: + description: "Maximum throughput at minimal cost" + tasks: + - { goal: "Deep analysis...", provider: "zai", model: "glm-5" } + - { goal: "Explore subsystem...", provider: "zai", model: "glm-4.7-flash" } + - { goal: "Apply refactor A...", provider: "zai", model: "glm-4.6" } + - { goal: "Apply refactor B...", provider: "zai", model: "glm-4.6" } + total_cost: "~0.55x" + + scout-party: + description: "2:1 cheap models to safety net, safety net last" + rule: "Send 3+ cheapest first, safety net only if needed" + tasks: + - { goal: "Investigate X...", provider: "zai", model: "glm-4.6" } + - { goal: "Investigate Y...", provider: "zai", model: "glm-4.6" } + - { goal: "Investigate Z...", provider: "zai", model: "glm-4.6" } + - { goal: "Safety net...", provider: "copilot", model: "claude-haiku-4.5" } + + three-for-one: + description: "3 diverse 1x opinions for the cost of 1 expensive call" + tasks: + - { goal: "Review architecture...", provider: "copilot", model: "claude-sonnet-4.6" } + - { goal: "Review architecture...", provider: "copilot", model: "gpt-5.4" } + - { goal: "Review architecture...", provider: "copilot", model: "gpt-5.3-codex" } + total_cost: "3x (= 1 Opus call, but 3 perspectives)" diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 1a779f8a0bb7..dc6d56edd1d8 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -877,5 +877,254 @@ def test_model_only_no_provider_inherits_parent_credentials(self, mock_creds, mo self.assertEqual(kwargs["base_url"], parent.base_url) +class TestPerCallModelProviderOverride(unittest.TestCase): + """Tests for per-call model/provider override in delegate_task.""" + + def test_schema_includes_model_and_provider(self): + """Schema should expose model and provider as top-level properties.""" + props = DELEGATE_TASK_SCHEMA["parameters"]["properties"] + self.assertIn("model", props) + self.assertIn("provider", props) + self.assertEqual(props["model"]["type"], "string") + self.assertEqual(props["provider"]["type"], "string") + + def test_batch_task_schema_includes_model_and_provider(self): + """Batch task items should also support model and provider overrides.""" + task_props = DELEGATE_TASK_SCHEMA["parameters"]["properties"]["tasks"]["items"]["properties"] + self.assertIn("model", task_props) + self.assertIn("provider", task_props) + + @patch("tools.delegate_tool._load_config") + @patch("tools.delegate_tool._resolve_delegation_credentials") + def test_per_call_model_override_reaches_credentials(self, mock_creds, mock_cfg): + """Top-level model param should be passed to credential resolution.""" + mock_cfg.return_value = {"max_iterations": 50} + mock_creds.return_value = { + "model": "google/gemini-3-flash-preview", + "provider": None, + "base_url": None, + "api_key": None, + "api_mode": None, + } + parent = _make_mock_parent(depth=0) + + with patch("run_agent.AIAgent") as MockAgent: + mock_child = MagicMock() + mock_child.run_conversation.return_value = { + "final_response": "done", "completed": True, "api_calls": 1 + } + MockAgent.return_value = mock_child + + delegate_task(goal="Test", model="google/gemini-3-flash-preview", parent_agent=parent) + + # Credentials should have been called with the per-call override + mock_creds.assert_called_once() + call_kwargs = mock_creds.call_args.kwargs + self.assertEqual(call_kwargs["override_model"], "google/gemini-3-flash-preview") + self.assertIsNone(call_kwargs["override_provider"]) + + @patch("tools.delegate_tool._load_config") + @patch("tools.delegate_tool._resolve_delegation_credentials") + def test_per_call_provider_override_reaches_credentials(self, mock_creds, mock_cfg): + """Top-level provider param should be passed to credential resolution.""" + mock_cfg.return_value = {"max_iterations": 50} + mock_creds.return_value = { + "model": None, + "provider": "openrouter", + "base_url": "https://openrouter.ai/api/v1", + "api_key": "sk-or-key", + "api_mode": "chat_completions", + } + parent = _make_mock_parent(depth=0) + + with patch("run_agent.AIAgent") as MockAgent: + mock_child = MagicMock() + mock_child.run_conversation.return_value = { + "final_response": "done", "completed": True, "api_calls": 1 + } + MockAgent.return_value = mock_child + + delegate_task(goal="Test", provider="openrouter", parent_agent=parent) + + call_kwargs = mock_creds.call_args.kwargs + self.assertEqual(call_kwargs["override_provider"], "openrouter") + self.assertIsNone(call_kwargs["override_model"]) + + @patch("tools.delegate_tool._load_config") + @patch("tools.delegate_tool._resolve_delegation_credentials") + def test_batch_per_task_model_override(self, mock_creds, mock_cfg): + """In batch mode, each task's model overrides the top-level model.""" + mock_cfg.return_value = {"max_iterations": 50} + mock_creds.return_value = { + "model": "meta-llama/llama-4-scout", + "provider": "openrouter", + "base_url": "https://openrouter.ai/api/v1", + "api_key": "sk-or-key", + "api_mode": "chat_completions", + } + parent = _make_mock_parent(depth=0) + + with patch("tools.delegate_tool._build_child_agent") as mock_build, \ + patch("tools.delegate_tool._run_single_child") as mock_run: + mock_build.return_value = MagicMock() + mock_run.return_value = { + "task_index": 0, "status": "completed", + "summary": "Done", "api_calls": 1, "duration_seconds": 1.0 + } + + tasks = [ + {"goal": "Task A", "model": "google/gemini-3-flash-preview"}, + {"goal": "Task B", "model": "meta-llama/llama-4-scout"}, + ] + delegate_task(tasks=tasks, parent_agent=parent) + + # Should have resolved credentials twice with different models + self.assertEqual(mock_creds.call_count, 2) + call_0_kwargs = mock_creds.call_args_list[0].kwargs + call_1_kwargs = mock_creds.call_args_list[1].kwargs + self.assertEqual(call_0_kwargs["override_model"], "google/gemini-3-flash-preview") + self.assertEqual(call_1_kwargs["override_model"], "meta-llama/llama-4-scout") + + @patch("tools.delegate_tool._load_config") + @patch("tools.delegate_tool._resolve_delegation_credentials") + def test_task_level_overrides_top_level(self, mock_creds, mock_cfg): + """Task-level model/provider should override top-level model/provider.""" + mock_cfg.return_value = {"max_iterations": 50} + mock_creds.return_value = { + "model": "google/gemini-3-flash-preview", + "provider": "openrouter", + "base_url": "https://openrouter.ai/api/v1", + "api_key": "sk-or-key", + "api_mode": "chat_completions", + } + parent = _make_mock_parent(depth=0) + + with patch("tools.delegate_tool._build_child_agent") as mock_build, \ + patch("tools.delegate_tool._run_single_child") as mock_run: + mock_build.return_value = MagicMock() + mock_run.return_value = { + "task_index": 0, "status": "completed", + "summary": "Done", "api_calls": 1, "duration_seconds": 1.0 + } + + tasks = [ + {"goal": "Task A"}, # should use top-level + {"goal": "Task B", "model": "deepseek/deepseek-r1"}, # should override + ] + delegate_task(tasks=tasks, model="google/gemini-3-flash-preview", parent_agent=parent) + + self.assertEqual(mock_creds.call_count, 2) + # Task A: no task-level model, should get top-level + self.assertEqual(mock_creds.call_args_list[0].kwargs["override_model"], "google/gemini-3-flash-preview") + # Task B: has task-level model, should override top-level + self.assertEqual(mock_creds.call_args_list[1].kwargs["override_model"], "deepseek/deepseek-r1") + + def test_resolve_credentials_per_call_model_overrides_config(self): + """Per-call override_model takes precedence over config model.""" + parent = _make_mock_parent(depth=0) + cfg = {"model": "config-model", "provider": ""} + creds = _resolve_delegation_credentials( + cfg, parent, override_model="per-call-model" + ) + self.assertEqual(creds["model"], "per-call-model") + + @patch("hermes_cli.runtime_provider.resolve_runtime_provider") + def test_resolve_credentials_per_call_provider_overrides_config(self, mock_resolve): + """Per-call override_provider takes precedence over config provider.""" + mock_resolve.return_value = { + "provider": "per-call-provider", + "base_url": "https://per-call.example.com/v1", + "api_key": "sk-test-key", + "api_mode": "chat_completions", + } + parent = _make_mock_parent(depth=0) + cfg = {"model": "", "provider": "config-provider"} + creds = _resolve_delegation_credentials( + cfg, parent, override_provider="per-call-provider" + ) + self.assertEqual(creds["provider"], "per-call-provider") + mock_resolve.assert_called_once_with(requested="per-call-provider") + + @patch("hermes_cli.runtime_provider.resolve_runtime_provider") + def test_resolve_credentials_config_fallback_when_no_override(self, mock_resolve): + """When no per-call override, config values are used.""" + mock_resolve.return_value = { + "provider": "config-provider", + "base_url": "https://config.example.com/v1", + "api_key": "sk-test-key", + "api_mode": "chat_completions", + } + parent = _make_mock_parent(depth=0) + cfg = {"model": "config-model", "provider": "config-provider"} + creds = _resolve_delegation_credentials(cfg, parent) + self.assertEqual(creds["model"], "config-model") + self.assertEqual(creds["provider"], "config-provider") + mock_resolve.assert_called_once_with(requested="config-provider") + + @patch("hermes_cli.runtime_provider.resolve_runtime_provider") + def test_resolve_credentials_none_override_does_not_override_config(self, mock_resolve): + """Passing override_model=None should not clear config model.""" + mock_resolve.return_value = { + "provider": "config-provider", + "base_url": "https://config.example.com/v1", + "api_key": "sk-test-key", + "api_mode": "chat_completions", + } + parent = _make_mock_parent(depth=0) + cfg = {"model": "config-model", "provider": "config-provider"} + creds = _resolve_delegation_credentials( + cfg, parent, override_model=None, override_provider=None + ) + self.assertEqual(creds["model"], "config-model") + self.assertEqual(creds["provider"], "config-provider") + mock_resolve.assert_called_once_with(requested="config-provider") + + @patch("tools.delegate_tool._load_config") + @patch("tools.delegate_tool._resolve_delegation_credentials") + def test_credential_error_with_per_call_override(self, mock_creds, mock_cfg): + """Per-call override that fails credential resolution returns JSON error.""" + mock_cfg.return_value = {"max_iterations": 50} + mock_creds.side_effect = ValueError("Cannot resolve provider 'bad-provider'") + parent = _make_mock_parent(depth=0) + + result = json.loads(delegate_task( + goal="Test", provider="bad-provider", parent_agent=parent + )) + self.assertIn("error", result) + self.assertIn("bad-provider", result["error"]) + + @patch("tools.delegate_tool._load_config") + @patch("tools.delegate_tool._resolve_delegation_credentials") + def test_no_override_falls_back_to_config_credentials(self, mock_creds, mock_cfg): + """Without per-call overrides, config delegation settings are used.""" + mock_cfg.return_value = { + "max_iterations": 45, + "model": "google/gemini-3-flash-preview", + "provider": "openrouter", + } + mock_creds.return_value = { + "model": "google/gemini-3-flash-preview", + "provider": "openrouter", + "base_url": "https://openrouter.ai/api/v1", + "api_key": "sk-or-key", + "api_mode": "chat_completions", + } + parent = _make_mock_parent(depth=0) + + with patch("run_agent.AIAgent") as MockAgent: + mock_child = MagicMock() + mock_child.run_conversation.return_value = { + "final_response": "done", "completed": True, "api_calls": 1 + } + MockAgent.return_value = mock_child + + delegate_task(goal="Test", parent_agent=parent) + + # Should pass None overrides, letting config values through + call_kwargs = mock_creds.call_args.kwargs + self.assertIsNone(call_kwargs["override_model"]) + self.assertIsNone(call_kwargs["override_provider"]) + + if __name__ == "__main__": unittest.main() diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 3608b8d56682..0565aba849bb 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -401,6 +401,8 @@ def delegate_task( toolsets: Optional[List[str]] = None, tasks: Optional[List[Dict[str, Any]]] = None, max_iterations: Optional[int] = None, + model: Optional[str] = None, + provider: Optional[str] = None, parent_agent=None, ) -> str: """ @@ -411,6 +413,13 @@ def delegate_task( - Batch: provide tasks array [{goal, context, toolsets}, ...] Returns JSON with results array, one entry per task. + + Per-call model/provider override: + - model/provider at the top level apply to all tasks (single or batch). + - In batch mode, each task item can specify its own model/provider, + which override the top-level values for that task. + - Per-call values take precedence over config.yaml delegation settings. + - Config values are used as fallback when nothing is specified at call time. """ if parent_agent is None: return json.dumps({"error": "delegate_task requires a parent agent context."}) @@ -425,21 +434,11 @@ def delegate_task( ) }) - # Load config + # Load config (used as fallback when per-call values are not set) cfg = _load_config() default_max_iter = cfg.get("max_iterations", DEFAULT_MAX_ITERATIONS) effective_max_iter = max_iterations or default_max_iter - # Resolve delegation credentials (provider:model pair). - # When delegation.provider is configured, this resolves the full credential - # bundle (base_url, api_key, api_mode) via the same runtime provider system - # used by CLI/gateway startup. When unconfigured, returns None values so - # children inherit from the parent. - try: - creds = _resolve_delegation_credentials(cfg, parent_agent) - except ValueError as exc: - return json.dumps({"error": str(exc)}) - # Normalize to task list if tasks and isinstance(tasks, list): task_list = tasks[:MAX_CONCURRENT_CHILDREN] @@ -475,6 +474,20 @@ def delegate_task( children = [] try: for i, t in enumerate(task_list): + # Per-call overrides: task-level > top-level > config + task_model = t.get("model") or model or None + task_provider = t.get("provider") or provider or None + + # Resolve credentials with per-call overrides merged into config + try: + creds = _resolve_delegation_credentials( + cfg, parent_agent, + override_model=task_model, + override_provider=task_provider, + ) + except ValueError as exc: + return json.dumps({"error": str(exc)}) + child = _build_child_agent( task_index=i, goal=t["goal"], context=t.get("context"), toolsets=t.get("toolsets") or toolsets, model=creds["model"], @@ -562,9 +575,19 @@ def delegate_task( }, ensure_ascii=False) -def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict: +def _resolve_delegation_credentials( + cfg: dict, + parent_agent, + override_model: Optional[str] = None, + override_provider: Optional[str] = None, +) -> dict: """Resolve credentials for subagent delegation. + Resolution priority (highest to lowest): + 1. Per-call override_model / override_provider (from delegate_task args) + 2. Config delegation.model / delegation.provider (from config.yaml) + 3. Parent agent's own model / provider (inheritance) + If ``delegation.base_url`` is configured, subagents use that direct OpenAI-compatible endpoint. Otherwise, if ``delegation.provider`` is configured, the full credential bundle (base_url, api_key, api_mode, @@ -577,8 +600,14 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict: Raises ValueError with a user-friendly message on credential failure. """ - configured_model = str(cfg.get("model") or "").strip() or None - configured_provider = str(cfg.get("provider") or "").strip() or None + configured_model = ( + (override_model or "").strip() or + str(cfg.get("model") or "").strip() or None + ) + configured_provider = ( + (override_provider or "").strip() or + str(cfg.get("provider") or "").strip() or None + ) configured_base_url = str(cfg.get("base_url") or "").strip() or None configured_api_key = str(cfg.get("api_key") or "").strip() or None @@ -703,7 +732,11 @@ def _load_config() -> dict: "- Subagents CANNOT call: delegate_task, clarify, memory, send_message, " "execute_code.\n" "- Each subagent gets its own terminal session (separate working directory and state).\n" - "- Results are always returned as an array, one entry per task." + "- Results are always returned as an array, one entry per task.\n\n" + "MODEL OVERRIDES:\n" + "- 'model' and 'provider' let you run subagents on a different model/provider.\n" + "- Per-call values override config.yaml delegation settings.\n" + "- In batch mode, each task item can specify its own model/provider." ), "parameters": { "type": "object", @@ -747,6 +780,14 @@ def _load_config() -> dict: "items": {"type": "string"}, "description": "Toolsets for this specific task", }, + "model": { + "type": "string", + "description": "Override model for this specific task (overrides top-level model)", + }, + "provider": { + "type": "string", + "description": "Override provider for this specific task (overrides top-level provider)", + }, }, "required": ["goal"], }, @@ -764,6 +805,22 @@ def _load_config() -> dict: "Only set lower for simple tasks." ), }, + "model": { + "type": "string", + "description": ( + "Override the model for this subagent (e.g. 'google/gemini-3-flash-preview'). " + "Takes precedence over config.yaml delegation.model. " + "In batch mode, set per-task model inside the tasks array instead." + ), + }, + "provider": { + "type": "string", + "description": ( + "Override the provider for this subagent (e.g. 'openrouter', 'nous', 'zai'). " + "Takes precedence over config.yaml delegation.provider. " + "In batch mode, set per-task provider inside the tasks array instead." + ), + }, }, "required": [], }, @@ -783,6 +840,8 @@ def _load_config() -> dict: toolsets=args.get("toolsets"), tasks=args.get("tasks"), max_iterations=args.get("max_iterations"), + model=args.get("model"), + provider=args.get("provider"), parent_agent=kw.get("parent_agent")), check_fn=check_delegate_requirements, emoji="🔀",