import memoize from 'lodash-es/memoize.js' import { refreshAndGetAwsCredentials } from '../auth.js' import { getAWSRegion, isEnvTruthy } from '../envUtils.js' import { logError } from '../log.js' import { getAWSClientProxyConfig } from '../proxy.js' export const getBedrockInferenceProfiles = memoize(async function (): Promise< string[] > { const [client, { ListInferenceProfilesCommand }] = await Promise.all([ createBedrockClient(), import('@aws-sdk/client-bedrock'), ]) const allProfiles = [] let nextToken: string | undefined try { do { const command = new ListInferenceProfilesCommand({ ...(nextToken && { nextToken }), typeEquals: 'SYSTEM_DEFINED', }) const response = await client.send(command) if (response.inferenceProfileSummaries) { allProfiles.push(...response.inferenceProfileSummaries) } nextToken = response.nextToken } while (nextToken) // Filter for Anthropic models (SYSTEM_DEFINED filtering handled in query) return allProfiles .filter(profile => profile.inferenceProfileId?.includes('anthropic')) .map(profile => profile.inferenceProfileId) .filter(Boolean) as string[] } catch (error) { logError(error as Error) throw error } }) export function findFirstMatch( profiles: string[], substring: string, ): string | null { return profiles.find(p => p.includes(substring)) ?? null } async function createBedrockClient() { const { BedrockClient } = await import('@aws-sdk/client-bedrock') // Match the Anthropic Bedrock SDK's region behavior exactly: // - Reads AWS_REGION or AWS_DEFAULT_REGION env vars (not AWS config files) // - Falls back to 'us-east-1' if neither is set // This ensures we query profiles from the same region the client will use const region = getAWSRegion() const skipAuth = isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH) const clientConfig: ConstructorParameters[0] = { region, ...(process.env.ANTHROPIC_BEDROCK_BASE_URL && { endpoint: process.env.ANTHROPIC_BEDROCK_BASE_URL, }), ...(await getAWSClientProxyConfig()), ...(skipAuth && { requestHandler: new ( await import('@smithy/node-http-handler') ).NodeHttpHandler(), httpAuthSchemes: [ { schemeId: 'smithy.api#noAuth', identityProvider: () => async () => ({}), signer: new (await import('@smithy/core')).NoAuthSigner(), }, ], httpAuthSchemeProvider: () => [{ schemeId: 'smithy.api#noAuth' }], }), } if (!skipAuth && !process.env.AWS_BEARER_TOKEN_BEDROCK) { // Only refresh credentials if not using API key authentication const cachedCredentials = await refreshAndGetAwsCredentials() if (cachedCredentials) { clientConfig.credentials = { accessKeyId: cachedCredentials.accessKeyId, secretAccessKey: cachedCredentials.secretAccessKey, sessionToken: cachedCredentials.sessionToken, } } } return new BedrockClient(clientConfig) } export async function createBedrockRuntimeClient() { const { BedrockRuntimeClient } = await import( '@aws-sdk/client-bedrock-runtime' ) const region = getAWSRegion() const skipAuth = isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH) const clientConfig: ConstructorParameters[0] = { region, ...(process.env.ANTHROPIC_BEDROCK_BASE_URL && { endpoint: process.env.ANTHROPIC_BEDROCK_BASE_URL, }), ...(await getAWSClientProxyConfig()), ...(skipAuth && { // BedrockRuntimeClient defaults to HTTP/2 without fallback // proxy servers may not support this, so we explicitly force HTTP/1.1 requestHandler: new ( await import('@smithy/node-http-handler') ).NodeHttpHandler(), httpAuthSchemes: [ { schemeId: 'smithy.api#noAuth', identityProvider: () => async () => ({}), signer: new (await import('@smithy/core')).NoAuthSigner(), }, ], httpAuthSchemeProvider: () => [{ schemeId: 'smithy.api#noAuth' }], }), } if (!skipAuth && !process.env.AWS_BEARER_TOKEN_BEDROCK) { // Only refresh credentials if not using API key authentication const cachedCredentials = await refreshAndGetAwsCredentials() if (cachedCredentials) { clientConfig.credentials = { accessKeyId: cachedCredentials.accessKeyId, secretAccessKey: cachedCredentials.secretAccessKey, sessionToken: cachedCredentials.sessionToken, } } } return new BedrockRuntimeClient(clientConfig) } export const getInferenceProfileBackingModel = memoize(async function ( profileId: string, ): Promise { try { const [client, { GetInferenceProfileCommand }] = await Promise.all([ createBedrockClient(), import('@aws-sdk/client-bedrock'), ]) const command = new GetInferenceProfileCommand({ inferenceProfileIdentifier: profileId, }) const response = await client.send(command) if (!response.models || response.models.length === 0) { return null } // Use the first model as the primary backing model for cost calculation // In practice, application inference profiles typically load balance between // similar models with the same cost structure const primaryModel = response.models[0] if (!primaryModel?.modelArn) { return null } // Extract model name from ARN // ARN format: arn:aws:bedrock:region:account:foundation-model/model-name const lastSlashIndex = primaryModel.modelArn.lastIndexOf('/') return lastSlashIndex >= 0 ? primaryModel.modelArn.substring(lastSlashIndex + 1) : primaryModel.modelArn } catch (error) { logError(error as Error) return null } }) /** * Check if a model ID is a foundation model (e.g., "anthropic.claude-sonnet-4-5-20250929-v1:0") */ export function isFoundationModel(modelId: string): boolean { return modelId.startsWith('anthropic.') } /** * Cross-region inference profile prefixes for Bedrock. * These prefixes allow routing requests to models in specific regions. */ const BEDROCK_REGION_PREFIXES = ['us', 'eu', 'apac', 'global'] as const /** * Extract the model/inference profile ID from a Bedrock ARN. * If the input is not an ARN, returns it unchanged. * * ARN format: arn:aws:bedrock:::inference-profile/ * Also handles: arn:aws:bedrock:::application-inference-profile/ * And foundation model ARNs: arn:aws:bedrock:::foundation-model/ */ export function extractModelIdFromArn(modelId: string): string { if (!modelId.startsWith('arn:')) { return modelId } const lastSlashIndex = modelId.lastIndexOf('/') if (lastSlashIndex === -1) { return modelId } return modelId.substring(lastSlashIndex + 1) } export type BedrockRegionPrefix = (typeof BEDROCK_REGION_PREFIXES)[number] /** * Extract the region prefix from a Bedrock cross-region inference model ID. * Handles both plain model IDs and full ARN format. * For example: * - "eu.anthropic.claude-sonnet-4-5-20250929-v1:0" → "eu" * - "us.anthropic.claude-3-7-sonnet-20250219-v1:0" → "us" * - "arn:aws:bedrock:ap-northeast-2:123:inference-profile/global.anthropic.claude-opus-4-6-v1" → "global" * - "anthropic.claude-3-5-sonnet-20241022-v2:0" → undefined (foundation model) * - "claude-sonnet-4-5-20250929" → undefined (first-party format) */ export function getBedrockRegionPrefix( modelId: string, ): BedrockRegionPrefix | undefined { // Extract the inference profile ID from ARN format if present // ARN format: arn:aws:bedrock:::inference-profile/ const effectiveModelId = extractModelIdFromArn(modelId) for (const prefix of BEDROCK_REGION_PREFIXES) { if (effectiveModelId.startsWith(`${prefix}.anthropic.`)) { return prefix } } return undefined } /** * Apply a region prefix to a Bedrock model ID. * If the model already has a different region prefix, it will be replaced. * If the model is a foundation model (anthropic.*), the prefix will be added. * If the model is not a Bedrock model, it will be returned as-is. * * For example: * - applyBedrockRegionPrefix("us.anthropic.claude-sonnet-4-5-v1:0", "eu") → "eu.anthropic.claude-sonnet-4-5-v1:0" * - applyBedrockRegionPrefix("anthropic.claude-sonnet-4-5-v1:0", "eu") → "eu.anthropic.claude-sonnet-4-5-v1:0" * - applyBedrockRegionPrefix("claude-sonnet-4-5-20250929", "eu") → "claude-sonnet-4-5-20250929" (not a Bedrock model) */ export function applyBedrockRegionPrefix( modelId: string, prefix: BedrockRegionPrefix, ): string { // Check if it already has a region prefix and replace it const existingPrefix = getBedrockRegionPrefix(modelId) if (existingPrefix) { return modelId.replace(`${existingPrefix}.`, `${prefix}.`) } // Check if it's a foundation model (anthropic.*) and add the prefix if (isFoundationModel(modelId)) { return `${prefix}.${modelId}` } // Not a Bedrock model format, return as-is return modelId }