feat: per-agent model routing — route different agents to different providers (#238)

* feat: add agentModels and agentRouting to SettingsSchema

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add agentRouting module for per-agent provider resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: thread providerOverride through OpenAI shim for per-agent routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: getAnthropicClient accepts providerOverride for agent routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: thread providerOverride through Options and queryModel calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: thread providerOverride through query loop and ToolUseContext

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: resolve agent routing in runAgent and inject providerOverride

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add Agent Routing configuration guide to README

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add unit tests for resolveAgentProvider + plaintext api_key note

- 15 tests covering priority chain (name > subagentType > default > null)
- normalize() case-insensitive and hyphen/underscore equivalence
- Edge cases: null settings, missing config sections, non-existent model
- README note about api_key stored in plaintext

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* security: address code review — SSRF, credential leak, key collision

- base_url schema now uses z.string().url() for SSRF mitigation
- Strip auth headers (Authorization, x-api-key, api-key) from
  defaultHeaders when providerOverride is active, preventing
  Anthropic credentials from leaking to third-party endpoints
- Warn on duplicate normalized routing keys to prevent silent shadowing
- providerOverride.apiKey is never logged (verified via grep)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: 冯俊辉 <fengjunhui@shiyanjia.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
JasonVon
2026-04-03 21:47:26 +08:00
committed by GitHub
parent 59ab2701f7
commit fb32e3f829
11 changed files with 319 additions and 9 deletions

View File

@@ -644,7 +644,8 @@ export const AgentTool = buildTool({
useExactTools: true
}),
worktreePath: worktreeInfo?.worktreePath,
description
description,
agentName: name,
};
// Helper to wrap execution with a cwd override: explicit cwd arg (KAIROS)

View File

@@ -57,6 +57,8 @@ import { clearSessionHooks } from '../../utils/hooks/sessionHooks.js'
import { executeSubagentStartHooks } from '../../utils/hooks.js'
import { createUserMessage } from '../../utils/messages.js'
import { getAgentModel } from '../../utils/model/agent.js'
import { resolveAgentProvider } from '../../services/api/agentRouting.js'
import { getInitialSettings } from '../../utils/settings/settings.js'
import type { ModelAlias } from '../../utils/model/aliases.js'
import {
clearAgentTranscriptSubdir,
@@ -267,6 +269,7 @@ export async function* runAgent({
description,
transcriptSubdir,
onQueryProgress,
agentName,
}: {
agentDefinition: AgentDefinition
promptMessages: Message[]
@@ -326,6 +329,8 @@ export async function* runAgent({
* during long single-block streams (e.g. thinking) where no assistant
* message is yielded for >60s. */
onQueryProgress?: () => void
/** Agent name (team member name) for routing resolution */
agentName?: string
}): AsyncGenerator<Message, void> {
// Track subagent usage for feature discovery
@@ -344,6 +349,14 @@ export async function* runAgent({
permissionMode,
)
// Resolve per-agent provider routing from settings
const providerOverride = resolveAgentProvider(
agentName,
agentDefinition.agentType,
getInitialSettings(),
)
const effectiveModel = providerOverride ? providerOverride.model : resolvedAgentModel
const agentId = override?.agentId ? override.agentId : createAgentId()
// Route this agent's transcript into a grouping subdirectory if requested
@@ -675,7 +688,8 @@ export async function* runAgent({
commands: [],
debug: toolUseContext.options.debug,
verbose: toolUseContext.options.verbose,
mainLoopModel: resolvedAgentModel,
mainLoopModel: effectiveModel,
providerOverride: providerOverride ?? undefined,
// For fork children (useExactTools), inherit thinking config to match the
// parent's API request prefix for prompt cache hits. For regular
// sub-agents, disable thinking to control output token costs.