/** * LLM provider abstraction for the Lucidly research agent. * * The `ResearchAgent` (researchAgent.ts) drives a tool-use loop. It needs: * 1. A normalized message format it can append to as the conversation unfolds. * 2. Some `LLMProvider` that knows how to (a) translate that normalized history into the * provider's native wire format, (b) call the API (streaming), and (c) translate the * response back. * * Four providers ship out of the box: * • `AnthropicProvider` — Messages API. Tool-use first-class, normalized blocks map 1:1. * • `GroqProvider` — OpenAI-compatible on Groq Cloud (Llama 3.x). Free tier available. * • `DeepSeekProvider` — OpenAI-compatible on api.deepseek.com (deepseek-chat / -reasoner). * • `OpenAIProvider` — OpenAI-compatible on api.openai.com (gpt-4.1, gpt-5, Codex models). * * The three OpenAI-compatible providers share a common base (`OpenAICompatProvider`) since their * wire format is identical — they only differ in default model + base URL. Adding a fifth * OpenAI-compat provider (Mistral, Together, Bedrock-via-OpenAI-proxy) = ~10 lines of subclass. * * Streaming + resilience: every provider streams SSE, supports `AbortSignal` for user-stop, * auto-retries on HTTP 429 (parses `Retry-After` + "try again in Xs" bodies), and auto-retries * on network-blip mid-stream (up to 2x, ONLY if no content has been emitted — retrying after * partial text would duplicate it in the consumer's UI). * * Why we ship our own adapters instead of LangChain/LiteLLM: 350-line abstraction with tight * control over the wire format. No transitive deps. */ import type { ToolDescriptor } from "./researchTools.js"; export type AgentBlock = { type: "text"; text: string; } | { type: "tool_call"; id: string; name: string; input: Record; } | { type: "tool_result"; toolCallId: string; content: string; isError?: boolean; }; export interface AgentMessage { role: "user" | "assistant"; content: AgentBlock[]; } export type AgentStopReason = "end_turn" | "tool_use" | "max_tokens" | "stop_sequence" | null; export interface LLMCompleteParams { system: string; tools: ToolDescriptor[]; messages: AgentMessage[]; maxTokens: number; /** Called once per streamed text fragment from the model. Optional — non-streaming consumers omit. */ onTextDelta?: (chunk: string) => void; /** Called when the provider returned 429 and we're about to back off. Optional. */ onRateLimit?: (info: { waitSec: number; attempt: number; maxRetries: number; }) => void; /** AbortSignal — when aborted, in-flight fetch is cancelled and a retry loop bails immediately. */ abortSignal?: AbortSignal; } export interface LLMResponse { content: AgentBlock[]; stopReason: AgentStopReason; usage: { input: number; output: number; }; } export interface LLMProvider { readonly name: string; readonly model: string; complete(params: LLMCompleteParams): Promise; } export interface AnthropicProviderConfig { apiKey: string; /** Default `claude-haiku-4-5-20251001`. Override with `claude-sonnet-4-6` for harder composition. */ model?: string; /** Override the API base URL. Use a proxy in production. Default: "https://api.anthropic.com". */ apiBaseUrl?: string; /** Set to true if you're calling this directly from a browser (key will be exposed). */ dangerousDirectBrowserAccess?: boolean; } export declare class AnthropicProvider implements LLMProvider { readonly name = "anthropic"; readonly model: string; private readonly apiKey; private readonly apiBaseUrl; private readonly directBrowser; constructor(cfg: AnthropicProviderConfig); complete(p: LLMCompleteParams): Promise; } export interface OpenAICompatProviderConfig { name: string; apiKey: string; model: string; apiBaseUrl: string; } /** * Base class for any OpenAI-compatible chat-completions provider. Subclasses below (Groq, * DeepSeek, OpenAI) just set name + apiBaseUrl + default model — the wire format is identical * across all three. */ export declare class OpenAICompatProvider implements LLMProvider { readonly name: string; readonly model: string; protected readonly apiKey: string; protected readonly apiBaseUrl: string; constructor(cfg: OpenAICompatProviderConfig); complete(p: LLMCompleteParams): Promise; } export interface GroqProviderConfig { apiKey: string; model?: string; apiBaseUrl?: string; } export declare class GroqProvider extends OpenAICompatProvider { constructor(cfg: GroqProviderConfig); } export interface DeepSeekProviderConfig { apiKey: string; /** Default `deepseek-chat`. Set `deepseek-reasoner` for R1 (reasoning trace currently ignored). */ model?: string; apiBaseUrl?: string; } export declare class DeepSeekProvider extends OpenAICompatProvider { constructor(cfg: DeepSeekProviderConfig); } export interface OpenAIProviderConfig { apiKey: string; /** Default `gpt-4.1`. Set to `gpt-5`, `gpt-5-codex`, `o3`, `o4-mini` etc. if you have access. */ model?: string; apiBaseUrl?: string; } export declare class OpenAIProvider extends OpenAICompatProvider { constructor(cfg: OpenAIProviderConfig); }