/** * @factory/ai — provider adapters. * * - `OpenAIAdapter` / `OssAdapter` / `CustomAdapter` — one OpenAI-compatible * adapter (baseUrl + optional Bearer key; OSS dimension discovered at * first use) * - `AnthropicAdapter` — claude family; **no embed** (gateway routes embed * to the fallback provider) * - `GoogleAdapter` — gemini family (`:streamGenerateContent?alt=sse`) * - `MockProvider` — deterministic, dependency-free, for the CI capability * suite (TASK-0121: "identical assertion set runs against the mock * provider always, and against the real provider when AI_REAL_TEST=1") */ export { AnthropicAdapter, type AnthropicOptions } from "./anthropic.js"; export { GoogleAdapter, type GoogleOptions } from "./google.js"; export { CustomAdapter, OpenAIAdapter, OpenAICompatibleAdapter, OssAdapter, type OpenAICompatibleOptions, } from "./openai-compatible.js"; export { abortError }; import { abortError } from "./http.js"; import { ProviderHttpError, httpJson, isRetryableStatus, sseData, type HttpOptions, } from "./http.js"; import { mapColumnsToProperties, parseMarkdownTable } from "../schemas/database.js"; /** * A deterministic in-process provider for unit tests and the CI capability * suite: echoes structured responses, honors the chat chunk protocol * (text deltas → tool calls → final usage), and counts "tokens" as * characters/4 so usage assertions are exact. */ export class MockProvider { readonly id: "openai" | "anthropic" | "google" | "oss" | "custom"; /** When > 0, `chat` throws a retryable HTTP `failStatus` for the next `failTimes` calls. */ failTimes = 0; /** HTTP status the failures carry (default 429; use 500 for the 5xx path). */ failStatus = 429; calls = 0; /** * TASK-0122: ms to wait BETWEEN deltas (0 = no pacing). Tests that assert * mid-stream cancellation set this so the consumer's abort lands between * deltas; the wait races `req.signal` and aborts throw `AbortError`. */ deltaDelayMs = 0; /** * TASK-0122 (E2E): when > 0, `chat` emits exactly this many deltas and * then throws a retryable `failStatus` — a deterministic MID-stream * provider outage. The gateway never re-streams after partial output * (would duplicate content), so the consumer sees the error in-band: * exactly ONE user request fails, the next (the UI's Retry button) is a * fresh call and succeeds. 0 = disabled (the default). */ failAfterDeltas = 0; /** Embedding dimension the mock reports. */ readonly dimension: number; private callsSeen: { model: string }[] = []; constructor(id: "openai" | "anthropic" | "google" | "oss" | "custom" = "openai", dimension = 8) { this.id = id; this.dimension = dimension; } async models(): Promise< Array<{ id: string; provider: "openai" | "anthropic" | "google" | "oss" | "custom"; kind: "chat" | "embed" | "chat+embed"; contextWindow: number; maxOutput: number; toolCalls: boolean; vision: boolean; pricePer1kIn: number; pricePer1kOut: number; dimension: number }> > { return [ { id: "mock-chat", provider: this.id, kind: "chat", contextWindow: 32_000, maxOutput: 4_096, toolCalls: true, vision: false, pricePer1kIn: 0, pricePer1kOut: 0, dimension: 0, }, ]; } async *chat(req: { model: string; messages: Array<{ role: string; content: unknown }>; tools?: Array<{ name: string; description: string; parameters: Record }>; maxTokens?: number; metadata?: Record; signal?: AbortSignal; }): AsyncIterable<{ delta?: string; toolCallDelta?: { id: string; name: string; arguments: Record }; usage?: { in: number; out: number; total: number }; finishReason?: "stop" | "tool_calls" | "length" | "error"; done: boolean; }> { this.calls += 1; this.callsSeen.push({ model: req.model }); if (this.failTimes > 0) { this.failTimes -= 1; const { ProviderHttpError } = await import("./http.js"); throw new ProviderHttpError(this.failStatus, "mock provider failure", this.failStatus === 429 || this.failStatus >= 500); } // TASK-0123: deterministic WRITING mode. The app layer opts in via // `metadata.mode = "writing"` (+ `action`). The "model" parses the // `[BLOCK ]` markers the prompt builder emits (so the unit/E2E path // exercises the REAL prompt → provider contract, not a side channel) and // answers with ONE `toolCallDelta` carrying the `generateBlocks` result: // a deterministic per-block transform of the block's own text. if (req.metadata?.mode === "writing") { yield* this.writingChat(req); return; } // TASK-0124: deterministic DATABASE-GENERATION mode. The app layer opts in // via `metadata.mode = "database"` (+ `action` ∈ generate|extract|formula, // and, for generate, `prompt`; for extract, `columns`; for formula, // `properties`). The "model" parses the markers the prompt builder emits // and answers with ONE `toolCallDelta` carrying a VALID, deterministic // tool call — so the unit/E2E path exercises the REAL prompt → provider // → server-validation contract. A `metadata.hostile` seam makes it emit a // deliberately INVALID call (to exercise the retry-once → 422 path). if (req.metadata?.mode === "database") { yield* this.databaseChat(req); return; } // TASK-0125: deterministic RAG mode. The app layer opts in via // `metadata.mode = "rag"` and puts the retrieved, numbered context in the // user message as `[n] Source: