/** * AI Provider Abstraction * * Pluggable providers for AI enrichment/compression operations. * Supports Claude CLI (default), OpenAI-compatible APIs, and Google Gemini. * * @module @agentkits/memory/hooks/ai-provider */ import { execFileSync } from 'node:child_process'; // ===== Types ===== /** * AI provider configuration. * Stored in `.claude/memory/settings.json` under the `aiProvider` key. */ export interface AIProviderConfig { /** Provider type */ provider: 'claude-cli' | 'openai' | 'gemini'; /** API key (for HTTP providers; omit for claude-cli) */ apiKey?: string; /** Base URL for OpenAI-compatible API (default: https://api.openai.com/v1) */ baseUrl?: string; /** Model name (default varies by provider) */ model?: string; } /** Default provider configuration */ export const DEFAULT_AI_PROVIDER_CONFIG: AIProviderConfig = { provider: 'claude-cli', }; /** * Provider function contract — takes prompt + system prompt + timeout, * returns raw text response or null on failure. */ export type AIProviderFn = ( prompt: string, systemPrompt: string, timeoutMs: number ) => Promise; /** Synchronous availability check (best-effort). */ export type AIProviderAvailableCheck = () => boolean; /** Resolved provider with run function and availability check. */ export interface ResolvedProvider { run: AIProviderFn; isAvailable: AIProviderAvailableCheck; name: string; } // ===== Provider: Claude CLI ===== /** * Create a provider that uses `claude --print` CLI. * Wraps the existing execFileSync-based approach. */ export function createClaudeCliProvider(model: string): ResolvedProvider { let cliAvailable: boolean | null = null; const isAvailable: AIProviderAvailableCheck = () => { if (cliAvailable !== null) return cliAvailable; try { execFileSync('claude', ['--version'], { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'ignore'], }); cliAvailable = true; } catch { cliAvailable = false; } return cliAvailable; }; const run: AIProviderFn = async (prompt, systemPrompt, timeoutMs) => { try { const result = execFileSync('claude', [ '--print', '--model', model, '--system-prompt', systemPrompt, '--max-turns', '1', '--no-input', '-p', prompt, ], { encoding: 'utf-8', timeout: timeoutMs, stdio: ['pipe', 'pipe', 'ignore'], }); return result.trim() || null; } catch { return null; } }; return { run, isAvailable, name: 'claude-cli' }; } // ===== Provider: OpenAI-Compatible ===== /** * Create a provider that calls any OpenAI-compatible chat completions API. * Covers: OpenRouter, GLM/ZhipuAI, Ollama, LM Studio, vLLM, Together.ai, etc. */ export function createOpenAIProvider( apiKey: string, baseUrl: string, model: string ): ResolvedProvider { const isAvailable: AIProviderAvailableCheck = () => !!apiKey; const run: AIProviderFn = async (prompt, systemPrompt, timeoutMs) => { try { const url = `${baseUrl.replace(/\/$/, '')}/chat/completions`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt }, ], temperature: 0.3, max_tokens: 1024, }), signal: AbortSignal.timeout(timeoutMs), }); if (!response.ok) return null; const data = await response.json() as { choices?: Array<{ message?: { content?: string } }>; }; return data.choices?.[0]?.message?.content?.trim() || null; } catch { return null; } }; return { run, isAvailable, name: 'openai' }; } // ===== Provider: Google Gemini ===== /** * Create a provider that calls Google's Gemini API. * Uses the generateContent endpoint with system_instruction support. */ export function createGeminiProvider( apiKey: string, model: string ): ResolvedProvider { const isAvailable: AIProviderAvailableCheck = () => !!apiKey; const run: AIProviderFn = async (prompt, systemPrompt, timeoutMs) => { try { const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ system_instruction: { parts: [{ text: systemPrompt }] }, contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: 0.3, maxOutputTokens: 1024, }, }), signal: AbortSignal.timeout(timeoutMs), }); if (!response.ok) return null; const data = await response.json() as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> }; }>; }; return data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || null; } catch { return null; } }; return { run, isAvailable, name: 'gemini' }; } // ===== Provider Resolver ===== /** * Resolve which AI provider to use. * * Resolution order: * 1. Environment variables (AGENTKITS_AI_PROVIDER, etc.) — override everything * 2. Settings config (from settings.json) — persistent user preference * 3. Default: claude-cli */ export function resolveAIProvider(settingsConfig?: AIProviderConfig): ResolvedProvider { // Env vars override settings const envProvider = process.env.AGENTKITS_AI_PROVIDER; const envApiKey = process.env.AGENTKITS_AI_API_KEY; const envBaseUrl = process.env.AGENTKITS_AI_BASE_URL; const envModel = process.env.AGENTKITS_AI_MODEL; // Merge: env > settings > defaults const provider = envProvider || settingsConfig?.provider || 'claude-cli'; const apiKey = envApiKey || settingsConfig?.apiKey || ''; const baseUrl = envBaseUrl || settingsConfig?.baseUrl || 'https://api.openai.com/v1'; switch (provider) { case 'openai': return createOpenAIProvider( apiKey, baseUrl, envModel || settingsConfig?.model || 'gpt-4o-mini' ); case 'gemini': return createGeminiProvider( apiKey, envModel || settingsConfig?.model || 'gemini-2.0-flash' ); case 'claude-cli': default: return createClaudeCliProvider( envModel || settingsConfig?.model || 'haiku' ); } }