profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
schemas.ts 4,141 bytes
1 /**
2 * Runtime schemas for everything that crosses a trust boundary:
3 * - the structured JSON we force models to return (critique/revision/etc.)
4 * - the debate configuration accepted from API clients
5 *
6 * Model output is untrusted input. Every model response that is supposed to be
7 * JSON is parsed defensively and validated against these schemas, with one
8 * repair attempt on failure (see `structured.ts`).
9 */
10 import { z } from 'zod';
11
12 // ---------------------------------------------------------------------------
13 // Structured model outputs
14 // ---------------------------------------------------------------------------
15
16 /** One reviewer's assessment of one anonymized peer answer. */
17 export const peerReviewSchema = z.object({
18 label: z.string().min(1).max(4),
19 weaknesses: z.array(z.string()).default([]),
20 strengths: z.array(z.string()).default([]),
21 score: z.coerce.number().min(1).max(10),
22 justification: z.string().default(''),
23 /** The reviewer's prediction of the other reviewers' average score (1-10). */
24 predictedPeerMean: z.coerce.number().min(1).max(10).optional(),
25 /** The reviewer's guess at which model family wrote the answer (anonymity audit). */
26 authorGuess: z
27 .object({
28 family: z.string().min(1).max(40),
29 confidence: z.coerce.number().min(0).max(1),
30 })
31 .optional(),
32 });
33
34 export const critiqueOutputSchema = z.object({
35 reviews: z.array(peerReviewSchema).min(1),
36 });
37 export type CritiqueOutput = z.infer<typeof critiqueOutputSchema>;
38
39 export const revisionOutputSchema = z.object({
40 answer: z.string().min(1),
41 changelog: z.object({
42 changed: z.boolean(),
43 summary: z.string().default(''),
44 bullets: z.array(z.string()).default([]),
45 }),
46 });
47 export type RevisionOutput = z.infer<typeof revisionOutputSchema>;
48
49 export const convergenceOutputSchema = z.object({
50 score: z.coerce.number().min(0).max(100),
51 disagreements: z
52 .array(
53 z.object({
54 topic: z.string(),
55 summary: z.string().default(''),
56 positions: z
57 .array(z.object({ label: z.string(), stance: z.string() }))
58 .default([]),
59 }),
60 )
61 .default([]),
62 });
63 export type ConvergenceOutput = z.infer<typeof convergenceOutputSchema>;
64
65 export const provenanceOutputSchema = z.object({
66 claims: z
67 .array(
68 z.object({
69 text: z.string().min(1),
70 supportedBy: z.array(z.string()).default([]),
71 contestedBy: z.array(z.string()).default([]),
72 }),
73 )
74 .min(1),
75 });
76 export type ProvenanceOutput = z.infer<typeof provenanceOutputSchema>;
77
78 export const synthesisOutputSchema = z.object({
79 finalAnswer: z.string().min(1),
80 dissent: z
81 .array(
82 z.object({
83 topic: z.string(),
84 positions: z
85 .array(z.object({ label: z.string(), position: z.string() }))
86 .default([]),
87 }),
88 )
89 .default([]),
90 });
91 export type SynthesisOutput = z.infer<typeof synthesisOutputSchema>;
92
93 // ---------------------------------------------------------------------------
94 // Debate configuration (API input)
95 // ---------------------------------------------------------------------------
96
97 /** OpenRouter model slugs look like "vendor/model[:variant]". */
98 export const modelSlugSchema = z
99 .string()
100 .min(1)
101 .max(128)
102 .regex(/^[\w.-]+\/[\w.:-]+$/, 'Expected an OpenRouter model slug like "vendor/model"');
103
104 export const debateConfigInputSchema = z
105 .object({
106 question: z.string().trim().min(3).max(8000),
107 models: z.array(modelSlugSchema).min(3).max(6),
108 chairmanModel: modelSlugSchema,
109 convergenceModel: modelSlugSchema.optional(),
110 maxRounds: z.number().int().min(1).max(5).default(3),
111 convergenceThreshold: z.number().int().min(0).max(100).default(85),
112 temperature: z.number().min(0).max(2).default(0.7),
113 perModelTimeoutMs: z.number().int().min(5_000).max(600_000).default(90_000),
114 maxCostUsd: z.number().positive().max(1_000).optional(),
115 })
116 .refine((c) => new Set(c.models).size === c.models.length, {
117 message: 'Council models must be unique',
118 path: ['models'],
119 });
120
121 export type DebateConfigInput = z.infer<typeof debateConfigInputSchema>;
122