Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions apps/api/test/archive-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,66 @@ const archiveRun = {
status: "completed",
evaluatorSlug: "exact-text",
evaluatorVersion: "1.0.0",
evaluatorKind: "deterministic",
definitionSha256:
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
rawResultSha256:
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
exactMatch: true,
},
runnerBuild: "build-test",
createdAt: "2026-09-24T08:00:00.000Z",
completedAt: "2026-09-24T08:00:01.000Z",
sealedAt: "2026-09-24T08:00:01.100Z",
config: {
maxOutputTokens: 64,
},
requestBlob: {
sha256:
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
sizeBytes: 123,
mimeType: "application/json",
visibility: "private",
},
responseBlob: {
sha256:
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
sizeBytes: 456,
mimeType: "application/json",
visibility: "private",
},
responseHeadersSha256:
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
usage: {
inputTokens: 4,
outputTokens: 1,
totalTokens: 5,
},
timing: {
durationMs: 1000,
},
evidence: [
{
id: "00000000-0000-4000-8000-000000000026",
level: "E4",
executionPath: "first_party_direct",
collector: "modelapse-smoke",
sourceId: null,
notes: null,
createdAt: "2026-09-24T08:00:01.100Z",
attestation: {
id: "00000000-0000-4000-8000-000000000027",
keyId: "prod-key",
algorithm: "Ed25519",
payloadSha256:
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
signature: "base64-signature",
keyValidFrom: "2026-09-24T00:00:00.000Z",
keyValidTo: null,
createdAt: "2026-09-24T08:00:01.100Z",
},
},
],
} as const;

