import { z } from "zod"; export const reasoningEffortSchema = z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]); export const generationSettingsSchema = z .object({ temperature: z.number().min(0).max(2).optional(), topP: z.number().min(0).max(1).optional(), topK: z.number().int().min(1).max(500).optional(), maxTokens: z.number().int().min(1).max(400_000).optional(), stop: z.array(z.string().min(1).max(64)).max(4).optional(), seed: z.number().int().min(0).max(2_147_483_647).optional(), frequencyPenalty: z.number().min(-2).max(2).optional(), presencePenalty: z.number().min(-2).max(2).optional(), reasoningEffort: reasoningEffortSchema.optional(), thinkingBudget: z.number().int().min(0).max(200_000).optional(), includeReasoning: z.boolean().optional(), verbosity: z.enum(["low", "medium", "high"]).optional(), responseFormat: z .object({ type: z.enum(["text", "json", "json_schema"]), schema: z.record(z.string(), z.unknown()).optional(), schemaName: z.string().max(64).optional(), strict: z.boolean().optional(), }) .optional(), toolChoice: z.union([z.enum(["auto", "none", "required"]), z.object({ type: z.literal("tool"), name: z.string() })]).optional(), webSearch: z.boolean().optional(), codeExecution: z.boolean().optional(), /** Built-in tool ids (calculator, clock, random). */ tools: z.array(z.string()).max(10).optional(), }) .strict(); export type GenerationSettingsInput = z.infer; /** Prior turns replayed by the client for temporary (ephemeral) chats, which have no server-side history. */ export const ephemeralHistorySchema = z .array( z.object({ role: z.enum(["user", "assistant"]), content: z.string().max(200_000), }), ) .max(200); export const chatRequestSchema = z.object({ conversationId: z.string().max(64).optional(), modelKey: z.string().min(3).max(120), action: z.enum(["send", "regenerate", "edit", "continue", "retry"]).default("send"), message: z .object({ text: z.string().max(200_000), attachmentIds: z.array(z.string().max(64)).max(10).optional(), }) .optional(), /** For regenerate/edit/continue/retry. */ targetMessageId: z.string().max(64).optional(), systemPrompt: z.string().max(50_000).nullable().optional(), settings: generationSettingsSchema.optional(), /** Client-side id echo so the UI can reconcile optimistic messages. */ clientId: z.string().max(64).optional(), /** * Temporary chat: stream the answer without creating a conversation or message rows. * Usage is still recorded (with a null conversation) so cost tracking stays exact. * Only `action: "send"` without `conversationId` is allowed; prior turns come from `history`. */ ephemeral: z.boolean().optional(), history: ephemeralHistorySchema.optional(), /** Project (workspace) the new conversation belongs to. Ignored for existing conversations. */ projectId: z.string().max(64).nullable().optional(), }); export type ChatRequestInput = z.infer; /** * POST /api/chat/adopt — insert an assistant message that was generated elsewhere (inline * Compare / Arena) into a conversation, switching the conversation to that model. When no * `conversationId` is given, a new conversation is created with `userText` as the first turn. * Content/metrics are copied server-side from `arenaResponseId` when provided. */ export const chatAdoptSchema = z .object({ conversationId: z.string().max(64).optional(), modelKey: z.string().min(3).max(120), arenaResponseId: z.string().max(64).optional(), content: z.string().max(400_000).optional(), userText: z.string().max(200_000).optional(), systemPrompt: z.string().max(50_000).nullable().optional(), settings: generationSettingsSchema.optional(), projectId: z.string().max(64).nullable().optional(), }) .refine((v) => Boolean(v.arenaResponseId || (v.content && v.content.trim())), { message: "content or arenaResponseId is required" }) .refine((v) => Boolean(v.conversationId || (v.userText && v.userText.trim())), { message: "userText is required when creating a conversation" }); export type ChatAdoptInput = z.infer; export const arenaRunSchema = z.object({ prompt: z.string().min(1).max(100_000), systemPrompt: z.string().max(20_000).optional(), modelKeys: z.array(z.string().min(3).max(120)).min(1).max(4), settings: generationSettingsSchema.optional(), attachmentIds: z.array(z.string().max(64)).max(6).optional(), /** Blind Arena: identities hidden (Model A/B/C/D, shuffled) until the user votes or reveals. */ blind: z.boolean().optional(), }); export const arenaStreamSchema = z.object({ sessionId: z.string().max(64), modelKey: z.string().min(3).max(120), }); /** Criterion ids: built-ins or `custom:`. */ export const arenaCriterionSchema = z.string().regex(/^(best|accurate|writing|coding|value|fastest|custom:[a-z0-9][a-z0-9-]{0,31})$/, "Invalid criterion"); export const arenaCategorySchema = z.enum(["coding", "research", "writing", "reasoning", "general"]); export const arenaVoteSchema = z.object({ sessionId: z.string().max(64), responseId: z.string().max(64), criterion: arenaCriterionSchema, category: arenaCategorySchema.optional(), }); export const arenaRetractVoteSchema = z.object({ sessionId: z.string().max(64), criterion: arenaCriterionSchema, });