source dump of claude code
at main 265 lines 9.2 kB view raw
1import memoize from 'lodash-es/memoize.js' 2import { refreshAndGetAwsCredentials } from '../auth.js' 3import { getAWSRegion, isEnvTruthy } from '../envUtils.js' 4import { logError } from '../log.js' 5import { getAWSClientProxyConfig } from '../proxy.js' 6 7export const getBedrockInferenceProfiles = memoize(async function (): Promise< 8 string[] 9> { 10 const [client, { ListInferenceProfilesCommand }] = await Promise.all([ 11 createBedrockClient(), 12 import('@aws-sdk/client-bedrock'), 13 ]) 14 const allProfiles = [] 15 let nextToken: string | undefined 16 17 try { 18 do { 19 const command = new ListInferenceProfilesCommand({ 20 ...(nextToken && { nextToken }), 21 typeEquals: 'SYSTEM_DEFINED', 22 }) 23 const response = await client.send(command) 24 25 if (response.inferenceProfileSummaries) { 26 allProfiles.push(...response.inferenceProfileSummaries) 27 } 28 29 nextToken = response.nextToken 30 } while (nextToken) 31 32 // Filter for Anthropic models (SYSTEM_DEFINED filtering handled in query) 33 return allProfiles 34 .filter(profile => profile.inferenceProfileId?.includes('anthropic')) 35 .map(profile => profile.inferenceProfileId) 36 .filter(Boolean) as string[] 37 } catch (error) { 38 logError(error as Error) 39 throw error 40 } 41}) 42 43export function findFirstMatch( 44 profiles: string[], 45 substring: string, 46): string | null { 47 return profiles.find(p => p.includes(substring)) ?? null 48} 49 50async function createBedrockClient() { 51 const { BedrockClient } = await import('@aws-sdk/client-bedrock') 52 // Match the Anthropic Bedrock SDK's region behavior exactly: 53 // - Reads AWS_REGION or AWS_DEFAULT_REGION env vars (not AWS config files) 54 // - Falls back to 'us-east-1' if neither is set 55 // This ensures we query profiles from the same region the client will use 56 const region = getAWSRegion() 57 58 const skipAuth = isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH) 59 60 const clientConfig: ConstructorParameters<typeof BedrockClient>[0] = { 61 region, 62 ...(process.env.ANTHROPIC_BEDROCK_BASE_URL && { 63 endpoint: process.env.ANTHROPIC_BEDROCK_BASE_URL, 64 }), 65 ...(await getAWSClientProxyConfig()), 66 ...(skipAuth && { 67 requestHandler: new ( 68 await import('@smithy/node-http-handler') 69 ).NodeHttpHandler(), 70 httpAuthSchemes: [ 71 { 72 schemeId: 'smithy.api#noAuth', 73 identityProvider: () => async () => ({}), 74 signer: new (await import('@smithy/core')).NoAuthSigner(), 75 }, 76 ], 77 httpAuthSchemeProvider: () => [{ schemeId: 'smithy.api#noAuth' }], 78 }), 79 } 80 81 if (!skipAuth && !process.env.AWS_BEARER_TOKEN_BEDROCK) { 82 // Only refresh credentials if not using API key authentication 83 const cachedCredentials = await refreshAndGetAwsCredentials() 84 if (cachedCredentials) { 85 clientConfig.credentials = { 86 accessKeyId: cachedCredentials.accessKeyId, 87 secretAccessKey: cachedCredentials.secretAccessKey, 88 sessionToken: cachedCredentials.sessionToken, 89 } 90 } 91 } 92 93 return new BedrockClient(clientConfig) 94} 95 96export async function createBedrockRuntimeClient() { 97 const { BedrockRuntimeClient } = await import( 98 '@aws-sdk/client-bedrock-runtime' 99 ) 100 const region = getAWSRegion() 101 const skipAuth = isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH) 102 103 const clientConfig: ConstructorParameters<typeof BedrockRuntimeClient>[0] = { 104 region, 105 ...(process.env.ANTHROPIC_BEDROCK_BASE_URL && { 106 endpoint: process.env.ANTHROPIC_BEDROCK_BASE_URL, 107 }), 108 ...(await getAWSClientProxyConfig()), 109 ...(skipAuth && { 110 // BedrockRuntimeClient defaults to HTTP/2 without fallback 111 // proxy servers may not support this, so we explicitly force HTTP/1.1 112 requestHandler: new ( 113 await import('@smithy/node-http-handler') 114 ).NodeHttpHandler(), 115 httpAuthSchemes: [ 116 { 117 schemeId: 'smithy.api#noAuth', 118 identityProvider: () => async () => ({}), 119 signer: new (await import('@smithy/core')).NoAuthSigner(), 120 }, 121 ], 122 httpAuthSchemeProvider: () => [{ schemeId: 'smithy.api#noAuth' }], 123 }), 124 } 125 126 if (!skipAuth && !process.env.AWS_BEARER_TOKEN_BEDROCK) { 127 // Only refresh credentials if not using API key authentication 128 const cachedCredentials = await refreshAndGetAwsCredentials() 129 if (cachedCredentials) { 130 clientConfig.credentials = { 131 accessKeyId: cachedCredentials.accessKeyId, 132 secretAccessKey: cachedCredentials.secretAccessKey, 133 sessionToken: cachedCredentials.sessionToken, 134 } 135 } 136 } 137 138 return new BedrockRuntimeClient(clientConfig) 139} 140 141export const getInferenceProfileBackingModel = memoize(async function ( 142 profileId: string, 143): Promise<string | null> { 144 try { 145 const [client, { GetInferenceProfileCommand }] = await Promise.all([ 146 createBedrockClient(), 147 import('@aws-sdk/client-bedrock'), 148 ]) 149 const command = new GetInferenceProfileCommand({ 150 inferenceProfileIdentifier: profileId, 151 }) 152 const response = await client.send(command) 153 154 if (!response.models || response.models.length === 0) { 155 return null 156 } 157 158 // Use the first model as the primary backing model for cost calculation 159 // In practice, application inference profiles typically load balance between 160 // similar models with the same cost structure 161 const primaryModel = response.models[0] 162 if (!primaryModel?.modelArn) { 163 return null 164 } 165 166 // Extract model name from ARN 167 // ARN format: arn:aws:bedrock:region:account:foundation-model/model-name 168 const lastSlashIndex = primaryModel.modelArn.lastIndexOf('/') 169 return lastSlashIndex >= 0 170 ? primaryModel.modelArn.substring(lastSlashIndex + 1) 171 : primaryModel.modelArn 172 } catch (error) { 173 logError(error as Error) 174 return null 175 } 176}) 177 178/** 179 * Check if a model ID is a foundation model (e.g., "anthropic.claude-sonnet-4-5-20250929-v1:0") 180 */ 181export function isFoundationModel(modelId: string): boolean { 182 return modelId.startsWith('anthropic.') 183} 184 185/** 186 * Cross-region inference profile prefixes for Bedrock. 187 * These prefixes allow routing requests to models in specific regions. 188 */ 189const BEDROCK_REGION_PREFIXES = ['us', 'eu', 'apac', 'global'] as const 190 191/** 192 * Extract the model/inference profile ID from a Bedrock ARN. 193 * If the input is not an ARN, returns it unchanged. 194 * 195 * ARN format: arn:aws:bedrock:<region>:<account>:inference-profile/<profile-id> 196 * Also handles: arn:aws:bedrock:<region>:<account>:application-inference-profile/<profile-id> 197 * And foundation model ARNs: arn:aws:bedrock:<region>::foundation-model/<model-id> 198 */ 199export function extractModelIdFromArn(modelId: string): string { 200 if (!modelId.startsWith('arn:')) { 201 return modelId 202 } 203 const lastSlashIndex = modelId.lastIndexOf('/') 204 if (lastSlashIndex === -1) { 205 return modelId 206 } 207 return modelId.substring(lastSlashIndex + 1) 208} 209 210export type BedrockRegionPrefix = (typeof BEDROCK_REGION_PREFIXES)[number] 211 212/** 213 * Extract the region prefix from a Bedrock cross-region inference model ID. 214 * Handles both plain model IDs and full ARN format. 215 * For example: 216 * - "eu.anthropic.claude-sonnet-4-5-20250929-v1:0" → "eu" 217 * - "us.anthropic.claude-3-7-sonnet-20250219-v1:0" → "us" 218 * - "arn:aws:bedrock:ap-northeast-2:123:inference-profile/global.anthropic.claude-opus-4-6-v1" → "global" 219 * - "anthropic.claude-3-5-sonnet-20241022-v2:0" → undefined (foundation model) 220 * - "claude-sonnet-4-5-20250929" → undefined (first-party format) 221 */ 222export function getBedrockRegionPrefix( 223 modelId: string, 224): BedrockRegionPrefix | undefined { 225 // Extract the inference profile ID from ARN format if present 226 // ARN format: arn:aws:bedrock:<region>:<account>:inference-profile/<profile-id> 227 const effectiveModelId = extractModelIdFromArn(modelId) 228 229 for (const prefix of BEDROCK_REGION_PREFIXES) { 230 if (effectiveModelId.startsWith(`${prefix}.anthropic.`)) { 231 return prefix 232 } 233 } 234 return undefined 235} 236 237/** 238 * Apply a region prefix to a Bedrock model ID. 239 * If the model already has a different region prefix, it will be replaced. 240 * If the model is a foundation model (anthropic.*), the prefix will be added. 241 * If the model is not a Bedrock model, it will be returned as-is. 242 * 243 * For example: 244 * - applyBedrockRegionPrefix("us.anthropic.claude-sonnet-4-5-v1:0", "eu") → "eu.anthropic.claude-sonnet-4-5-v1:0" 245 * - applyBedrockRegionPrefix("anthropic.claude-sonnet-4-5-v1:0", "eu") → "eu.anthropic.claude-sonnet-4-5-v1:0" 246 * - applyBedrockRegionPrefix("claude-sonnet-4-5-20250929", "eu") → "claude-sonnet-4-5-20250929" (not a Bedrock model) 247 */ 248export function applyBedrockRegionPrefix( 249 modelId: string, 250 prefix: BedrockRegionPrefix, 251): string { 252 // Check if it already has a region prefix and replace it 253 const existingPrefix = getBedrockRegionPrefix(modelId) 254 if (existingPrefix) { 255 return modelId.replace(`${existingPrefix}.`, `${prefix}.`) 256 } 257 258 // Check if it's a foundation model (anthropic.*) and add the prefix 259 if (isFoundationModel(modelId)) { 260 return `${prefix}.${modelId}` 261 } 262 263 // Not a Bedrock model format, return as-is 264 return modelId 265}