import type Anthropic from '@anthropic-ai/sdk'; import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod'; import { getSettings } from '../adminSettings'; import { DraftSchema, DraftResult } from './draftSchema'; import { buildSystemPrompt, buildUserContent } from './draftPrompt'; import { costMicros } from './models'; /** * Read from Admin settings rather than the environment, so the choice can be * changed without a redeploy. getSettings supplies the fallback, so there is no * second default here to disagree with the one in the catalogue. */ async function draftingModel(): Promise { return (await getSettings()).draftingModel; } /** * Enough for a listing and its tags, and low enough that a model which starts * rambling is cut off rather than billed for indefinitely. */ const MAX_TOKENS = 2000; export interface DraftInput { photos: { mediaType: string; base64: string }[]; note: string | null; categories: string[]; tags: string[]; } export interface DraftOutcome { draft: DraftResult; model: string; inputTokens: number; outputTokens: number; costMicros: number; } /** * One submission, one draft. * * The client is a parameter rather than a module import so every test can pass * a stub. A test that reaches the real API is a defect in the test: this runs * on a public route and each call costs money. */ export async function draftListing( client: Anthropic, input: DraftInput ): Promise { const model = await draftingModel(); const response = await client.messages.parse({ model, max_tokens: MAX_TOKENS, system: buildSystemPrompt(input.categories, input.tags), messages: [{ role: 'user', content: buildUserContent(input.photos, input.note) as never }], output_config: { format: zodOutputFormat(DraftSchema) } }); // Null when the response did not satisfy the schema. Guarded rather than // asserted: the SDK's own examples reach for it with `?.`, and a model // answering in prose is exactly the case worth failing cleanly on. const draft = response.parsed_output; if (!draft) { throw new Error('the model did not return a draft matching the expected shape'); } const inputTokens = response.usage?.input_tokens ?? 0; const outputTokens = response.usage?.output_tokens ?? 0; return { draft, model, inputTokens, outputTokens, costMicros: costMicros(model, inputTokens, outputTokens) }; }