describe("Archive read API", () => {
Expand Down
92 changes: 92 additions & 0 deletions apps/web/src/modelapse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export interface ArchiveRun {
readonly status: string;
readonly evaluatorSlug: string;
readonly evaluatorVersion: string;
readonly evaluatorKind: string;
readonly definitionSha256: string;
readonly rawResultSha256: string | null;
readonly exactMatch: boolean | null;
} | null;
readonly runnerBuild: string;
Expand All @@ -110,6 +113,53 @@ export interface ArchiveRun {
readonly sealedAt: string | null;
}

export interface ArchiveBlob {
readonly sha256: string;
readonly sizeBytes: number;
readonly mimeType: string;
readonly visibility: string;
}

export interface ArchiveRunEvidence {
readonly id: string;
readonly level: string;
readonly executionPath: string;
readonly collector: string;
readonly sourceId: string | null;
readonly notes: string | null;
readonly createdAt: string;
readonly attestation: {
readonly id: string;
readonly keyId: string;
readonly algorithm: string;
readonly payloadSha256: string;
readonly signature: string;
readonly keyValidFrom: string;
readonly keyValidTo: string | null;
readonly createdAt: string;
} | null;
}

export interface ArchiveRunDetail extends ArchiveRun {
readonly configJson: string | null;
readonly requestBlob: ArchiveBlob | null;
readonly responseBlob: ArchiveBlob | null;
readonly responseHeadersSha256: string | null;
readonly usageJson: string | null;
readonly timingJson: string | null;
readonly evidence: readonly ArchiveRunEvidence[];
}

interface ArchiveRunDetailWire extends ArchiveRun {
readonly config: unknown;
readonly requestBlob: ArchiveBlob | null;
readonly responseBlob: ArchiveBlob | null;
readonly responseHeadersSha256: string | null;
readonly usage: unknown;
readonly timing: unknown;
readonly evidence: readonly ArchiveRunEvidence[];
}

export interface ControlJob {
readonly id: string;
readonly kind: string;
Expand Down Expand Up @@ -148,6 +198,10 @@ interface ReadJobInput extends OperatorInput {
readonly jobId: string;
}

interface ReadArchiveRunInput {
readonly runId: string;
}

interface ApiOptions {
readonly method?: "GET" | "POST";
readonly control?: boolean;
Expand Down Expand Up @@ -195,6 +249,11 @@ function apiErrorMessage(payload: unknown, status: number): string {
return `Modelapse API request failed with HTTP ${status}`;
}

function serializeArchiveMetadata(value: unknown): string | null {
if (value === null || value === undefined) return null;
return JSON.stringify(value, null, 2) ?? null;
}

function controlAuthDiagnostic(payload: unknown): string | null {
if (!isRecord(payload)) return null;

Expand Down Expand Up @@ -320,6 +379,17 @@ function parseReadJobInput(value: unknown): ReadJobInput {
return { operatorToken, jobId };
}

function parseArchiveRunInput(value: unknown): ReadArchiveRunInput {
if (!isRecord(value)) throw new Error("Archive Run request must be an object");

const runId = value.runId;
if (typeof runId !== "string" || !UUID_RE.test(runId)) {
throw new Error("runId must be a UUID");
}

return { runId };
}

function requireOperator(candidate: string): void {
const expected = process.env.MODELAPSE_WEB_OPERATOR_TOKEN;
if (!expected) {
Expand Down Expand Up @@ -361,6 +431,28 @@ export const getWorkbenchSnapshot = createServerFn({ method: "GET" }).handler(
},
);

export const getArchiveRun = createServerFn({ method: "POST" })
.validator(parseArchiveRunInput)
.handler(async ({ data }): Promise<ArchiveRunDetail | null> => {
try {
const result = await requestJson<{ run: ArchiveRunDetailWire }>(
`/v1/archive/runs/${data.runId}`,
);
const { config, usage, timing, ...run } = result.run;
return {
...run,
configJson: serializeArchiveMetadata(config),
usageJson: serializeArchiveMetadata(usage),
timingJson: serializeArchiveMetadata(timing),
};
} catch (error) {
if (error instanceof ApiRequestError && error.status === 404) {
return null;
}
throw error;
}
});

export const getControlCatalog = createServerFn({ method: "POST" })
.validator(parseOperatorInput)
.handler(async ({ data }): Promise<ControlCatalog> => {
Expand Down
24 changes: 21 additions & 3 deletions apps/web/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@
// This file is regenerated by the TanStack Start Vite plugin.

import { Route as rootRouteImport } from "./routes/__root";
import { Route as RunsRunIdRouteImport } from "./routes/runs.$runId";
import { Route as IndexRouteImport } from "./routes/index";

const RunsRunIdRoute = RunsRunIdRouteImport.update({
id: "/runs/$runId",
path: "/runs/$runId",
getParentRoute: () => rootRouteImport,
} as any);

const IndexRoute = IndexRouteImport.update({
id: "/",
path: "/",
Expand All @@ -14,23 +21,26 @@ const IndexRoute = IndexRouteImport.update({

export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
"/runs/$runId": typeof RunsRunIdRoute;
}

export interface FileRoutesByTo {
"/": typeof IndexRoute;
"/runs/$runId": typeof RunsRunIdRoute;
}

export interface FileRoutesById {
__root__: typeof rootRouteImport;
"/": typeof IndexRoute;
"/runs/$runId": typeof RunsRunIdRoute;
}

export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: "/";
fullPaths: "/" | "/runs/$runId";
fileRoutesByTo: FileRoutesByTo;
to: "/";
id: "__root__" | "/";
to: "/" | "/runs/$runId";
id: "__root__" | "/" | "/runs/$runId";
fileRoutesById: FileRoutesById;
}

Expand All @@ -43,11 +53,19 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof IndexRouteImport;
parentRoute: typeof rootRouteImport;
};
"/runs/$runId": {
id: "/runs/$runId";
path: "/runs/$runId";
fullPath: "/runs/$runId";
preLoaderRoute: typeof RunsRunIdRouteImport;
parentRoute: typeof rootRouteImport;
};
}
}

const rootRouteChildren = {
IndexRoute,
RunsRunIdRoute,
};

export const routeTree = rootRouteImport
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,9 @@ function ModelapseHome() {
{activeRun.model.marketingName ?? activeRun.requestedModel}
</span>
<span>{activeRun.test.caseSlug}</span>
<a className="text-link" href={`/runs/${activeRun.id}`}>
View Run →
</a>
</div>
) : null}
</div>
Expand Down Expand Up @@ -430,7 +433,9 @@ function ModelapseHome() {
{filteredRuns.map((run) => (
<article className="archive-row" role="row" key={run.id}>
<span>
<strong>{run.id.slice(0, 8)}</strong>
<a className="archive-run-link" href={`/runs/${run.id}`}>
{run.id.slice(0, 8)} →
</a>
<small>{run.provider.slug}</small>
</span>
<span>
Expand Down
Loading
Loading