158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
import type { PermissionMode } from '../permissions/PermissionMode.js'
|
|
import { capitalize } from '../stringUtils.js'
|
|
import { MODEL_ALIASES, type ModelAlias } from './aliases.js'
|
|
import { applyBedrockRegionPrefix, getBedrockRegionPrefix } from './bedrock.js'
|
|
import {
|
|
getCanonicalName,
|
|
getRuntimeMainLoopModel,
|
|
parseUserSpecifiedModel,
|
|
} from './model.js'
|
|
import { getAPIProvider } from './providers.js'
|
|
|
|
export const AGENT_MODEL_OPTIONS = [...MODEL_ALIASES, 'inherit'] as const
|
|
export type AgentModelAlias = (typeof AGENT_MODEL_OPTIONS)[number]
|
|
|
|
export type AgentModelOption = {
|
|
value: AgentModelAlias
|
|
label: string
|
|
description: string
|
|
}
|
|
|
|
/**
|
|
* Get the default subagent model. Returns 'inherit' so subagents inherit
|
|
* the model from the parent thread.
|
|
*/
|
|
export function getDefaultSubagentModel(): string {
|
|
return 'inherit'
|
|
}
|
|
|
|
/**
|
|
* Get the effective model string for an agent.
|
|
*
|
|
* For Bedrock, if the parent model uses a cross-region inference prefix (e.g., "eu.", "us."),
|
|
* that prefix is inherited by subagents using alias models (e.g., "sonnet", "haiku", "opus").
|
|
* This ensures subagents use the same region as the parent, which is necessary when
|
|
* IAM permissions are scoped to specific cross-region inference profiles.
|
|
*/
|
|
export function getAgentModel(
|
|
agentModel: string | undefined,
|
|
parentModel: string,
|
|
toolSpecifiedModel?: ModelAlias,
|
|
permissionMode?: PermissionMode,
|
|
): string {
|
|
if (process.env.CLAUDE_CODE_SUBAGENT_MODEL) {
|
|
return parseUserSpecifiedModel(process.env.CLAUDE_CODE_SUBAGENT_MODEL)
|
|
}
|
|
|
|
// Extract Bedrock region prefix from parent model to inherit for subagents.
|
|
// This ensures subagents use the same cross-region inference profile (e.g., "eu.", "us.")
|
|
// as the parent, which is required when IAM permissions only allow specific regions.
|
|
const parentRegionPrefix = getBedrockRegionPrefix(parentModel)
|
|
|
|
// Helper to apply parent region prefix for Bedrock models.
|
|
// `originalSpec` is the raw model string before resolution (alias or full ID).
|
|
// If the user explicitly specified a full model ID that already carries its own
|
|
// region prefix (e.g., "eu.anthropic.…"), we preserve it instead of overwriting
|
|
// with the parent's prefix. This prevents silent data-residency violations when
|
|
// an agent config intentionally pins to a different region than the parent.
|
|
const applyParentRegionPrefix = (
|
|
resolvedModel: string,
|
|
originalSpec: string,
|
|
): string => {
|
|
if (parentRegionPrefix && getAPIProvider() === 'bedrock') {
|
|
if (getBedrockRegionPrefix(originalSpec)) return resolvedModel
|
|
return applyBedrockRegionPrefix(resolvedModel, parentRegionPrefix)
|
|
}
|
|
return resolvedModel
|
|
}
|
|
|
|
// Prioritize tool-specified model if provided
|
|
if (toolSpecifiedModel) {
|
|
if (aliasMatchesParentTier(toolSpecifiedModel, parentModel)) {
|
|
return parentModel
|
|
}
|
|
const model = parseUserSpecifiedModel(toolSpecifiedModel)
|
|
return applyParentRegionPrefix(model, toolSpecifiedModel)
|
|
}
|
|
|
|
const agentModelWithExp = agentModel ?? getDefaultSubagentModel()
|
|
|
|
if (agentModelWithExp === 'inherit') {
|
|
// Apply runtime model resolution for inherit to get the effective model
|
|
// This ensures agents using 'inherit' get opusplan→Opus resolution in plan mode
|
|
return getRuntimeMainLoopModel({
|
|
permissionMode: permissionMode ?? 'default',
|
|
mainLoopModel: parentModel,
|
|
exceeds200kTokens: false,
|
|
})
|
|
}
|
|
|
|
if (aliasMatchesParentTier(agentModelWithExp, parentModel)) {
|
|
return parentModel
|
|
}
|
|
const model = parseUserSpecifiedModel(agentModelWithExp)
|
|
return applyParentRegionPrefix(model, agentModelWithExp)
|
|
}
|
|
|
|
/**
|
|
* Check if a bare family alias (opus/sonnet/haiku) matches the parent model's
|
|
* tier. When it does, the subagent inherits the parent's exact model string
|
|
* instead of resolving the alias to a provider default.
|
|
*
|
|
* Prevents surprising downgrades: a Vertex user on Opus 4.6 (via /model) who
|
|
* spawns a subagent with `model: opus` should get Opus 4.6, not whatever
|
|
* getDefaultOpusModel() returns for 3P.
|
|
* See https://github.com/anthropics/claude-code/issues/30815.
|
|
*
|
|
* Only bare family aliases match. `opus[1m]`, `best`, `opusplan` fall through
|
|
* since they carry semantics beyond "same tier as parent".
|
|
*/
|
|
function aliasMatchesParentTier(alias: string, parentModel: string): boolean {
|
|
const canonical = getCanonicalName(parentModel)
|
|
switch (alias.toLowerCase()) {
|
|
case 'opus':
|
|
return canonical.includes('opus')
|
|
case 'sonnet':
|
|
return canonical.includes('sonnet')
|
|
case 'haiku':
|
|
return canonical.includes('haiku')
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function getAgentModelDisplay(model: string | undefined): string {
|
|
// When model is omitted, getDefaultSubagentModel() returns 'inherit' at runtime
|
|
if (!model) return 'Inherit from parent (default)'
|
|
if (model === 'inherit') return 'Inherit from parent'
|
|
return capitalize(model)
|
|
}
|
|
|
|
/**
|
|
* Get available model options for agents
|
|
*/
|
|
export function getAgentModelOptions(): AgentModelOption[] {
|
|
return [
|
|
{
|
|
value: 'sonnet',
|
|
label: 'Sonnet',
|
|
description: 'Balanced performance - best for most agents',
|
|
},
|
|
{
|
|
value: 'opus',
|
|
label: 'Opus',
|
|
description: 'Most capable for complex reasoning tasks',
|
|
},
|
|
{
|
|
value: 'haiku',
|
|
label: 'Haiku',
|
|
description: 'Fast and efficient for simple tasks',
|
|
},
|
|
{
|
|
value: 'inherit',
|
|
label: 'Inherit from parent',
|
|
description: 'Use the same model as the main conversation',
|
|
},
|
|
]
|
|
}
|