UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

195 lines 7.83 kB
/** * Anthropic Message Batches API Adapter * * Implements batch processing using Anthropic's Message Batches API: * - 50% cost discount * - 24-hour turnaround * - Up to 10,000 requests per batch * * This file is a small adapter on top of the BatchProvider port (`./provider.js`). * It owns only the Anthropic-specific request/response shapes and HTTP calls; * shared concerns (polling, JSON-Schema conversion, JSON parsing) live in the port. * * @see https://docs.anthropic.com/en/docs/build-with-claude/message-batches * * @packageDocumentation */ import { schema as convertSchema } from '../schema.js'; import { pollUntilComplete, registerBatchAdapter, tryParseJson, zodToJsonSchema, } from './provider.js'; // ============================================================================ // Anthropic client // ============================================================================ let anthropicApiKey; let anthropicBaseUrl = 'https://api.anthropic.com/v1'; const ANTHROPIC_VERSION = '2023-06-01'; const ANTHROPIC_BETA = 'message-batches-2024-09-24'; /** Configure the Anthropic client. */ export function configureAnthropic(options) { if (options.apiKey) anthropicApiKey = options.apiKey; if (options.baseUrl) anthropicBaseUrl = options.baseUrl; } function getApiKey() { const key = anthropicApiKey || process.env['ANTHROPIC_API_KEY']; if (!key) { throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY or call configureAnthropic()'); } return key; } function anthropicHeaders() { return { 'x-api-key': getApiKey(), 'anthropic-version': ANTHROPIC_VERSION, 'anthropic-beta': ANTHROPIC_BETA, 'Content-Type': 'application/json', }; } async function anthropicRequest(method, path, body) { const response = await fetch(`${anthropicBaseUrl}${path}`, { method, headers: anthropicHeaders(), ...(body !== undefined && { body: JSON.stringify(body) }), }); if (!response.ok) { const error = await response.text(); throw new Error(`Anthropic API error: ${response.status} ${error}`); } return response.json(); } function mapStatus(batch) { if (batch.cancel_initiated_at) { return batch.processing_status === 'ended' ? 'cancelled' : 'cancelling'; } if (batch.processing_status === 'ended') { return 'completed'; } return 'in_progress'; } function toBatchJob(batch, totalItemsHint) { const totalFromCounts = batch.request_counts.processing + batch.request_counts.succeeded + batch.request_counts.errored + batch.request_counts.canceled + batch.request_counts.expired; return { id: batch.id, provider: 'anthropic', status: mapStatus(batch), totalItems: totalItemsHint ?? totalFromCounts, completedItems: batch.request_counts.succeeded, failedItems: batch.request_counts.errored + batch.request_counts.expired + batch.request_counts.canceled, createdAt: new Date(batch.created_at), ...(batch.ended_at && { completedAt: new Date(batch.ended_at) }), expiresAt: new Date(batch.expires_at), }; } // ============================================================================ // Anthropic adapter (BatchProvider port) // ============================================================================ const anthropicAdapter = { async submit(items, options) { const model = options.model || 'claude-sonnet-4-20250514'; const maxTokens = 4096; const requests = items.map((item) => { const request = { custom_id: item.id, params: { model, max_tokens: item.options?.maxTokens || maxTokens, messages: [{ role: 'user', content: item.prompt }], ...(item.options?.system !== undefined && { system: item.options.system }), ...(item.options?.temperature !== undefined && { temperature: item.options.temperature, }), }, }; if (item.schema) { const zodSchema = convertSchema(item.schema); request.params.tools = [ { name: 'structured_response', description: 'Generate a structured response matching the schema', input_schema: zodToJsonSchema(zodSchema), }, ]; request.params.tool_choice = { type: 'tool', name: 'structured_response' }; } return request; }); const batch = await anthropicRequest('POST', '/messages/batches', { requests, }); const job = toBatchJob(batch, items.length); if (options.webhookUrl !== undefined) { job.webhookUrl = options.webhookUrl; } const completion = this.waitForCompletion(batch.id); return { job, completion }; }, async getStatus(batchId) { const batch = await anthropicRequest('GET', `/messages/batches/${batchId}`); return toBatchJob(batch); }, async cancel(batchId) { await anthropicRequest('POST', `/messages/batches/${batchId}/cancel`); }, async getResults(batchId) { const status = await this.getStatus(batchId); if (status.status !== 'completed' && status.status !== 'cancelled') { throw new Error(`Batch not complete. Status: ${status.status}`); } // Anthropic returns results via a signed URL on the batch object. const batch = await anthropicRequest('GET', `/messages/batches/${batchId}`); if (!batch.results_url) { throw new Error('No results URL available'); } const response = await fetch(batch.results_url, { headers: anthropicHeaders() }); if (!response.ok) { throw new Error(`Failed to fetch results: ${response.status}`); } const lines = (await response.text()).trim().split('\n'); return lines.map(parseAnthropicResult); }, async waitForCompletion(batchId, pollInterval = 5000) { return pollUntilComplete(this, batchId, { pollInterval }); }, }; function parseAnthropicResult(line) { const result = JSON.parse(line); if (result.result.type === 'succeeded' && result.result.message) { const message = result.result.message; const toolUse = message.content.find((c) => c.type === 'tool_use'); const textContent = message.content.find((c) => c.type === 'text'); let extractedResult; if (toolUse?.input) { extractedResult = toolUse.input; } else if (textContent?.text) { extractedResult = tryParseJson(textContent.text); } return { id: result.custom_id, customId: result.custom_id, status: 'completed', result: extractedResult, usage: { promptTokens: message.usage.input_tokens, completionTokens: message.usage.output_tokens, totalTokens: message.usage.input_tokens + message.usage.output_tokens, }, }; } return { id: result.custom_id, customId: result.custom_id, status: 'failed', error: result.result.error?.message || `Request ${result.result.type}`, }; } // ============================================================================ // Register adapter // ============================================================================ registerBatchAdapter('anthropic', anthropicAdapter); export { anthropicAdapter }; //# sourceMappingURL=anthropic.js.map