diff --git a/src/agents/orchestrator.js b/src/agents/orchestrator.js index f4f4df1..c16ec45 100644 --- a/src/agents/orchestrator.js +++ b/src/agents/orchestrator.js @@ -1,5 +1,9 @@ import { config } from '../config.js' import { AGENTS, getAgentById } from './registry.js' +import { getProvider, listProviders } from '../providers/index.js' + +// Legacy imports — kept for backward compatibility +// New code should use getProvider() from the provider layer import { runResearch, runSummary, diff --git a/src/config.js b/src/config.js index 12a1920..abf6ce0 100644 --- a/src/config.js +++ b/src/config.js @@ -49,6 +49,10 @@ export const config = { 100, toNumberOr(process.env.ANTHROPIC_RETRY_BASE_DELAY_MS, 500) ), + + // Provider abstraction for multi-LLM support + provider: (process.env.PROVIDER || 'anthropic').toLowerCase(), + logFormat: (process.env.LOG_FORMAT || 'json').toLowerCase() === 'pretty' ? 'pretty' : 'json', // Rate limiting (defaults are intentionally permissive for demos) rateLimit: { diff --git a/src/providers/anthropic-provider.js b/src/providers/anthropic-provider.js new file mode 100644 index 0000000..d0ffd03 --- /dev/null +++ b/src/providers/anthropic-provider.js @@ -0,0 +1,36 @@ +/** + * Anthropic Provider — default implementation. Behavior unchanged from services.js. + */ +import Anthropic from '@anthropic-ai/sdk' +import { config } from '../config.js' + +export const name = 'anthropic' +// eslint-disable-next-line no-unused-vars +const _anthropicClient = new Anthropic({ apiKey: config.anthropicApiKey }) +let available = true + +export const capabilities = { + research: { model: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' }, + summary: { model: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' }, + analysis: { model: 'claude-sonnet-4-5-20250929', fallback: 'claude-haiku-4-5-20251001', label: 'Claude Sonnet 4.5' }, + code: { model: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' }, +} + +export function setApiKey(newKey) { + _anthropicClient = new Anthropic({ apiKey: newKey }) + config.anthropicApiKey = newKey + available = true +} + +export const isAvailable = () => available + +import { runResearch, runSummary, runAnalysis, runCode, createAnthropicMessage } from '../agents/services.js' +export const research = runResearch +export const summary = runSummary +export const analysis = runAnalysis +export const code = runCode +export const createMessage = createAnthropicMessage + +export function getProvider() { + return { name, capabilities, research, summary, analysis, code, createMessage, setApiKey, isAvailable } +} diff --git a/src/providers/index.js b/src/providers/index.js new file mode 100644 index 0000000..c35474d --- /dev/null +++ b/src/providers/index.js @@ -0,0 +1,45 @@ +/** + * Provider Factory — selects and caches the configured LLM provider. + * Usage: import { getProvider } from './providers/index.js' + */ +import { config } from '../config.js' +import { validateProvider } from './interface.js' +import { getProvider as getAnthropic } from './anthropic-provider.js' +import { getProvider as getOpenAI } from './openai-provider.js' + +const REGISTRY = { anthropic: getAnthropic, openai: getOpenAI } +const NAMES = Object.keys(REGISTRY) +let _cached = null + +export function getProvider() { + if (_cached) return _cached + const name = (config.provider || 'anthropic').toLowerCase() + if (!REGISTRY[name]) { + console.warn(`[provider] Unknown "${name}", using anthropic. Available: ${NAMES.join(', ')}`) + _cached = REGISTRY.anthropic() + return _cached + } + _cached = REGISTRY[name]() + const v = validateProvider(_cached) + if (!v.valid) { + console.error(`[provider] "${name}" incomplete: ${v.missing.join(', ')}. Using anthropic.`) + _cached = REGISTRY.anthropic() + } + return _cached +} + +export function listProviders() { + return NAMES.map((n) => { + const p = REGISTRY[n]() + return { + name: n, + available: p.isAvailable ? p.isAvailable() : true, + capabilities: p.capabilities, + } + }) +} + +export function resetProvider() { + _cached = null +} +export { validateProvider, PROVIDER_MODES } from './interface.js' diff --git a/src/providers/interface.js b/src/providers/interface.js new file mode 100644 index 0000000..3a1de4c --- /dev/null +++ b/src/providers/interface.js @@ -0,0 +1,10 @@ +/** + * Provider Interface — contract for LLM provider implementations. + * Required: name, capabilities, research(), summary(), analysis(), code() + */ +export function validateProvider(provider) { + const required = ['name', 'research', 'summary', 'analysis', 'code', 'capabilities'] + const missing = required.filter((key) => typeof provider[key] === 'undefined') + return { valid: missing.length === 0, missing } +} +export const PROVIDER_MODES = ['research', 'summary', 'analysis', 'code'] diff --git a/src/providers/openai-provider.js b/src/providers/openai-provider.js new file mode 100644 index 0000000..44567d8 --- /dev/null +++ b/src/providers/openai-provider.js @@ -0,0 +1,26 @@ +/** + * OpenAI Provider — PLACEHOLDER. Set PROVIDER=openai to activate when implemented. + */ +export const name = 'openai' +export const capabilities = { + research: { model: 'gpt-4o-mini', label: 'GPT-4o Mini' }, + summary: { model: 'gpt-4o-mini', label: 'GPT-4o Mini' }, + analysis: { model: 'gpt-4o', fallback: 'gpt-4o-mini', label: 'GPT-4o' }, + code: { model: 'gpt-4o-mini', label: 'GPT-4o Mini' }, +} +function notYet(mode) { + return async () => { + throw new Error( + `OpenAI ${mode} not implemented. Use PROVIDER=anthropic.` + ) + } +} +export const research = notYet('research') +export const summary = notYet('summary') +export const analysis = notYet('analysis') +export const code = notYet('code') +export const isAvailable = () => false +export function setApiKey(_k) {} +export function getProvider() { + return { name, capabilities, research, summary, analysis, code, setApiKey, isAvailable } +}