chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
@@ -0,0 +1,135 @@
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
/** 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<ClassifierApi>): URL;
|
||||
/** Wraps the System One request in the service's request envelope. */
|
||||
payload(model: ClassifierModel<ClassifierApi>, request: SystemOneWireRequest): unknown;
|
||||
/** Extracts the System One output (`{ answers, usage }`) from the service's response envelope. */
|
||||
output(body: unknown): Record<string, unknown>;
|
||||
}
|
||||
|
||||
function probabilities(label: string, value: unknown, id: string): Record<string, number> {
|
||||
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<string, ClassifierAnswer> {
|
||||
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<ClassifierApi>,
|
||||
context: ClassifierContext,
|
||||
options: ClassifierOptions | undefined,
|
||||
): Promise<ClassifierResult> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user