/** * Base LLM Adapter * * Abstract base class providing shared functionality for all LLM provider adapters. * Implements retry logic, HoloScript generation prompting, and response validation. * * @version 1.0.0 */ import type { ILLMProvider, LLMCompletionRequest, LLMCompletionResponse, LLMRequestOptions, LLMStreamChunk, LLMFileMetadata, LLMFileUploadRequest, HoloScriptGenerationRequest, HoloScriptGenerationResponse, LLMProviderName, LLMProviderConfig, TokenUsage, Capabilities } from './types'; import type { RealtimeSessionConfig, RealtimeSession } from './realtime'; /** * Extract unique @trait references from a HoloScript code snippet. * Exported for direct testing — was inline-called from validateAndTrack * below. 2026-04-23: exported so provider.test.ts can assert its contract * without the class-method round-trip that broke when it moved out of * BaseLLMAdapter. */ export declare function extractTraits(code: string): string[]; export declare abstract class BaseLLMAdapter implements ILLMProvider { abstract readonly name: LLMProviderName; abstract readonly models: readonly string[]; abstract readonly defaultHoloScriptModel: string; /** * Capability manifest. Conservative default (DEFAULT_CAPABILITIES) so * existing adapters compile without change; each adapter overrides * with its actual declarations to participate in capability-aware * routing. See `Capabilities` in types.ts for the full field set. */ readonly capabilities: Capabilities; protected readonly config: Required; constructor(config: LLMProviderConfig); protected abstract getDefaultModel(): string; abstract complete(request: LLMCompletionRequest, model?: string, options?: LLMRequestOptions): Promise; uploadFile(_request: LLMFileUploadRequest): Promise; /** * Default `openRealtimeSession` — providers that don't support the realtime * voice transport axis inherit this explicit unsupported-provider throw * (mirror of `uploadFile`). Only adapters whose manifest declares * `capabilities.realtimeVoice === true` (e.g. OpenAIRealtimeAdapter) override * it. Realtime is a SEPARATE transport from complete()/streamCompletion(). */ openRealtimeSession(_config: RealtimeSessionConfig): Promise; /** * Default `streamCompletion` implementation: call `complete()`, then yield * the full response as a synthesized batch of stream chunks. * * Adapters that support NATIVE streaming (Anthropic, Ollama, OpenAI) * override this with a real translation of their provider's stream events * to `LLMStreamChunk`. Adapters that don't (Mock, BitNet, Gemini) inherit * this default — callers get the same chunk shape, just batched at the end * instead of token-by-token. * * Synthesis order: text chunks first (one `text_delta` carrying the full * concatenated text), then tool-use chunks (one `tool_use_start` + * `tool_use_end` per tool — no `tool_use_input_delta` since the input is * already fully parsed), finally `message_stop`. This preserves the * type-level invariant that `tool_use_end` carries fully-parsed input. */ streamCompletion(request: LLMCompletionRequest, model?: string): AsyncIterable; /** * Generate HoloScript code from a natural language description. * * Transient-error retry lives inside `complete()` (each adapter wraps its * call in `withRetry`). The outer retry loop that previously lived here * was multiplicative with the inner one (4 outer × 4 inner = 16 worst-case * calls on persistent rate-limits) without adding behavior the inner loop * doesn't already cover. */ generateHoloScript(request: HoloScriptGenerationRequest): Promise; /** * Health check - tests connectivity and authentication. */ healthCheck(): Promise<{ ok: boolean; latencyMs: number; error?: string; }>; /** * Probe an OpenAI-compatible local inference server (llama.cpp, Ollama, * LM Studio, bitnet.cpp). Tries `${baseURL}/health` first, falls back to * `${baseURL}/v1/models` if that 404s — different runtimes ship different * health endpoints. 5s timeout per probe. * * `formatError` brands the error string per adapter so the failure message * carries the right setup hint (e.g. local-llm says "Start with: llama-server * -m model.gguf"; bitnet says "Run: python run_inference.py --serve"). * * Local-server adapters (local-llm, bitnet) override the cloud-flavored * `healthCheck()` (which calls `complete()`) and delegate here instead — * pinging a tiny endpoint is much cheaper than a full chat round-trip. */ protected healthCheckLocalServer(baseURL: string, formatError: (baseURL: string, message: string) => string): Promise<{ ok: boolean; latencyMs: number; error?: string; }>; protected buildGenerationPrompt(description: string, format: string, maxObjects?: number): string; /** * Extract HoloScript code from LLM response, stripping markdown fences if present. */ protected extractHoloScriptCode(content: string): string; /** * Basic structural validation of generated HoloScript code. */ protected validateHoloScriptOutput(code: string): { valid: boolean; errors: string[]; }; protected sleep(ms: number): Promise; /** * Run an async operation with retry on transient errors. * * Retries `LLMProviderError` instances where `retryable=true` (e.g. * `LLMRateLimitError`, 5xx via `mapXError`) up to `this.config.maxRetries` * times. `LLMAuthenticationError`, `LLMContextLengthError`, and any * `LLMProviderError` with `retryable=false` (4xx other than 429) throw * immediately. Non-`LLMProviderError` exceptions (network errors, SDK * shapes the adapter didn't classify) get one retry then re-throw — they * could be transient or programmer errors, one retry covers the common * "first connection from cold worker" case without masking real bugs. * * Backoff is `2^attempt * 100ms + jitter`, capped at 8000ms. When the * caught error is `LLMRateLimitError` with `retryAfterMs`, that value is * used instead of the exponential backoff. * * Adapters call this around their SDK invocation: previously the SDK's * own retry was disabled (`maxRetries: 0` with the comment "We handle * retries ourselves") but no handler existed; this is that handler. */ protected withRetry(operation: () => Promise): Promise; /** * Create a zero-usage TokenUsage for mock/error cases. */ protected zeroUsage(): TokenUsage; }