source dump of claude code
at main 389 lines 16 kB view raw
1import Anthropic, { type ClientOptions } from '@anthropic-ai/sdk' 2import { randomUUID } from 'crypto' 3import type { GoogleAuth } from 'google-auth-library' 4import { 5 checkAndRefreshOAuthTokenIfNeeded, 6 getAnthropicApiKey, 7 getApiKeyFromApiKeyHelper, 8 getClaudeAIOAuthTokens, 9 isClaudeAISubscriber, 10 refreshAndGetAwsCredentials, 11 refreshGcpCredentialsIfNeeded, 12} from 'src/utils/auth.js' 13import { getUserAgent } from 'src/utils/http.js' 14import { getSmallFastModel } from 'src/utils/model/model.js' 15import { 16 getAPIProvider, 17 isFirstPartyAnthropicBaseUrl, 18} from 'src/utils/model/providers.js' 19import { getProxyFetchOptions } from 'src/utils/proxy.js' 20import { 21 getIsNonInteractiveSession, 22 getSessionId, 23} from '../../bootstrap/state.js' 24import { getOauthConfig } from '../../constants/oauth.js' 25import { isDebugToStdErr, logForDebugging } from '../../utils/debug.js' 26import { 27 getAWSRegion, 28 getVertexRegionForModel, 29 isEnvTruthy, 30} from '../../utils/envUtils.js' 31 32/** 33 * Environment variables for different client types: 34 * 35 * Direct API: 36 * - ANTHROPIC_API_KEY: Required for direct API access 37 * 38 * AWS Bedrock: 39 * - AWS credentials configured via aws-sdk defaults 40 * - AWS_REGION or AWS_DEFAULT_REGION: Sets the AWS region for all models (default: us-east-1) 41 * - ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION: Optional. Override AWS region specifically for the small fast model (Haiku) 42 * 43 * Foundry (Azure): 44 * - ANTHROPIC_FOUNDRY_RESOURCE: Your Azure resource name (e.g., 'my-resource') 45 * For the full endpoint: https://{resource}.services.ai.azure.com/anthropic/v1/messages 46 * - ANTHROPIC_FOUNDRY_BASE_URL: Optional. Alternative to resource - provide full base URL directly 47 * (e.g., 'https://my-resource.services.ai.azure.com') 48 * 49 * Authentication (one of the following): 50 * - ANTHROPIC_FOUNDRY_API_KEY: Your Microsoft Foundry API key (if using API key auth) 51 * - Azure AD authentication: If no API key is provided, uses DefaultAzureCredential 52 * which supports multiple auth methods (environment variables, managed identity, 53 * Azure CLI, etc.). See: https://docs.microsoft.com/en-us/javascript/api/@azure/identity 54 * 55 * Vertex AI: 56 * - Model-specific region variables (highest priority): 57 * - VERTEX_REGION_CLAUDE_3_5_HAIKU: Region for Claude 3.5 Haiku model 58 * - VERTEX_REGION_CLAUDE_HAIKU_4_5: Region for Claude Haiku 4.5 model 59 * - VERTEX_REGION_CLAUDE_3_5_SONNET: Region for Claude 3.5 Sonnet model 60 * - VERTEX_REGION_CLAUDE_3_7_SONNET: Region for Claude 3.7 Sonnet model 61 * - CLOUD_ML_REGION: Optional. The default GCP region to use for all models 62 * If specific model region not specified above 63 * - ANTHROPIC_VERTEX_PROJECT_ID: Required. Your GCP project ID 64 * - Standard GCP credentials configured via google-auth-library 65 * 66 * Priority for determining region: 67 * 1. Hardcoded model-specific environment variables 68 * 2. Global CLOUD_ML_REGION variable 69 * 3. Default region from config 70 * 4. Fallback region (us-east5) 71 */ 72 73function createStderrLogger(): ClientOptions['logger'] { 74 return { 75 error: (msg, ...args) => 76 // biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console 77 console.error('[Anthropic SDK ERROR]', msg, ...args), 78 // biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console 79 warn: (msg, ...args) => console.error('[Anthropic SDK WARN]', msg, ...args), 80 // biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console 81 info: (msg, ...args) => console.error('[Anthropic SDK INFO]', msg, ...args), 82 debug: (msg, ...args) => 83 // biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console 84 console.error('[Anthropic SDK DEBUG]', msg, ...args), 85 } 86} 87 88export async function getAnthropicClient({ 89 apiKey, 90 maxRetries, 91 model, 92 fetchOverride, 93 source, 94}: { 95 apiKey?: string 96 maxRetries: number 97 model?: string 98 fetchOverride?: ClientOptions['fetch'] 99 source?: string 100}): Promise<Anthropic> { 101 const containerId = process.env.CLAUDE_CODE_CONTAINER_ID 102 const remoteSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID 103 const clientApp = process.env.CLAUDE_AGENT_SDK_CLIENT_APP 104 const customHeaders = getCustomHeaders() 105 const defaultHeaders: { [key: string]: string } = { 106 'x-app': 'cli', 107 'User-Agent': getUserAgent(), 108 'X-Claude-Code-Session-Id': getSessionId(), 109 ...customHeaders, 110 ...(containerId ? { 'x-claude-remote-container-id': containerId } : {}), 111 ...(remoteSessionId 112 ? { 'x-claude-remote-session-id': remoteSessionId } 113 : {}), 114 // SDK consumers can identify their app/library for backend analytics 115 ...(clientApp ? { 'x-client-app': clientApp } : {}), 116 } 117 118 // Log API client configuration for HFI debugging 119 logForDebugging( 120 `[API:request] Creating client, ANTHROPIC_CUSTOM_HEADERS present: ${!!process.env.ANTHROPIC_CUSTOM_HEADERS}, has Authorization header: ${!!customHeaders['Authorization']}`, 121 ) 122 123 // Add additional protection header if enabled via env var 124 const additionalProtectionEnabled = isEnvTruthy( 125 process.env.CLAUDE_CODE_ADDITIONAL_PROTECTION, 126 ) 127 if (additionalProtectionEnabled) { 128 defaultHeaders['x-anthropic-additional-protection'] = 'true' 129 } 130 131 logForDebugging('[API:auth] OAuth token check starting') 132 await checkAndRefreshOAuthTokenIfNeeded() 133 logForDebugging('[API:auth] OAuth token check complete') 134 135 if (!isClaudeAISubscriber()) { 136 await configureApiKeyHeaders(defaultHeaders, getIsNonInteractiveSession()) 137 } 138 139 const resolvedFetch = buildFetch(fetchOverride, source) 140 141 const ARGS = { 142 defaultHeaders, 143 maxRetries, 144 timeout: parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10), 145 dangerouslyAllowBrowser: true, 146 fetchOptions: getProxyFetchOptions({ 147 forAnthropicAPI: true, 148 }) as ClientOptions['fetchOptions'], 149 ...(resolvedFetch && { 150 fetch: resolvedFetch, 151 }), 152 } 153 if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)) { 154 const { AnthropicBedrock } = await import('@anthropic-ai/bedrock-sdk') 155 // Use region override for small fast model if specified 156 const awsRegion = 157 model === getSmallFastModel() && 158 process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION 159 ? process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION 160 : getAWSRegion() 161 162 const bedrockArgs: ConstructorParameters<typeof AnthropicBedrock>[0] = { 163 ...ARGS, 164 awsRegion, 165 ...(isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH) && { 166 skipAuth: true, 167 }), 168 ...(isDebugToStdErr() && { logger: createStderrLogger() }), 169 } 170 171 // Add API key authentication if available 172 if (process.env.AWS_BEARER_TOKEN_BEDROCK) { 173 bedrockArgs.skipAuth = true 174 // Add the Bearer token for Bedrock API key authentication 175 bedrockArgs.defaultHeaders = { 176 ...bedrockArgs.defaultHeaders, 177 Authorization: `Bearer ${process.env.AWS_BEARER_TOKEN_BEDROCK}`, 178 } 179 } else if (!isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH)) { 180 // Refresh auth and get credentials with cache clearing 181 const cachedCredentials = await refreshAndGetAwsCredentials() 182 if (cachedCredentials) { 183 bedrockArgs.awsAccessKey = cachedCredentials.accessKeyId 184 bedrockArgs.awsSecretKey = cachedCredentials.secretAccessKey 185 bedrockArgs.awsSessionToken = cachedCredentials.sessionToken 186 } 187 } 188 // we have always been lying about the return type - this doesn't support batching or models 189 return new AnthropicBedrock(bedrockArgs) as unknown as Anthropic 190 } 191 if (isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)) { 192 const { AnthropicFoundry } = await import('@anthropic-ai/foundry-sdk') 193 // Determine Azure AD token provider based on configuration 194 // SDK reads ANTHROPIC_FOUNDRY_API_KEY by default 195 let azureADTokenProvider: (() => Promise<string>) | undefined 196 if (!process.env.ANTHROPIC_FOUNDRY_API_KEY) { 197 if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_FOUNDRY_AUTH)) { 198 // Mock token provider for testing/proxy scenarios (similar to Vertex mock GoogleAuth) 199 azureADTokenProvider = () => Promise.resolve('') 200 } else { 201 // Use real Azure AD authentication with DefaultAzureCredential 202 const { 203 DefaultAzureCredential: AzureCredential, 204 getBearerTokenProvider, 205 } = await import('@azure/identity') 206 azureADTokenProvider = getBearerTokenProvider( 207 new AzureCredential(), 208 'https://cognitiveservices.azure.com/.default', 209 ) 210 } 211 } 212 213 const foundryArgs: ConstructorParameters<typeof AnthropicFoundry>[0] = { 214 ...ARGS, 215 ...(azureADTokenProvider && { azureADTokenProvider }), 216 ...(isDebugToStdErr() && { logger: createStderrLogger() }), 217 } 218 // we have always been lying about the return type - this doesn't support batching or models 219 return new AnthropicFoundry(foundryArgs) as unknown as Anthropic 220 } 221 if (isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)) { 222 // Refresh GCP credentials if gcpAuthRefresh is configured and credentials are expired 223 // This is similar to how we handle AWS credential refresh for Bedrock 224 if (!isEnvTruthy(process.env.CLAUDE_CODE_SKIP_VERTEX_AUTH)) { 225 await refreshGcpCredentialsIfNeeded() 226 } 227 228 const [{ AnthropicVertex }, { GoogleAuth }] = await Promise.all([ 229 import('@anthropic-ai/vertex-sdk'), 230 import('google-auth-library'), 231 ]) 232 // TODO: Cache either GoogleAuth instance or AuthClient to improve performance 233 // Currently we create a new GoogleAuth instance for every getAnthropicClient() call 234 // This could cause repeated authentication flows and metadata server checks 235 // However, caching needs careful handling of: 236 // - Credential refresh/expiration 237 // - Environment variable changes (GOOGLE_APPLICATION_CREDENTIALS, project vars) 238 // - Cross-request auth state management 239 // See: https://github.com/googleapis/google-auth-library-nodejs/issues/390 for caching challenges 240 241 // Prevent metadata server timeout by providing projectId as fallback 242 // google-auth-library checks project ID in this order: 243 // 1. Environment variables (GCLOUD_PROJECT, GOOGLE_CLOUD_PROJECT, etc.) 244 // 2. Credential files (service account JSON, ADC file) 245 // 3. gcloud config 246 // 4. GCE metadata server (causes 12s timeout outside GCP) 247 // 248 // We only set projectId if user hasn't configured other discovery methods 249 // to avoid interfering with their existing auth setup 250 251 // Check project environment variables in same order as google-auth-library 252 // See: https://github.com/googleapis/google-auth-library-nodejs/blob/main/src/auth/googleauth.ts 253 const hasProjectEnvVar = 254 process.env['GCLOUD_PROJECT'] || 255 process.env['GOOGLE_CLOUD_PROJECT'] || 256 process.env['gcloud_project'] || 257 process.env['google_cloud_project'] 258 259 // Check for credential file paths (service account or ADC) 260 // Note: We're checking both standard and lowercase variants to be safe, 261 // though we should verify what google-auth-library actually checks 262 const hasKeyFile = 263 process.env['GOOGLE_APPLICATION_CREDENTIALS'] || 264 process.env['google_application_credentials'] 265 266 const googleAuth = isEnvTruthy(process.env.CLAUDE_CODE_SKIP_VERTEX_AUTH) 267 ? ({ 268 // Mock GoogleAuth for testing/proxy scenarios 269 getClient: () => ({ 270 getRequestHeaders: () => ({}), 271 }), 272 } as unknown as GoogleAuth) 273 : new GoogleAuth({ 274 scopes: ['https://www.googleapis.com/auth/cloud-platform'], 275 // Only use ANTHROPIC_VERTEX_PROJECT_ID as last resort fallback 276 // This prevents the 12-second metadata server timeout when: 277 // - No project env vars are set AND 278 // - No credential keyfile is specified AND 279 // - ADC file exists but lacks project_id field 280 // 281 // Risk: If auth project != API target project, this could cause billing/audit issues 282 // Mitigation: Users can set GOOGLE_CLOUD_PROJECT to override 283 ...(hasProjectEnvVar || hasKeyFile 284 ? {} 285 : { 286 projectId: process.env.ANTHROPIC_VERTEX_PROJECT_ID, 287 }), 288 }) 289 290 const vertexArgs: ConstructorParameters<typeof AnthropicVertex>[0] = { 291 ...ARGS, 292 region: getVertexRegionForModel(model), 293 googleAuth, 294 ...(isDebugToStdErr() && { logger: createStderrLogger() }), 295 } 296 // we have always been lying about the return type - this doesn't support batching or models 297 return new AnthropicVertex(vertexArgs) as unknown as Anthropic 298 } 299 300 // Determine authentication method based on available tokens 301 const clientConfig: ConstructorParameters<typeof Anthropic>[0] = { 302 apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(), 303 authToken: isClaudeAISubscriber() 304 ? getClaudeAIOAuthTokens()?.accessToken 305 : undefined, 306 // Set baseURL from OAuth config when using staging OAuth 307 ...(process.env.USER_TYPE === 'ant' && 308 isEnvTruthy(process.env.USE_STAGING_OAUTH) 309 ? { baseURL: getOauthConfig().BASE_API_URL } 310 : {}), 311 ...ARGS, 312 ...(isDebugToStdErr() && { logger: createStderrLogger() }), 313 } 314 315 return new Anthropic(clientConfig) 316} 317 318async function configureApiKeyHeaders( 319 headers: Record<string, string>, 320 isNonInteractiveSession: boolean, 321): Promise<void> { 322 const token = 323 process.env.ANTHROPIC_AUTH_TOKEN || 324 (await getApiKeyFromApiKeyHelper(isNonInteractiveSession)) 325 if (token) { 326 headers['Authorization'] = `Bearer ${token}` 327 } 328} 329 330function getCustomHeaders(): Record<string, string> { 331 const customHeaders: Record<string, string> = {} 332 const customHeadersEnv = process.env.ANTHROPIC_CUSTOM_HEADERS 333 334 if (!customHeadersEnv) return customHeaders 335 336 // Split by newlines to support multiple headers 337 const headerStrings = customHeadersEnv.split(/\n|\r\n/) 338 339 for (const headerString of headerStrings) { 340 if (!headerString.trim()) continue 341 342 // Parse header in format "Name: Value" (curl style). Split on first `:` 343 // then trim — avoids regex backtracking on malformed long header lines. 344 const colonIdx = headerString.indexOf(':') 345 if (colonIdx === -1) continue 346 const name = headerString.slice(0, colonIdx).trim() 347 const value = headerString.slice(colonIdx + 1).trim() 348 if (name) { 349 customHeaders[name] = value 350 } 351 } 352 353 return customHeaders 354} 355 356export const CLIENT_REQUEST_ID_HEADER = 'x-client-request-id' 357 358function buildFetch( 359 fetchOverride: ClientOptions['fetch'], 360 source: string | undefined, 361): ClientOptions['fetch'] { 362 // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins 363 const inner = fetchOverride ?? globalThis.fetch 364 // Only send to the first-party API — Bedrock/Vertex/Foundry don't log it 365 // and unknown headers risk rejection by strict proxies (inc-4029 class). 366 const injectClientRequestId = 367 getAPIProvider() === 'firstParty' && isFirstPartyAnthropicBaseUrl() 368 return (input, init) => { 369 // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins 370 const headers = new Headers(init?.headers) 371 // Generate a client-side request ID so timeouts (which return no server 372 // request ID) can still be correlated with server logs by the API team. 373 // Callers that want to track the ID themselves can pre-set the header. 374 if (injectClientRequestId && !headers.has(CLIENT_REQUEST_ID_HEADER)) { 375 headers.set(CLIENT_REQUEST_ID_HEADER, randomUUID()) 376 } 377 try { 378 // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins 379 const url = input instanceof Request ? input.url : String(input) 380 const id = headers.get(CLIENT_REQUEST_ID_HEADER) 381 logForDebugging( 382 `[API REQUEST] ${new URL(url).pathname}${id ? ` ${CLIENT_REQUEST_ID_HEADER}=${id}` : ''} source=${source ?? 'unknown'}`, 383 ) 384 } catch { 385 // never let logging crash the fetch 386 } 387 return inner(input, { ...init, headers }) 388 } 389}