forked from sanbuphy/learn-coding-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.ts
More file actions
389 lines (360 loc) · 15.8 KB
/
client.ts
File metadata and controls
389 lines (360 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
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 })
}
}