62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
|
|
export type ModelAccessSurface = "pi" | "openrouter" | "openai" | "chatgpt-cyber" | "anthropic-fable" | "anthropic-mythos" | "sakana-fugu" | "hermes-moa" | "other";
|
|
export type ModelAccessStatus = "available" | "blocked" | "missing" | "unknown";
|
|
|
|
export interface ModelAccessProbe {
|
|
surface: ModelAccessSurface;
|
|
command: string;
|
|
status: ModelAccessStatus;
|
|
evidence: string;
|
|
model?: string;
|
|
}
|
|
|
|
export interface ModelAccessReceiptOptions {
|
|
probes: ModelAccessProbe[];
|
|
output?: string;
|
|
freshnessHours?: number;
|
|
now?: Date;
|
|
}
|
|
|
|
export interface ModelAccessReceipt {
|
|
schema: "fable.model_access.receipt.v1";
|
|
createdAt: string;
|
|
probes: ModelAccessProbe[];
|
|
summary: Record<ModelAccessStatus, number>;
|
|
freshness: { hours: number; expiresAt: string };
|
|
decision: "verified" | "partial" | "blocked";
|
|
claims: string[];
|
|
reasons: string[];
|
|
}
|
|
|
|
export function createModelAccessReceipt(opts: ModelAccessReceiptOptions): ModelAccessReceipt {
|
|
const now = opts.now ?? new Date();
|
|
const freshnessHours = opts.freshnessHours ?? 24;
|
|
const summary = opts.probes.reduce(
|
|
(acc, probe) => ({ ...acc, [probe.status]: acc[probe.status] + 1 }),
|
|
{ available: 0, blocked: 0, missing: 0, unknown: 0 },
|
|
);
|
|
const reasons: string[] = [];
|
|
if (opts.probes.length === 0) reasons.push("no model access probes supplied");
|
|
if (opts.probes.some((probe) => probe.surface === "chatgpt-cyber" && probe.status !== "available")) reasons.push("ChatGPT Cyber access not verified");
|
|
if (summary.unknown > 0) reasons.push("one or more model surfaces are unknown");
|
|
|
|
return {
|
|
schema: "fable.model_access.receipt.v1",
|
|
createdAt: now.toISOString(),
|
|
probes: opts.probes,
|
|
summary,
|
|
freshness: { hours: freshnessHours, expiresAt: new Date(now.getTime() + freshnessHours * 60 * 60 * 1000).toISOString() },
|
|
decision: opts.probes.length === 0 || summary.available === 0 ? "blocked" : reasons.length ? "partial" : "verified",
|
|
claims: opts.probes.filter((probe) => probe.status === "available").map((probe) => `${probe.surface}${probe.model ? `:${probe.model}` : ""} available`),
|
|
reasons,
|
|
};
|
|
}
|
|
|
|
export function writeModelAccessReceipt(file: string, receipt: ModelAccessReceipt): string {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
return file;
|
|
}
|