UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

502 lines 23.7 kB
/** * V3 CLI Providers Command * Manage AI providers, models, and configurations * * Created with ❤️ by ruv.io */ import { output } from '../output.js'; import { configManager } from '../services/config-file-manager.js'; const PROVIDER_CATALOG = [ { name: 'Anthropic', type: 'LLM', models: 'claude-3.5-sonnet, opus', envVar: 'ANTHROPIC_API_KEY', configName: 'anthropic' }, { name: 'OpenAI', type: 'LLM', models: 'gpt-4o, gpt-4-turbo', envVar: 'OPENAI_API_KEY', configName: 'openai' }, { name: 'OpenAI', type: 'Embedding', models: 'text-embedding-3-small/large', envVar: 'OPENAI_API_KEY', configName: 'openai' }, { name: 'Google', type: 'LLM', models: 'gemini-pro, gemini-ultra', envVar: 'GOOGLE_API_KEY', configName: 'google' }, // #1725: Ollama Cloud — Tier-2 default per ADR-026 (~$100/mo flat-rate alternative // to per-token pricing). OpenAI-compat API at https://ollama.com/v1/chat/completions. { name: 'Ollama', type: 'LLM', models: 'gpt-oss:120b-cloud, llama3:70b-cloud, qwen2.5-coder:32b-cloud', envVar: 'OLLAMA_API_KEY', configName: 'ollama' }, { name: 'Transformers.js', type: 'Embedding', models: 'Xenova/all-MiniLM-L6-v2' }, { name: 'Agentic Flow', type: 'Embedding', models: 'ONNX optimized' }, { name: 'Mock', type: 'All', models: 'mock-*' }, ]; /** * Resolve the API key for a provider by checking the config file first, * then falling back to well-known environment variables. */ function resolveApiKey(providerName, configuredProviders) { // Check config file entry const entry = configuredProviders.find((p) => typeof p.name === 'string' && p.name.toLowerCase() === providerName.toLowerCase()); if (entry?.apiKey && typeof entry.apiKey === 'string') { return entry.apiKey; } // Check environment variable const envMapping = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', google: 'GOOGLE_API_KEY', ollama: 'OLLAMA_API_KEY', // #1725 — Tier-2 routing }; const envVar = envMapping[providerName.toLowerCase()]; if (envVar && process.env[envVar]) { return process.env[envVar]; } return undefined; } /** * Make a lightweight HTTP request to verify provider API key validity. * Uses a 5-second timeout. Returns { ok, reason }. */ async function testProviderConnectivity(providerName, apiKey) { const endpoints = { anthropic: { url: 'https://api.anthropic.com/v1/models', headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', }, }, openai: { url: 'https://api.openai.com/v1/models', headers: { 'Authorization': `Bearer ${apiKey}`, }, }, google: { url: `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, headers: {}, }, // #1725 — Ollama Cloud uses an OpenAI-compatible /v1 surface. ollama: { url: 'https://ollama.com/api/tags', headers: { 'Authorization': `Bearer ${apiKey}`, }, }, }; const endpointConfig = endpoints[providerName.toLowerCase()]; if (!endpointConfig) { return { ok: false, reason: 'No test endpoint available for this provider' }; } try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const res = await fetch(endpointConfig.url, { method: 'GET', headers: endpointConfig.headers, signal: controller.signal, }); clearTimeout(timeout); if (res.ok || res.status === 200) { return { ok: true, reason: 'Connected successfully' }; } if (res.status === 401 || res.status === 403) { return { ok: false, reason: `Authentication failed (HTTP ${res.status})` }; } // A non-auth error but the server responded — key format may be fine return { ok: false, reason: `Unexpected response (HTTP ${res.status})` }; } catch (err) { if (err instanceof Error && err.name === 'AbortError') { return { ok: false, reason: 'Connection timed out (5s)' }; } const msg = err instanceof Error ? err.message : String(err); return { ok: false, reason: `Connection failed: ${msg}` }; } } // List subcommand const listCommand = { name: 'list', description: 'List available AI providers and models', options: [ { name: 'type', short: 't', type: 'string', description: 'Filter by type: llm, embedding, image', default: 'all' }, { name: 'active', short: 'a', type: 'boolean', description: 'Show only active providers' }, ], examples: [ { command: 'claude-flow providers list', description: 'List all providers' }, { command: 'claude-flow providers list -t embedding', description: 'List embedding providers' }, ], action: async (ctx) => { const type = ctx.flags.type || 'all'; const activeOnly = ctx.flags.active; // Load user configuration const cwd = process.cwd(); const config = configManager.getConfig(cwd); const agents = (config.agents ?? {}); const configuredProviders = (agents.providers ?? []); // Build table rows from the catalog, enriched with configuration status const rows = []; for (const entry of PROVIDER_CATALOG) { // Apply type filter if (type !== 'all' && entry.type.toLowerCase() !== type.toLowerCase()) { continue; } let status; let keySource = ''; if (entry.configName) { const apiKey = resolveApiKey(entry.configName, configuredProviders); if (apiKey) { // Determine the source for the key const configEntry = configuredProviders.find((p) => typeof p.name === 'string' && p.name.toLowerCase() === entry.configName.toLowerCase()); if (configEntry?.apiKey && typeof configEntry.apiKey === 'string') { keySource = 'config'; } else { keySource = 'env'; } status = output.success(`Configured (${keySource})`); } else { status = output.warning('Not configured'); } } else if (entry.name === 'Mock') { status = output.dim('Dev only'); } else { // Local-only providers (Transformers.js, Agentic Flow) — always available status = output.success('Available (local)'); } if (activeOnly && !status.includes('Configured') && !status.includes('Available')) { continue; } rows.push({ provider: entry.name, type: entry.type, models: entry.models, status, }); } // Also show any providers in config that are not in the static catalog for (const cp of configuredProviders) { const cpName = cp.name || ''; const alreadyListed = PROVIDER_CATALOG.some((e) => e.configName?.toLowerCase() === cpName.toLowerCase() || e.name.toLowerCase() === cpName.toLowerCase()); if (!alreadyListed && cpName) { const hasKey = !!(cp.apiKey || resolveApiKey(cpName, configuredProviders)); rows.push({ provider: cpName, type: cp.type || 'Custom', models: cp.model || output.dim('(not specified)'), status: hasKey ? output.success('Configured (config)') : output.warning('Not configured'), }); } } output.writeln(); output.writeln(output.bold('Providers')); output.writeln(output.dim('─'.repeat(60))); if (rows.length === 0) { output.writeln(output.dim(' No providers match the current filter.')); } else { output.printTable({ columns: [ { key: 'provider', header: 'Provider', width: 18 }, { key: 'type', header: 'Type', width: 12 }, { key: 'models', header: 'Models', width: 25 }, { key: 'status', header: 'Status', width: 20 }, ], data: rows, }); } output.writeln(); output.writeln(output.dim('Tip: Use "providers configure -p <name> -k <key>" to set API keys.')); return { success: true }; }, }; // Configure subcommand const configureCommand = { name: 'configure', description: 'Configure provider settings and API keys', options: [ { name: 'provider', short: 'p', type: 'string', description: 'Provider name', required: true }, { name: 'key', short: 'k', type: 'string', description: 'API key' }, { name: 'model', short: 'm', type: 'string', description: 'Default model' }, { name: 'endpoint', short: 'e', type: 'string', description: 'Custom endpoint URL' }, ], examples: [ { command: 'claude-flow providers configure -p openai -k sk-...', description: 'Set OpenAI key' }, { command: 'claude-flow providers configure -p anthropic -m claude-3.5-sonnet', description: 'Set default model' }, ], action: async (ctx) => { try { const provider = ctx.flags.provider || (ctx.args && ctx.args[0]) || ''; const apiKey = ctx.flags.key; const model = ctx.flags.model; const endpoint = ctx.flags.endpoint; if (!provider) { output.printError('Provider name is required. Use -p <name> or pass as first argument.'); return { success: false, exitCode: 1 }; } const cwd = process.cwd(); const config = configManager.getConfig(cwd); // Ensure agents.providers array exists const agents = (config.agents ?? {}); const providers = (agents.providers ?? []); // Find existing provider entry or create a new one let entry = providers.find((p) => typeof p.name === 'string' && p.name.toLowerCase() === provider.toLowerCase()); if (!entry) { entry = { name: provider, enabled: true }; providers.push(entry); } // Apply supplied settings if (apiKey !== undefined) entry.apiKey = apiKey; if (model !== undefined) entry.model = model; if (endpoint !== undefined) entry.baseUrl = endpoint; agents.providers = providers; configManager.set(cwd, 'agents.providers', providers); output.writeln(); output.writeln(output.bold(`Configured: ${provider}`)); output.writeln(output.dim('─'.repeat(40))); if (apiKey) output.writeln(` API Key : ${apiKey.slice(0, 6)}...${apiKey.slice(-4)}`); if (model) output.writeln(` Model : ${model}`); if (endpoint) output.writeln(` Endpoint: ${endpoint}`); if (!apiKey && !model && !endpoint) { output.writeln(` Provider "${provider}" registered (no settings changed).`); } output.writeln(); output.writeln(output.success(`Provider "${provider}" configuration saved.`)); return { success: true }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); output.printError(`Failed to configure provider: ${msg}`); return { success: false, exitCode: 1 }; } }, }; // Test subcommand const testCommand = { name: 'test', description: 'Test provider connectivity and API access', options: [ { name: 'provider', short: 'p', type: 'string', description: 'Provider to test' }, { name: 'all', short: 'a', type: 'boolean', description: 'Test all configured providers' }, ], examples: [ { command: 'claude-flow providers test -p openai', description: 'Test OpenAI connection' }, { command: 'claude-flow providers test --all', description: 'Test all providers' }, ], action: async (ctx) => { try { const provider = ctx.flags.provider || (ctx.args && ctx.args[0]) || ''; const testAll = ctx.flags.all; output.writeln(); output.writeln(output.bold('Provider Connectivity Test')); output.writeln(output.dim('─'.repeat(50))); const cwd = process.cwd(); const config = configManager.getConfig(cwd); const agents = (config.agents ?? {}); const configuredProviders = (agents.providers ?? []); const knownTargets = [ { name: 'Anthropic', configName: 'anthropic' }, { name: 'OpenAI', configName: 'openai' }, { name: 'Google', configName: 'google' }, ]; // Add Ollama as a special case (endpoint-based, no API key) const ollamaEntry = configuredProviders.find((p) => typeof p.name === 'string' && p.name.toLowerCase() === 'ollama'); let targets; if (testAll || !provider) { targets = [...knownTargets]; } else { const match = knownTargets.find((t) => t.name.toLowerCase() === provider.toLowerCase() || t.configName === provider.toLowerCase()); targets = match ? [match] : [{ name: provider, configName: provider.toLowerCase() }]; } const results = []; // Test API-key-based providers with real connectivity checks for (const target of targets) { const apiKey = resolveApiKey(target.configName, configuredProviders); if (!apiKey) { results.push({ name: target.name, pass: false, reason: 'Not configured (no API key found)' }); continue; } output.writeln(output.dim(` Testing ${target.name}...`)); const result = await testProviderConnectivity(target.name, apiKey); results.push({ name: target.name, pass: result.ok, reason: result.reason }); } // Test Ollama separately (endpoint-based, no API key needed) if (testAll || !provider || provider.toLowerCase() === 'ollama') { const baseUrl = ollamaEntry?.baseUrl || 'http://localhost:11434'; output.writeln(output.dim(` Testing Ollama at ${baseUrl}...`)); try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const res = await fetch(baseUrl, { signal: controller.signal }); clearTimeout(timeout); if (res.ok) { results.push({ name: 'Ollama', pass: true, reason: `Connected at ${baseUrl}` }); } else { results.push({ name: 'Ollama', pass: false, reason: `HTTP ${res.status} from ${baseUrl}` }); } } catch { results.push({ name: 'Ollama', pass: false, reason: `Unreachable at ${baseUrl}` }); } } // Also test any custom providers from config that were not in the known list if (testAll || !provider) { for (const cp of configuredProviders) { const cpName = cp.name || ''; const alreadyTested = results.some((r) => r.name.toLowerCase() === cpName.toLowerCase()); if (alreadyTested || !cpName) continue; const apiKey = resolveApiKey(cpName, configuredProviders); if (!apiKey) { results.push({ name: cpName, pass: false, reason: 'No API key found' }); } else { // For custom providers we can only verify the key exists results.push({ name: cpName, pass: true, reason: 'API key found (no test endpoint available)' }); } } } let anyPassed = false; output.writeln(); for (const r of results) { const icon = r.pass ? output.success('PASS') : output.error('FAIL'); output.writeln(` ${icon} ${r.name}: ${r.reason}`); if (r.pass) anyPassed = true; } output.writeln(); if (results.length === 0) { output.writeln(output.warning('No providers to test. Use "providers configure" to add providers.')); } else if (anyPassed) { output.writeln(output.success(`${results.filter((r) => r.pass).length}/${results.length} provider(s) passed.`)); } else { output.writeln(output.warning('No providers passed connectivity checks.')); } return { success: anyPassed }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); output.printError(`Provider test failed: ${msg}`); return { success: false, exitCode: 1 }; } }, }; // Models subcommand const modelsCommand = { name: 'models', description: 'List and manage available models', options: [ { name: 'provider', short: 'p', type: 'string', description: 'Filter by provider' }, { name: 'capability', short: 'c', type: 'string', description: 'Filter by capability: chat, completion, embedding' }, ], examples: [ { command: 'claude-flow providers models', description: 'List all models' }, { command: 'claude-flow providers models -p anthropic', description: 'List Anthropic models' }, ], action: async (ctx) => { output.writeln(); output.writeln(output.bold('Available Models')); output.writeln(output.dim('─'.repeat(70))); output.printTable({ columns: [ { key: 'model', header: 'Model', width: 28 }, { key: 'provider', header: 'Provider', width: 14 }, { key: 'capability', header: 'Capability', width: 12 }, { key: 'context', header: 'Context', width: 10 }, { key: 'cost', header: 'Cost/1K', width: 12 }, ], data: [ { model: 'claude-3.5-sonnet-20241022', provider: 'Anthropic', capability: 'Chat', context: '200K', cost: '$0.003/$0.015' }, { model: 'claude-3-opus-20240229', provider: 'Anthropic', capability: 'Chat', context: '200K', cost: '$0.015/$0.075' }, { model: 'gpt-4o', provider: 'OpenAI', capability: 'Chat', context: '128K', cost: '$0.005/$0.015' }, { model: 'gpt-4-turbo', provider: 'OpenAI', capability: 'Chat', context: '128K', cost: '$0.01/$0.03' }, { model: 'text-embedding-3-small', provider: 'OpenAI', capability: 'Embedding', context: '8K', cost: '$0.00002' }, { model: 'text-embedding-3-large', provider: 'OpenAI', capability: 'Embedding', context: '8K', cost: '$0.00013' }, { model: 'Xenova/all-MiniLM-L6-v2', provider: 'Transformers', capability: 'Embedding', context: '512', cost: output.success('Free') }, ], }); return { success: true }; }, }; // Usage subcommand const usageCommand = { name: 'usage', description: 'View provider usage and costs', options: [ { name: 'provider', short: 'p', type: 'string', description: 'Filter by provider' }, { name: 'timeframe', short: 't', type: 'string', description: 'Timeframe: 24h, 7d, 30d', default: '7d' }, ], examples: [ { command: 'claude-flow providers usage', description: 'View all usage' }, { command: 'claude-flow providers usage -t 30d', description: 'View 30-day usage' }, ], action: async (ctx) => { const timeframe = ctx.flags.timeframe || '7d'; output.writeln(); output.writeln(output.bold(`Provider Usage (${timeframe})`)); output.writeln(output.dim('─'.repeat(60))); output.printTable({ columns: [ { key: 'provider', header: 'Provider', width: 15 }, { key: 'requests', header: 'Requests', width: 12 }, { key: 'tokens', header: 'Tokens', width: 15 }, { key: 'cost', header: 'Est. Cost', width: 12 }, { key: 'trend', header: 'Trend', width: 12 }, ], data: [ { provider: 'Anthropic', requests: '12,847', tokens: '4.2M', cost: '$12.60', trend: output.warning('↑ 15%') }, { provider: 'OpenAI (LLM)', requests: '3,421', tokens: '1.1M', cost: '$5.50', trend: output.success('↓ 8%') }, { provider: 'OpenAI (Embed)', requests: '89,234', tokens: '12.4M', cost: '$0.25', trend: output.success('↓ 12%') }, { provider: 'Transformers.js', requests: '234,567', tokens: '45.2M', cost: output.success('$0.00'), trend: '→' }, ], }); output.writeln(); output.printBox([ `Total Requests: 340,069`, `Total Tokens: 62.9M`, `Total Cost: $18.35`, ``, `Savings from local embeddings: $890.12`, ].join('\n'), 'Summary'); return { success: true }; }, }; // Main providers command export const providersCommand = { name: 'providers', description: 'Manage AI providers, models, and configurations', subcommands: [listCommand, configureCommand, testCommand, modelsCommand, usageCommand], examples: [ { command: 'claude-flow providers list', description: 'List all providers' }, { command: 'claude-flow providers configure -p openai', description: 'Configure OpenAI' }, { command: 'claude-flow providers test --all', description: 'Test all providers' }, ], action: async () => { output.writeln(); output.writeln(output.bold('RuFlo Provider Management')); output.writeln(output.dim('Multi-provider AI orchestration')); output.writeln(); output.writeln('Subcommands:'); output.printList([ 'list - List available providers and their status', 'configure - Configure provider settings and API keys', 'test - Test provider connectivity', 'models - List and manage available models', 'usage - View usage statistics and costs', ]); output.writeln(); output.writeln('Supported Providers:'); output.printList([ 'Anthropic (Claude models)', 'OpenAI (GPT + embeddings)', 'Transformers.js (local ONNX)', 'Agentic Flow (optimized ONNX with SIMD)', ]); output.writeln(); output.writeln(output.dim('Created with ❤️ by ruv.io')); return { success: true }; }, }; export default providersCommand; //# sourceMappingURL=providers.js.map