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