import Anthropic from '@anthropic-ai/sdk'; /** * The client, or null when there is no key. * * Null rather than a throw, because an unconfigured environment is a working * one: submissions still arrive and wait undrafted. The worker treats null * exactly as it treats a failed call, which keeps one path rather than two. * * Constructed once and cached. The SDK holds a connection pool, and building * one per submission would be wasteful on a route a stranger can trigger. */ let cached: Anthropic | null = null; let resolved = false; /** * The headers a key needs beyond the key itself. * * An *identity-linked* key — one issued against a workspace rather than * standing alone — is refused without an `anthropic-workspace-id` naming the * workspace the request acts in: * * 400 invalid_request_error: anthropic-workspace-id is required when * authenticating with an identity-linked API key * * Nothing about a key's shape says which kind it is, so this cannot be detected * from configuration — only from a real call, which is what #223's task 8 was * for and what found it (#271). * * Sent only when set. Plenty of keys need no workspace, and sending an empty * header would turn the ordinary case into a different error. */ function workspaceHeaders(): Record | undefined { const workspaceId = process.env.ANTHROPIC_WORKSPACE_ID; if (workspaceId === undefined || workspaceId.trim() === '') return undefined; return { 'anthropic-workspace-id': workspaceId.trim() }; } export function getAnthropicClient(): Anthropic | null { if (resolved) return cached; const key = process.env.ANTHROPIC_API_KEY; cached = key !== undefined && key.trim() !== '' ? new Anthropic({ apiKey: key, defaultHeaders: workspaceHeaders() }) : null; resolved = true; return cached; } /** Exposed for tests, which need a fresh decision per case. */ export function resetAnthropicClient(): void { cached = null; resolved = false; }