profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
llm-client.ts 2,292 bytes
1 /**
2 * The narrow LLM contract the orchestrator depends on.
3 *
4 * The orchestrator never imports the Vercel AI SDK, OpenRouter, or `fetch`
5 * directly - it only knows this interface. That keeps the engine pure and
6 * unit-testable: tests inject a deterministic mock (see `mock-client.ts`),
7 * production injects the OpenRouter-backed client (see `lib/openrouter.ts`).
8 */
9 import type { StageType, Usage } from './types';
10
11 export interface LlmMessage {
12 role: 'system' | 'user' | 'assistant';
13 content: string;
14 }
15
16 /** Opaque, provider-ignored hint used for observability and by the mock. */
17 export interface LlmRequestMeta {
18 stage: StageType;
19 round: number;
20 participantId?: string;
21 /** Deterministic seed the mock uses to synthesize reproducible output. */
22 seed?: string;
23 }
24
25 export interface LlmRequest {
26 model: string;
27 messages: LlmMessage[];
28 temperature?: number;
29 maxTokens?: number;
30 /** Ask the provider for JSON output mode when supported. */
31 json?: boolean;
32 signal?: AbortSignal;
33 meta?: LlmRequestMeta;
34 }
35
36 export interface LlmResult {
37 text: string;
38 usage: Usage;
39 model: string;
40 latencyMs: number;
41 }
42
43 export interface LlmStreamHandle {
44 /** Text deltas as they arrive. Consume fully before awaiting `result`. */
45 stream: AsyncIterable<string>;
46 /** Resolves once the stream is exhausted, with final text + usage. */
47 result: Promise<LlmResult>;
48 }
49
50 export interface LlmClient {
51 /** One-shot completion (used for critique / revision / convergence / synthesis). */
52 complete(req: LlmRequest): Promise<LlmResult>;
53 /** Streaming completion (used for the answer + revision stages the UI watches). */
54 streamComplete(req: LlmRequest): Promise<LlmStreamHandle>;
55 }
56
57 /**
58 * Thrown by a client when a call fails in a way the orchestrator should treat
59 * as a per-model failure (timeout, HTTP error, refusal). Carries whether the
60 * error is retryable so the client's own backoff logic can decide.
61 */
62 export class LlmError extends Error {
63 readonly status?: number;
64 readonly retryable: boolean;
65 constructor(message: string, opts?: { status?: number; retryable?: boolean; cause?: unknown }) {
66 super(message, { cause: opts?.cause });
67 this.name = 'LlmError';
68 this.status = opts?.status;
69 this.retryable = opts?.retryable ?? false;
70 }
71 }
72