fable-agent/src/fable5/model-access-receipt.ts

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;
}