import type { ClassifierAnswer, ClassifierApi, ClassifierContext, ClassifierModel, ClassifierOptions, ClassifierResult, } from "../types.ts"; import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts"; import { isRecord, parseClassifierUsage, postClassifierRequest, requiredNumber } from "./classifier-shared.ts"; /** TypeSafe System One request body without the transport-specific envelope. */ export interface SystemOneWireRequest { state: ClassifierContext["state"]; questions: Record; } /** Differences between services that serve System One models. */ export interface SystemOneTransport { /** Classifier API implemented by this transport. */ api: ClassifierApi; /** Service name used in error messages. */ label: string; /** Absolute request URL. */ url(model: ClassifierModel): URL; /** Wraps the System One request in the service's request envelope. */ payload(model: ClassifierModel, request: SystemOneWireRequest): unknown; /** Extracts the System One output (`{ answers, usage }`) from the service's response envelope. */ output(body: unknown): Record; } function probabilities(label: string, value: unknown, id: string): Record { if (!isRecord(value)) throw new Error(`${label} returned invalid probabilities for ${id}`); return Object.fromEntries( Object.entries(value).map(([key, probability]) => [ key, requiredNumber(label, probability, `probability for ${id}.${key}`), ]), ); } function parseAnswers(label: string, value: unknown, context: ClassifierContext): Record { if (!isRecord(value)) throw new Error(`${label} returned an unexpected response`); const answers: Array<[string, ClassifierAnswer]> = []; for (const [id, question] of Object.entries(context.questions)) { const answer = value[id]; if (!isRecord(answer)) throw new Error(`${label} did not return an answer for ${id}`); if (question.type === "choice") { if (answer.type !== "choice" || typeof answer.choice !== "string") { throw new Error(`${label} did not return a choice answer for ${id}`); } answers.push([ id, { type: "choice", choice: answer.choice, probabilities: probabilities(label, answer.probabilities, id), confidence: requiredNumber(label, answer.confidence, `confidence for ${id}`), }, ]); } else if (question.type === "score") { if (answer.type !== "score") throw new Error(`${label} did not return a score answer for ${id}`); answers.push([ id, { type: "score", score: requiredNumber(label, answer.score, `score for ${id}`), confidence: requiredNumber(label, answer.confidence, `confidence for ${id}`), }, ]); } else { if (answer.type !== "noul") throw new Error(`${label} did not return a bool answer for ${id}`); answers.push([ id, { type: "bool", probability: requiredNumber(label, answer.noul, `probability for ${id}`), }, ]); } } return Object.fromEntries(answers); } /** Maps public `bool` questions to TypeSafe's wire-level `noul` type. */ function wireRequest(context: ClassifierContext): SystemOneWireRequest { return { state: context.state, questions: Object.fromEntries( Object.entries(context.questions).map(([id, question]) => [ id, question.type === "bool" ? { ...question, type: "noul" } : question, ]), ), }; } /** Runs one System One classification over the given transport. */ export async function classifySystemOne( transport: SystemOneTransport, model: ClassifierModel, context: ClassifierContext, options: ClassifierOptions | undefined, ): Promise { const output: ClassifierResult = { api: model.api, provider: model.provider, model: model.id, answers: {}, stopReason: "stop", timestamp: Date.now(), }; try { if (model.api !== transport.api) throw new Error(`Unsupported classifier API: ${model.api}`); if (context.images?.length) throw new Error(`${transport.label} does not support image input`); const body = await postClassifierRequest( transport.label, transport.url(model), model, transport.payload(model, wireRequest(context)), options, ); const result = transport.output(body); // Set before parsing answers: a request with malformed answers was still billed. const usage = parseClassifierUsage(result.usage, model); if (usage) output.usage = usage; output.answers = parseAnswers(transport.label, result.answers, context); return output; } catch (error) { output.stopReason = options?.signal?.aborted ? "aborted" : "error"; output.errorMessage = formatProviderError(normalizeProviderError(error), `${transport.label} error`); return output; } }