Files
TREK/server/src/nest/llm-parse/clients/anthropic.client.ts
T
2026-06-24 18:45:52 +02:00

86 lines
2.9 KiB
TypeScript

import type { LlmExtractionClient, LlmExtractionInput } from '../llm-provider.interface';
const TIMEOUT_MS = 120_000;
const MAX_TOKENS = 8192;
const ANTHROPIC_VERSION = '2023-06-01';
const TOOL_NAME = 'emit_reservations';
/**
* Anthropic Messages API client. Structured output via forced tool-use: a single
* `emit_reservations` tool whose `input_schema` is the reservations schema, with
* `tool_choice` forcing it — the documented, reliable way to get structured JSON.
* PDFs go as native base64 `document` blocks (Anthropic reads scanned PDFs).
* Raw fetch (no SDK) to match the codebase's HTTP style.
*/
export class AnthropicClient implements LlmExtractionClient {
async extract(input: LlmExtractionInput): Promise<Record<string, unknown>[]> {
const base = (input.baseUrl ?? 'https://api.anthropic.com').replace(/\/+$/, '');
const url = `${base}/v1/messages`;
const content: unknown[] = [];
if (input.file) {
content.push({
type: 'document',
source: { type: 'base64', media_type: input.file.mimeType, data: input.file.data.toString('base64') },
});
}
content.push({
type: 'text',
text: input.text ? `${USER_TEXT}\n\n${input.text}` : USER_TEXT,
});
const body = {
model: input.model,
max_tokens: MAX_TOKENS,
system: input.prompt,
tools: [
{
name: TOOL_NAME,
description: 'Return the travel reservations extracted from the document.',
input_schema: input.jsonSchema,
},
],
tool_choice: { type: 'tool', name: TOOL_NAME },
messages: [{ role: 'user', content }],
};
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
let res: Response;
try {
res = await fetch(url, {
method: 'POST',
signal: controller.signal,
headers: {
'content-type': 'application/json',
'x-api-key': input.apiKey ?? '',
'anthropic-version': ANTHROPIC_VERSION,
},
body: JSON.stringify(body),
});
} finally {
clearTimeout(timer);
}
if (!res.ok) {
const detail = await res.text().catch(() => '');
throw new Error(`Anthropic request failed (${res.status}): ${detail.slice(0, 300)}`);
}
const data = (await res.json()) as {
stop_reason?: string;
content?: { type: string; name?: string; input?: { reservations?: unknown } }[];
};
if (data.stop_reason === 'refusal') {
throw new Error('Anthropic declined to process this document');
}
const toolUse = data.content?.find(b => b.type === 'tool_use' && b.name === TOOL_NAME);
const reservations = toolUse?.input?.reservations;
return Array.isArray(reservations) ? (reservations as Record<string, unknown>[]) : [];
}
}
const USER_TEXT = 'Extract every travel reservation from the following document as schema.org JSON-LD.';