init claude-code
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
import Anthropic, { type ClientOptions } from '@anthropic-ai/sdk'
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { GoogleAuth } from 'google-auth-library'
|
||||
import {
|
||||
checkAndRefreshOAuthTokenIfNeeded,
|
||||
getAnthropicApiKey,
|
||||
getApiKeyFromApiKeyHelper,
|
||||
getClaudeAIOAuthTokens,
|
||||
isClaudeAISubscriber,
|
||||
refreshAndGetAwsCredentials,
|
||||
refreshGcpCredentialsIfNeeded,
|
||||
} from 'src/utils/auth.js'
|
||||
import { getUserAgent } from 'src/utils/http.js'
|
||||
import { getSmallFastModel } from 'src/utils/model/model.js'
|
||||
import {
|
||||
getAPIProvider,
|
||||
isFirstPartyAnthropicBaseUrl,
|
||||
} from 'src/utils/model/providers.js'
|
||||
import { getProxyFetchOptions } from 'src/utils/proxy.js'
|
||||
import {
|
||||
getIsNonInteractiveSession,
|
||||
getSessionId,
|
||||
} from '../../bootstrap/state.js'
|
||||
import { getOauthConfig } from '../../constants/oauth.js'
|
||||
import { isDebugToStdErr, logForDebugging } from '../../utils/debug.js'
|
||||
import {
|
||||
getAWSRegion,
|
||||
getVertexRegionForModel,
|
||||
isEnvTruthy,
|
||||
} from '../../utils/envUtils.js'
|
||||
|
||||
/**
|
||||
* Environment variables for different client types:
|
||||
*
|
||||
* Direct API:
|
||||
* - ANTHROPIC_API_KEY: Required for direct API access
|
||||
*
|
||||
* AWS Bedrock:
|
||||
* - AWS credentials configured via aws-sdk defaults
|
||||
* - AWS_REGION or AWS_DEFAULT_REGION: Sets the AWS region for all models (default: us-east-1)
|
||||
* - ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION: Optional. Override AWS region specifically for the small fast model (Haiku)
|
||||
*
|
||||
* Foundry (Azure):
|
||||
* - ANTHROPIC_FOUNDRY_RESOURCE: Your Azure resource name (e.g., 'my-resource')
|
||||
* For the full endpoint: https://{resource}.services.ai.azure.com/anthropic/v1/messages
|
||||
* - ANTHROPIC_FOUNDRY_BASE_URL: Optional. Alternative to resource - provide full base URL directly
|
||||
* (e.g., 'https://my-resource.services.ai.azure.com')
|
||||
*
|
||||
* Authentication (one of the following):
|
||||
* - ANTHROPIC_FOUNDRY_API_KEY: Your Microsoft Foundry API key (if using API key auth)
|
||||
* - Azure AD authentication: If no API key is provided, uses DefaultAzureCredential
|
||||
* which supports multiple auth methods (environment variables, managed identity,
|
||||
* Azure CLI, etc.). See: https://docs.microsoft.com/en-us/javascript/api/@azure/identity
|
||||
*
|
||||
* Vertex AI:
|
||||
* - Model-specific region variables (highest priority):
|
||||
* - VERTEX_REGION_CLAUDE_3_5_HAIKU: Region for Claude 3.5 Haiku model
|
||||
* - VERTEX_REGION_CLAUDE_HAIKU_4_5: Region for Claude Haiku 4.5 model
|
||||
* - VERTEX_REGION_CLAUDE_3_5_SONNET: Region for Claude 3.5 Sonnet model
|
||||
* - VERTEX_REGION_CLAUDE_3_7_SONNET: Region for Claude 3.7 Sonnet model
|
||||
* - CLOUD_ML_REGION: Optional. The default GCP region to use for all models
|
||||
* If specific model region not specified above
|
||||
* - ANTHROPIC_VERTEX_PROJECT_ID: Required. Your GCP project ID
|
||||
* - Standard GCP credentials configured via google-auth-library
|
||||
*
|
||||
* Priority for determining region:
|
||||
* 1. Hardcoded model-specific environment variables
|
||||
* 2. Global CLOUD_ML_REGION variable
|
||||
* 3. Default region from config
|
||||
* 4. Fallback region (us-east5)
|
||||
*/
|
||||
|
||||
function createStderrLogger(): ClientOptions['logger'] {
|
||||
return {
|
||||
error: (msg, ...args) =>
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console
|
||||
console.error('[Anthropic SDK ERROR]', msg, ...args),
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console
|
||||
warn: (msg, ...args) => console.error('[Anthropic SDK WARN]', msg, ...args),
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console
|
||||
info: (msg, ...args) => console.error('[Anthropic SDK INFO]', msg, ...args),
|
||||
debug: (msg, ...args) =>
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output -- SDK logger must use console
|
||||
console.error('[Anthropic SDK DEBUG]', msg, ...args),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAnthropicClient({
|
||||
apiKey,
|
||||
maxRetries,
|
||||
model,
|
||||
fetchOverride,
|
||||
source,
|
||||
}: {
|
||||
apiKey?: string
|
||||
maxRetries: number
|
||||
model?: string
|
||||
fetchOverride?: ClientOptions['fetch']
|
||||
source?: string
|
||||
}): Promise<Anthropic> {
|
||||
const containerId = process.env.CLAUDE_CODE_CONTAINER_ID
|
||||
const remoteSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID
|
||||
const clientApp = process.env.CLAUDE_AGENT_SDK_CLIENT_APP
|
||||
const customHeaders = getCustomHeaders()
|
||||
const defaultHeaders: { [key: string]: string } = {
|
||||
'x-app': 'cli',
|
||||
'User-Agent': getUserAgent(),
|
||||
'X-Claude-Code-Session-Id': getSessionId(),
|
||||
...customHeaders,
|
||||
...(containerId ? { 'x-claude-remote-container-id': containerId } : {}),
|
||||
...(remoteSessionId
|
||||
? { 'x-claude-remote-session-id': remoteSessionId }
|
||||
: {}),
|
||||
// SDK consumers can identify their app/library for backend analytics
|
||||
...(clientApp ? { 'x-client-app': clientApp } : {}),
|
||||
}
|
||||
|
||||
// Log API client configuration for HFI debugging
|
||||
logForDebugging(
|
||||
`[API:request] Creating client, ANTHROPIC_CUSTOM_HEADERS present: ${!!process.env.ANTHROPIC_CUSTOM_HEADERS}, has Authorization header: ${!!customHeaders['Authorization']}`,
|
||||
)
|
||||
|
||||
// Add additional protection header if enabled via env var
|
||||
const additionalProtectionEnabled = isEnvTruthy(
|
||||
process.env.CLAUDE_CODE_ADDITIONAL_PROTECTION,
|
||||
)
|
||||
if (additionalProtectionEnabled) {
|
||||
defaultHeaders['x-anthropic-additional-protection'] = 'true'
|
||||
}
|
||||
|
||||
logForDebugging('[API:auth] OAuth token check starting')
|
||||
await checkAndRefreshOAuthTokenIfNeeded()
|
||||
logForDebugging('[API:auth] OAuth token check complete')
|
||||
|
||||
if (!isClaudeAISubscriber()) {
|
||||
await configureApiKeyHeaders(defaultHeaders, getIsNonInteractiveSession())
|
||||
}
|
||||
|
||||
const resolvedFetch = buildFetch(fetchOverride, source)
|
||||
|
||||
const ARGS = {
|
||||
defaultHeaders,
|
||||
maxRetries,
|
||||
timeout: parseInt(process.env.API_TIMEOUT_MS || String(600 * 1000), 10),
|
||||
dangerouslyAllowBrowser: true,
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
forAnthropicAPI: true,
|
||||
}) as ClientOptions['fetchOptions'],
|
||||
...(resolvedFetch && {
|
||||
fetch: resolvedFetch,
|
||||
}),
|
||||
}
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)) {
|
||||
const { AnthropicBedrock } = await import('@anthropic-ai/bedrock-sdk')
|
||||
// Use region override for small fast model if specified
|
||||
const awsRegion =
|
||||
model === getSmallFastModel() &&
|
||||
process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION
|
||||
? process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION
|
||||
: getAWSRegion()
|
||||
|
||||
const bedrockArgs: ConstructorParameters<typeof AnthropicBedrock>[0] = {
|
||||
...ARGS,
|
||||
awsRegion,
|
||||
...(isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH) && {
|
||||
skipAuth: true,
|
||||
}),
|
||||
...(isDebugToStdErr() && { logger: createStderrLogger() }),
|
||||
}
|
||||
|
||||
// Add API key authentication if available
|
||||
if (process.env.AWS_BEARER_TOKEN_BEDROCK) {
|
||||
bedrockArgs.skipAuth = true
|
||||
// Add the Bearer token for Bedrock API key authentication
|
||||
bedrockArgs.defaultHeaders = {
|
||||
...bedrockArgs.defaultHeaders,
|
||||
Authorization: `Bearer ${process.env.AWS_BEARER_TOKEN_BEDROCK}`,
|
||||
}
|
||||
} else if (!isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH)) {
|
||||
// Refresh auth and get credentials with cache clearing
|
||||
const cachedCredentials = await refreshAndGetAwsCredentials()
|
||||
if (cachedCredentials) {
|
||||
bedrockArgs.awsAccessKey = cachedCredentials.accessKeyId
|
||||
bedrockArgs.awsSecretKey = cachedCredentials.secretAccessKey
|
||||
bedrockArgs.awsSessionToken = cachedCredentials.sessionToken
|
||||
}
|
||||
}
|
||||
// we have always been lying about the return type - this doesn't support batching or models
|
||||
return new AnthropicBedrock(bedrockArgs) as unknown as Anthropic
|
||||
}
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)) {
|
||||
const { AnthropicFoundry } = await import('@anthropic-ai/foundry-sdk')
|
||||
// Determine Azure AD token provider based on configuration
|
||||
// SDK reads ANTHROPIC_FOUNDRY_API_KEY by default
|
||||
let azureADTokenProvider: (() => Promise<string>) | undefined
|
||||
if (!process.env.ANTHROPIC_FOUNDRY_API_KEY) {
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_FOUNDRY_AUTH)) {
|
||||
// Mock token provider for testing/proxy scenarios (similar to Vertex mock GoogleAuth)
|
||||
azureADTokenProvider = () => Promise.resolve('')
|
||||
} else {
|
||||
// Use real Azure AD authentication with DefaultAzureCredential
|
||||
const {
|
||||
DefaultAzureCredential: AzureCredential,
|
||||
getBearerTokenProvider,
|
||||
} = await import('@azure/identity')
|
||||
azureADTokenProvider = getBearerTokenProvider(
|
||||
new AzureCredential(),
|
||||
'https://cognitiveservices.azure.com/.default',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const foundryArgs: ConstructorParameters<typeof AnthropicFoundry>[0] = {
|
||||
...ARGS,
|
||||
...(azureADTokenProvider && { azureADTokenProvider }),
|
||||
...(isDebugToStdErr() && { logger: createStderrLogger() }),
|
||||
}
|
||||
// we have always been lying about the return type - this doesn't support batching or models
|
||||
return new AnthropicFoundry(foundryArgs) as unknown as Anthropic
|
||||
}
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)) {
|
||||
// Refresh GCP credentials if gcpAuthRefresh is configured and credentials are expired
|
||||
// This is similar to how we handle AWS credential refresh for Bedrock
|
||||
if (!isEnvTruthy(process.env.CLAUDE_CODE_SKIP_VERTEX_AUTH)) {
|
||||
await refreshGcpCredentialsIfNeeded()
|
||||
}
|
||||
|
||||
const [{ AnthropicVertex }, { GoogleAuth }] = await Promise.all([
|
||||
import('@anthropic-ai/vertex-sdk'),
|
||||
import('google-auth-library'),
|
||||
])
|
||||
// TODO: Cache either GoogleAuth instance or AuthClient to improve performance
|
||||
// Currently we create a new GoogleAuth instance for every getAnthropicClient() call
|
||||
// This could cause repeated authentication flows and metadata server checks
|
||||
// However, caching needs careful handling of:
|
||||
// - Credential refresh/expiration
|
||||
// - Environment variable changes (GOOGLE_APPLICATION_CREDENTIALS, project vars)
|
||||
// - Cross-request auth state management
|
||||
// See: https://github.com/googleapis/google-auth-library-nodejs/issues/390 for caching challenges
|
||||
|
||||
// Prevent metadata server timeout by providing projectId as fallback
|
||||
// google-auth-library checks project ID in this order:
|
||||
// 1. Environment variables (GCLOUD_PROJECT, GOOGLE_CLOUD_PROJECT, etc.)
|
||||
// 2. Credential files (service account JSON, ADC file)
|
||||
// 3. gcloud config
|
||||
// 4. GCE metadata server (causes 12s timeout outside GCP)
|
||||
//
|
||||
// We only set projectId if user hasn't configured other discovery methods
|
||||
// to avoid interfering with their existing auth setup
|
||||
|
||||
// Check project environment variables in same order as google-auth-library
|
||||
// See: https://github.com/googleapis/google-auth-library-nodejs/blob/main/src/auth/googleauth.ts
|
||||
const hasProjectEnvVar =
|
||||
process.env['GCLOUD_PROJECT'] ||
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] ||
|
||||
process.env['gcloud_project'] ||
|
||||
process.env['google_cloud_project']
|
||||
|
||||
// Check for credential file paths (service account or ADC)
|
||||
// Note: We're checking both standard and lowercase variants to be safe,
|
||||
// though we should verify what google-auth-library actually checks
|
||||
const hasKeyFile =
|
||||
process.env['GOOGLE_APPLICATION_CREDENTIALS'] ||
|
||||
process.env['google_application_credentials']
|
||||
|
||||
const googleAuth = isEnvTruthy(process.env.CLAUDE_CODE_SKIP_VERTEX_AUTH)
|
||||
? ({
|
||||
// Mock GoogleAuth for testing/proxy scenarios
|
||||
getClient: () => ({
|
||||
getRequestHeaders: () => ({}),
|
||||
}),
|
||||
} as unknown as GoogleAuth)
|
||||
: new GoogleAuth({
|
||||
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
||||
// Only use ANTHROPIC_VERTEX_PROJECT_ID as last resort fallback
|
||||
// This prevents the 12-second metadata server timeout when:
|
||||
// - No project env vars are set AND
|
||||
// - No credential keyfile is specified AND
|
||||
// - ADC file exists but lacks project_id field
|
||||
//
|
||||
// Risk: If auth project != API target project, this could cause billing/audit issues
|
||||
// Mitigation: Users can set GOOGLE_CLOUD_PROJECT to override
|
||||
...(hasProjectEnvVar || hasKeyFile
|
||||
? {}
|
||||
: {
|
||||
projectId: process.env.ANTHROPIC_VERTEX_PROJECT_ID,
|
||||
}),
|
||||
})
|
||||
|
||||
const vertexArgs: ConstructorParameters<typeof AnthropicVertex>[0] = {
|
||||
...ARGS,
|
||||
region: getVertexRegionForModel(model),
|
||||
googleAuth,
|
||||
...(isDebugToStdErr() && { logger: createStderrLogger() }),
|
||||
}
|
||||
// we have always been lying about the return type - this doesn't support batching or models
|
||||
return new AnthropicVertex(vertexArgs) as unknown as Anthropic
|
||||
}
|
||||
|
||||
// Determine authentication method based on available tokens
|
||||
const clientConfig: ConstructorParameters<typeof Anthropic>[0] = {
|
||||
apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(),
|
||||
authToken: isClaudeAISubscriber()
|
||||
? getClaudeAIOAuthTokens()?.accessToken
|
||||
: undefined,
|
||||
// Set baseURL from OAuth config when using staging OAuth
|
||||
...(process.env.USER_TYPE === 'ant' &&
|
||||
isEnvTruthy(process.env.USE_STAGING_OAUTH)
|
||||
? { baseURL: getOauthConfig().BASE_API_URL }
|
||||
: {}),
|
||||
...ARGS,
|
||||
...(isDebugToStdErr() && { logger: createStderrLogger() }),
|
||||
}
|
||||
|
||||
return new Anthropic(clientConfig)
|
||||
}
|
||||
|
||||
async function configureApiKeyHeaders(
|
||||
headers: Record<string, string>,
|
||||
isNonInteractiveSession: boolean,
|
||||
): Promise<void> {
|
||||
const token =
|
||||
process.env.ANTHROPIC_AUTH_TOKEN ||
|
||||
(await getApiKeyFromApiKeyHelper(isNonInteractiveSession))
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomHeaders(): Record<string, string> {
|
||||
const customHeaders: Record<string, string> = {}
|
||||
const customHeadersEnv = process.env.ANTHROPIC_CUSTOM_HEADERS
|
||||
|
||||
if (!customHeadersEnv) return customHeaders
|
||||
|
||||
// Split by newlines to support multiple headers
|
||||
const headerStrings = customHeadersEnv.split(/\n|\r\n/)
|
||||
|
||||
for (const headerString of headerStrings) {
|
||||
if (!headerString.trim()) continue
|
||||
|
||||
// Parse header in format "Name: Value" (curl style). Split on first `:`
|
||||
// then trim — avoids regex backtracking on malformed long header lines.
|
||||
const colonIdx = headerString.indexOf(':')
|
||||
if (colonIdx === -1) continue
|
||||
const name = headerString.slice(0, colonIdx).trim()
|
||||
const value = headerString.slice(colonIdx + 1).trim()
|
||||
if (name) {
|
||||
customHeaders[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
return customHeaders
|
||||
}
|
||||
|
||||
export const CLIENT_REQUEST_ID_HEADER = 'x-client-request-id'
|
||||
|
||||
function buildFetch(
|
||||
fetchOverride: ClientOptions['fetch'],
|
||||
source: string | undefined,
|
||||
): ClientOptions['fetch'] {
|
||||
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
|
||||
const inner = fetchOverride ?? globalThis.fetch
|
||||
// Only send to the first-party API — Bedrock/Vertex/Foundry don't log it
|
||||
// and unknown headers risk rejection by strict proxies (inc-4029 class).
|
||||
const injectClientRequestId =
|
||||
getAPIProvider() === 'firstParty' && isFirstPartyAnthropicBaseUrl()
|
||||
return (input, init) => {
|
||||
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
|
||||
const headers = new Headers(init?.headers)
|
||||
// Generate a client-side request ID so timeouts (which return no server
|
||||
// request ID) can still be correlated with server logs by the API team.
|
||||
// Callers that want to track the ID themselves can pre-set the header.
|
||||
if (injectClientRequestId && !headers.has(CLIENT_REQUEST_ID_HEADER)) {
|
||||
headers.set(CLIENT_REQUEST_ID_HEADER, randomUUID())
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
|
||||
const url = input instanceof Request ? input.url : String(input)
|
||||
const id = headers.get(CLIENT_REQUEST_ID_HEADER)
|
||||
logForDebugging(
|
||||
`[API REQUEST] ${new URL(url).pathname}${id ? ` ${CLIENT_REQUEST_ID_HEADER}=${id}` : ''} source=${source ?? 'unknown'}`,
|
||||
)
|
||||
} catch {
|
||||
// never let logging crash the fetch
|
||||
}
|
||||
return inner(input, { ...init, headers })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user