Download src/services/api/client.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 18.8 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/services/api/client.ts
- Command line
-
hf download hf://chenbhao/codev/src/services/api/client.ts
-
curl -L -o client.ts https://huggingface.co/chenbhao/codev/resolve/main/src/services/api/client.ts
18.8 kB
| 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, | |
| getOpenRouterApiKey, | |
| getOpenAIApiKey, | |
| getLocalBaseUrl, | |
| getOpenCodeApiKey, | |
| getOpenCodeModelName, | |
| } from 'src/utils/auth.js' | |
| import { getUserAgent } from 'src/utils/http.js' | |
| import { getSmallFastModel } from 'src/utils/model/model.js' | |
| import { | |
| getAPIProvider, | |
| isFirstPartyAnthropicBaseUrl, | |
| getOpenRouterBaseUrl, | |
| getOpenAIBaseUrl, | |
| getOpencodeBaseUrl, | |
| } from 'src/utils/model/providers.js' | |
| import { getProxyFetchOptions } from 'src/utils/proxy.js' | |
| import { createOpenCodeFetchOverride, fetchOpencodeModels } from './opencodeClient.js' | |
| import { createNvidiaFetchOverride } from './nvidiaClient.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()) | |
| } | |
| // For OpenCode provider, use custom fetch to convert Anthropic format to OpenAI format | |
| const provider = getAPIProvider() | |
| let opencodeFetchOverride: ClientOptions['fetch'] | undefined | |
| if (provider === 'opencode') { | |
| const resolvedModel = model || getOpenCodeModelName() || 'big-pickle' | |
| // Fetch models in background | |
| void fetchOpencodeModels() | |
| opencodeFetchOverride = createOpenCodeFetchOverride(resolvedModel) | |
| } | |
| // For NVIDIA provider, use custom fetch to convert Anthropic format to OpenAI-compatible format | |
| let nvidiaFetchOverride: ClientOptions['fetch'] | undefined | |
| if (provider === 'nvidia') { | |
| nvidiaFetchOverride = createNvidiaFetchOverride() | |
| } | |
| const resolvedFetch = buildFetch(fetchOverride || opencodeFetchOverride || nvidiaFetchOverride, 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 | |
| let 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() }), | |
| } | |
| // Handle OpenRouter | |
| if (provider === 'openrouter') { | |
| const openRouterApiKey = getOpenRouterApiKey() | |
| if (openRouterApiKey) { | |
| clientConfig.apiKey = openRouterApiKey | |
| clientConfig.baseURL = getOpenRouterBaseUrl() | |
| } | |
| } | |
| // Handle OpenAI | |
| if (provider === 'openai') { | |
| const openAIApiKey = getOpenAIApiKey() | |
| if (openAIApiKey) { | |
| clientConfig.apiKey = openAIApiKey | |
| clientConfig.baseURL = getOpenAIBaseUrl() | |
| } | |
| } | |
| // Handle Local | |
| if (provider === 'local') { | |
| const localBaseUrl = await getLocalBaseUrl() | |
| if (localBaseUrl) { | |
| clientConfig.baseURL = localBaseUrl | |
| clientConfig.apiKey = 'local-model' // Use a dummy API key for local models | |
| } | |
| } | |
| // Handle OpenCode Zen - uses custom fetch override to convert Anthropic format to OpenAI format | |
| // No need to set baseURL here as the fetch override handles the endpoint directly | |
| if (provider === 'opencode') { | |
| // The fetch override will handle the API conversion | |
| // Just set a dummy apiKey to satisfy the SDK | |
| clientConfig.apiKey = 'opencode-zen' | |
| } | |
| // Handle NVIDIA - uses custom fetch override to convert Anthropic format to OpenAI format | |
| if (provider === 'nvidia') { | |
| clientConfig.apiKey = 'nvidia-nim' | |
| } | |
| return new Anthropic(clientConfig) | |
| } | |
| async function configureApiKeyHeaders( | |
| headers: Record<string, string>, | |
| isNonInteractiveSession: boolean, | |
| ): Promise<void> { | |
| const provider = getAPIProvider() | |
| // Skip for OpenRouter, OpenAI, Local, OpenCode, and NVIDIA - they use apiKey parameter instead | |
| if (provider === 'openrouter' || provider === 'openai' || provider === 'local' || provider === 'opencode' || provider === 'nvidia') { | |
| return | |
| } | |
| 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 }) | |
| } | |
| } | |