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

@@ -95,12 +95,14 @@ export async function getAnthropicClient({
model,
fetchOverride,
source,
providerOverride,
}: {
apiKey?: string
maxRetries: number
model?: string
fetchOverride?: ClientOptions['fetch']
source?: string
providerOverride?: { model: string; baseURL: string; apiKey: string }
}): Promise<Anthropic> {
const containerId = process.env.CLAUDE_CODE_CONTAINER_ID
const remoteSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID
@@ -154,6 +156,24 @@ export async function getAnthropicClient({
fetch: resolvedFetch,
}),
}
// Agent routing override: use per-agent provider when configured.
// Strip auth-related headers to prevent leaking Anthropic credentials
// to third-party endpoints (SSRF / credential forwarding mitigation).
if (providerOverride) {
const { createOpenAIShimClient } = await import('./openaiShim.js')
const safeHeaders: Record<string, string> = {}
for (const [k, v] of Object.entries(defaultHeaders)) {
const lower = k.toLowerCase()
if (lower === 'authorization' || lower === 'x-api-key' || lower === 'api-key') continue
safeHeaders[k] = v
}
return createOpenAIShimClient({
defaultHeaders: safeHeaders,
maxRetries,
timeout: parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10),
providerOverride,
}) as unknown as Anthropic
}
if (
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ||