TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { z } from 'zod';2import { CATEGORIES, GRADERS } from '@rareindex/taxonomy';3import { getRouter } from '../router.js';4import type { ContentPart, CostContext, ImageInput, ModelProvider } from '../types.js';56/** Structured identification guess (§114). Every field is nullable: unknown stays null, never guessed. */7export const IdentificationSchema = z.object({8 categorySlug: z.string().nullable().describe('One of the RareIndex taxonomy slugs provided, or null'),9 name: z.string().nullable().describe('Item name (e.g. "Charizard", "Daytona", "Air Jordan 1 Chicago", "Millennium Falcon")'),10 brand: z.string().nullable(),11 franchise: z.string().nullable(),12 set: z.string().nullable().describe('Set / series / collection name'),13 number: z.string().nullable().describe('Card number, reference number, set number, style code…'),14 year: z.number().int().nullable(),15 variant: z.string().nullable().describe('Edition/variant: 1st Edition, Holo, Shadowless, Foil, colorway…'),16 language: z.string().nullable(),17 grader: z.string().nullable().describe('psa, bgs, cgc, sgc, wata, vga, pcgs, ngc… or null if raw/unknown'),18 grade: z.string().nullable(),19 certificationNumber: z.string().nullable(),20 conditionNotes: z.string().nullable().describe('Visible condition observations, hedged'),21 likelyGradeRange: z.string().nullable().describe('e.g. "PSA 7–8" — only when the image supports it'),22 confidence: z.number().min(0).max(1).describe('Overall confidence 0–1 that the identification is correct'),23 rationale: z.string().describe('Short explanation of the visual/textual cues used'),24 searchQueries: z.array(z.string()).max(5).describe('Up to 5 short search strings to find this item in a catalog'),25 warnings: z.array(z.string()).describe('Anything suspicious: possible reprint, proxy, mismatched slab, unreadable'),26});27export type Identification = z.infer<typeof IdentificationSchema>;2829function taxonomyPrompt(): string {30 const lines = CATEGORIES.filter((c) => c.phase <= 3).map((c) => `${c.slug} — ${c.name}${c.parent ? ` (child of ${c.parent})` : ''}`);31 return `RareIndex taxonomy slugs (choose the most specific that applies):\n${lines.join('\n')}\n\nGrader slugs: ${GRADERS.map((g) => g.slug).join(', ')}.`;32}3334const SYSTEM = `You are RareIndex's identification model for collectibles (trading cards, sports cards, comics, video games, sneakers, watches, LEGO, toys, coins, books, art…).35Identify the object as precisely as the evidence allows. Never invent details you cannot see or read: unknown fields must be null. Report a calibrated confidence. You do not authenticate items and must flag signs of reprints, proxies or inconsistent grading labels in warnings.36${taxonomyPrompt()}`;3738export interface IdentifyOptions {39 images?: ImageInput[];40 text?: string;41 provider?: ModelProvider;42 cost?: CostContext;43}4445/** Identify a collectible from photos and/or a free-text description. */46export async function identifyCollectible(opts: IdentifyOptions) {47 const provider = opts.provider ?? getRouter();48 const parts: ContentPart[] = [];49 for (const img of opts.images ?? []) parts.push({ type: 'image', image: img });50 if (opts.text) parts.push({ type: 'text', text: `Description / listing text:\n${opts.text}` });51 if (parts.length === 0) throw new Error('identifyCollectible: provide images or text');52 const role = opts.images?.length ? 'vision' : 'classify';53 const res = await provider.extract(role, {54 schema: IdentificationSchema,55 system: SYSTEM,56 prompt: 'Identify this collectible. Fill the schema; use null for anything not evidenced.',57 input: parts,58 maxTokens: 2048,59 effort: 'medium',60 cost: { endpoint: 'identify', ...(opts.cost ?? {}) },61 });62 const data = res.data;63 if (data.categorySlug && !CATEGORIES.some((c) => c.slug === data.categorySlug)) {64 data.warnings.push(`model proposed unknown category ${data.categorySlug}`);65 data.categorySlug = null;66 }67 return { ...res, data };68}6970/** Cheap category classification from a title/description (used by normalizers and taxonomy discovery). */71export const CategoryGuessSchema = z.object({72 categorySlug: z.string().nullable(),73 confidence: z.number().min(0).max(1),74 alternatives: z.array(z.string()).max(3),75});76export async function classifyCategory(title: string, opts: { provider?: ModelProvider; cost?: CostContext } = {}) {77 const provider = opts.provider ?? getRouter();78 const res = await provider.extract('classify', {79 schema: CategoryGuessSchema,80 system: `Classify collectible listings into RareIndex taxonomy slugs. Return null when none applies.\n${taxonomyPrompt()}`,81 prompt: 'Classify this listing title.',82 input: title,83 maxTokens: 256,84 effort: 'low',85 cost: { endpoint: 'classify', ...(opts.cost ?? {}) },86 });87 if (res.data.categorySlug && !CATEGORIES.some((c) => c.slug === res.data.categorySlug)) res.data.categorySlug = null;88 return res;89}90