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
JavaScript
/**
* 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