/ utils / model / agent.ts
agent.ts
  1  import type { PermissionMode } from '../permissions/PermissionMode.js'
  2  import { capitalize } from '../stringUtils.js'
  3  import { MODEL_ALIASES, type ModelAlias } from './aliases.js'
  4  import { applyBedrockRegionPrefix, getBedrockRegionPrefix } from './bedrock.js'
  5  import {
  6    getCanonicalName,
  7    getRuntimeMainLoopModel,
  8    parseUserSpecifiedModel,
  9  } from './model.js'
 10  import { getAPIProvider } from './providers.js'
 11  
 12  export const AGENT_MODEL_OPTIONS = [...MODEL_ALIASES, 'inherit'] as const
 13  export type AgentModelAlias = (typeof AGENT_MODEL_OPTIONS)[number]
 14  
 15  export type AgentModelOption = {
 16    value: AgentModelAlias
 17    label: string
 18    description: string
 19  }
 20  
 21  /**
 22   * Get the default subagent model. Returns 'inherit' so subagents inherit
 23   * the model from the parent thread.
 24   */
 25  export function getDefaultSubagentModel(): string {
 26    return 'inherit'
 27  }
 28  
 29  /**
 30   * Get the effective model string for an agent.
 31   *
 32   * For Bedrock, if the parent model uses a cross-region inference prefix (e.g., "eu.", "us."),
 33   * that prefix is inherited by subagents using alias models (e.g., "sonnet", "haiku", "opus").
 34   * This ensures subagents use the same region as the parent, which is necessary when
 35   * IAM permissions are scoped to specific cross-region inference profiles.
 36   */
 37  export function getAgentModel(
 38    agentModel: string | undefined,
 39    parentModel: string,
 40    toolSpecifiedModel?: ModelAlias,
 41    permissionMode?: PermissionMode,
 42  ): string {
 43    if (process.env.CLAUDE_CODE_SUBAGENT_MODEL) {
 44      return parseUserSpecifiedModel(process.env.CLAUDE_CODE_SUBAGENT_MODEL)
 45    }
 46  
 47    // Extract Bedrock region prefix from parent model to inherit for subagents.
 48    // This ensures subagents use the same cross-region inference profile (e.g., "eu.", "us.")
 49    // as the parent, which is required when IAM permissions only allow specific regions.
 50    const parentRegionPrefix = getBedrockRegionPrefix(parentModel)
 51  
 52    // Helper to apply parent region prefix for Bedrock models.
 53    // `originalSpec` is the raw model string before resolution (alias or full ID).
 54    // If the user explicitly specified a full model ID that already carries its own
 55    // region prefix (e.g., "eu.anthropic.…"), we preserve it instead of overwriting
 56    // with the parent's prefix. This prevents silent data-residency violations when
 57    // an agent config intentionally pins to a different region than the parent.
 58    const applyParentRegionPrefix = (
 59      resolvedModel: string,
 60      originalSpec: string,
 61    ): string => {
 62      if (parentRegionPrefix && getAPIProvider() === 'bedrock') {
 63        if (getBedrockRegionPrefix(originalSpec)) return resolvedModel
 64        return applyBedrockRegionPrefix(resolvedModel, parentRegionPrefix)
 65      }
 66      return resolvedModel
 67    }
 68  
 69    // Prioritize tool-specified model if provided
 70    if (toolSpecifiedModel) {
 71      if (aliasMatchesParentTier(toolSpecifiedModel, parentModel)) {
 72        return parentModel
 73      }
 74      const model = parseUserSpecifiedModel(toolSpecifiedModel)
 75      return applyParentRegionPrefix(model, toolSpecifiedModel)
 76    }
 77  
 78    const agentModelWithExp = agentModel ?? getDefaultSubagentModel()
 79  
 80    if (agentModelWithExp === 'inherit') {
 81      // Apply runtime model resolution for inherit to get the effective model
 82      // This ensures agents using 'inherit' get opusplan→Opus resolution in plan mode
 83      return getRuntimeMainLoopModel({
 84        permissionMode: permissionMode ?? 'default',
 85        mainLoopModel: parentModel,
 86        exceeds200kTokens: false,
 87      })
 88    }
 89  
 90    if (aliasMatchesParentTier(agentModelWithExp, parentModel)) {
 91      return parentModel
 92    }
 93    const model = parseUserSpecifiedModel(agentModelWithExp)
 94    return applyParentRegionPrefix(model, agentModelWithExp)
 95  }
 96  
 97  /**
 98   * Check if a bare family alias (opus/sonnet/haiku) matches the parent model's
 99   * tier. When it does, the subagent inherits the parent's exact model string
100   * instead of resolving the alias to a provider default.
101   *
102   * Prevents surprising downgrades: a Vertex user on Opus 4.6 (via /model) who
103   * spawns a subagent with `model: opus` should get Opus 4.6, not whatever
104   * getDefaultOpusModel() returns for 3P.
105   * See https://github.com/anthropics/claude-code/issues/30815.
106   *
107   * Only bare family aliases match. `opus[1m]`, `best`, `opusplan` fall through
108   * since they carry semantics beyond "same tier as parent".
109   */
110  function aliasMatchesParentTier(alias: string, parentModel: string): boolean {
111    const canonical = getCanonicalName(parentModel)
112    switch (alias.toLowerCase()) {
113      case 'opus':
114        return canonical.includes('opus')
115      case 'sonnet':
116        return canonical.includes('sonnet')
117      case 'haiku':
118        return canonical.includes('haiku')
119      default:
120        return false
121    }
122  }
123  
124  export function getAgentModelDisplay(model: string | undefined): string {
125    // When model is omitted, getDefaultSubagentModel() returns 'inherit' at runtime
126    if (!model) return 'Inherit from parent (default)'
127    if (model === 'inherit') return 'Inherit from parent'
128    return capitalize(model)
129  }
130  
131  /**
132   * Get available model options for agents
133   */
134  export function getAgentModelOptions(): AgentModelOption[] {
135    return [
136      {
137        value: 'sonnet',
138        label: 'Sonnet',
139        description: 'Balanced performance - best for most agents',
140      },
141      {
142        value: 'opus',
143        label: 'Opus',
144        description: 'Most capable for complex reasoning tasks',
145      },
146      {
147        value: 'haiku',
148        label: 'Haiku',
149        description: 'Fast and efficient for simple tasks',
150      },
151      {
152        value: 'inherit',
153        label: 'Inherit from parent',
154        description: 'Use the same model as the main conversation',
155      },
156    ]
157  }