orquesta-cli 0.1.16 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -13,6 +13,8 @@ import { initializeOptionalTools } from './tools/registry.js';
13
13
  import { sessionManager } from './core/session/session-manager.js';
14
14
  import { connectWithToken, showConnectionStatus, disconnectFromOrquesta, switchProject } from './setup/first-run-setup.js';
15
15
  import { syncOrquestaConfigs } from './orquesta/config-sync.js';
16
+ import { scanProviders, scanProvider, toEndpointConfig } from './core/config/auto-detect.js';
17
+ import { PROVIDERS } from './core/config/providers.js';
16
18
  const require = createRequire(import.meta.url);
17
19
  const packageJson = require('../package.json');
18
20
  const program = new Command();
@@ -34,6 +36,8 @@ program
34
36
  .option('--status', 'Show Orquesta connection status and exit')
35
37
  .option('--disconnect', 'Disconnect from Orquesta and exit')
36
38
  .option('--sync', 'Sync configurations with Orquesta and exit')
39
+ .option('--scan', 'Scan for available LLM providers (env vars + local ports)')
40
+ .option('--add-provider <providerId>', 'Add a specific provider by ID (e.g., openai, anthropic, ollama)')
37
41
  .action(async (options) => {
38
42
  if (options.eval) {
39
43
  await runEvalMode();
@@ -59,6 +63,85 @@ program
59
63
  process.exit(1);
60
64
  }
61
65
  }
66
+ if (options.scan) {
67
+ const ora = (await import('ora')).default;
68
+ const spinner = ora({ text: chalk.cyan('Scanning for LLM providers...'), color: 'cyan' }).start();
69
+ try {
70
+ const result = await scanProviders();
71
+ spinner.stop();
72
+ if (result.detected.length === 0) {
73
+ console.log(chalk.yellow('\nNo LLM providers detected.'));
74
+ console.log(chalk.dim('Set environment variables (e.g., OPENAI_API_KEY) or start a local provider (Ollama, LM Studio).'));
75
+ console.log(chalk.dim('\nSupported providers:'));
76
+ for (const p of PROVIDERS) {
77
+ const envHint = p.envVars.length > 0 ? chalk.dim(` (${p.envVars[0]})`) : p.isLocal ? chalk.dim(` (port ${p.localPort})`) : '';
78
+ console.log(chalk.white(` ${p.name}${envHint}`));
79
+ }
80
+ }
81
+ else {
82
+ console.log(chalk.green(`\nDetected ${result.detected.length} provider(s):\n`));
83
+ for (const d of result.detected) {
84
+ const modelCount = d.discoveredModels.length;
85
+ console.log(chalk.white(` ${d.provider.name}`));
86
+ console.log(chalk.dim(` Source: ${d.source}`));
87
+ console.log(chalk.dim(` Models: ${modelCount} discovered`));
88
+ if (modelCount > 0) {
89
+ const shown = d.discoveredModels.slice(0, 5);
90
+ for (const m of shown) {
91
+ console.log(chalk.dim(` - ${m.name} (${m.id})`));
92
+ }
93
+ if (modelCount > 5)
94
+ console.log(chalk.dim(` ... and ${modelCount - 5} more`));
95
+ }
96
+ console.log();
97
+ }
98
+ if (result.notFound.length > 0) {
99
+ console.log(chalk.dim(`Not found: ${result.notFound.join(', ')}`));
100
+ }
101
+ console.log(chalk.cyan('\nUse --add-provider <id> to add a detected provider as an endpoint.'));
102
+ }
103
+ }
104
+ catch (error) {
105
+ spinner.fail(chalk.red('Scan failed'));
106
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
107
+ }
108
+ return;
109
+ }
110
+ if (options.addProvider) {
111
+ const ora = (await import('ora')).default;
112
+ const spinner = ora({ text: chalk.cyan(`Detecting ${options.addProvider}...`), color: 'cyan' }).start();
113
+ try {
114
+ const detected = await scanProvider(options.addProvider);
115
+ if (!detected) {
116
+ spinner.fail(chalk.red(`Provider '${options.addProvider}' not found or not available`));
117
+ const provider = PROVIDERS.find(p => p.id === options.addProvider);
118
+ if (provider && provider.envVars.length > 0) {
119
+ console.log(chalk.dim(`Set ${provider.envVars[0]} environment variable and try again.`));
120
+ }
121
+ else if (provider?.isLocal) {
122
+ console.log(chalk.dim(`Start ${provider.name} on port ${provider.localPort} and try again.`));
123
+ }
124
+ process.exit(1);
125
+ }
126
+ const endpointConfig = toEndpointConfig(detected);
127
+ await configManager.addEndpoint(endpointConfig);
128
+ if (!configManager.getCurrentModel() && endpointConfig.models.length > 0) {
129
+ await configManager.setCurrentEndpoint(endpointConfig.id);
130
+ await configManager.setCurrentModel(endpointConfig.models[0].id);
131
+ }
132
+ spinner.succeed(chalk.green(`Added ${detected.provider.name} with ${detected.discoveredModels.length} model(s)`));
133
+ console.log(chalk.dim(` Endpoint: ${endpointConfig.baseUrl}`));
134
+ if (detected.discoveredModels.length > 0) {
135
+ console.log(chalk.dim(` Models: ${detected.discoveredModels.slice(0, 5).map(m => m.id).join(', ')}${detected.discoveredModels.length > 5 ? ` (+${detected.discoveredModels.length - 5} more)` : ''}`));
136
+ }
137
+ }
138
+ catch (error) {
139
+ spinner.fail(chalk.red('Failed to add provider'));
140
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
141
+ process.exit(1);
142
+ }
143
+ return;
144
+ }
62
145
  if (options.sync) {
63
146
  if (!configManager.hasOrquestaConnection()) {
64
147
  console.log(chalk.yellow('Not connected to Orquesta. Use --token to connect first.'));
@@ -196,6 +279,8 @@ program.on('command:*', () => {
196
279
  console.log(chalk.white(' --status Show Orquesta connection status\n'));
197
280
  console.log(chalk.white(' --sync Sync configurations with Orquesta\n'));
198
281
  console.log(chalk.white(' --disconnect Disconnect from Orquesta\n'));
282
+ console.log(chalk.white(' --scan Scan for available LLM providers\n'));
283
+ console.log(chalk.white(' --add-provider <id> Add a provider (e.g., openai, anthropic, ollama)\n'));
199
284
  console.log(chalk.white(' --verbose Enable verbose logging\n'));
200
285
  console.log(chalk.white(' --debug Enable debug logging\n'));
201
286
  console.log(chalk.white('\nUse /help in interactive mode for more help.\n'));
@@ -0,0 +1,17 @@
1
+ import { ProviderDefinition } from './providers.js';
2
+ import { EndpointConfig, ModelInfo } from '../../types/index.js';
3
+ export interface DetectedProvider {
4
+ provider: ProviderDefinition;
5
+ apiKey?: string;
6
+ source: string;
7
+ discoveredModels: ModelInfo[];
8
+ }
9
+ export interface ScanResult {
10
+ detected: DetectedProvider[];
11
+ notFound: string[];
12
+ }
13
+ export declare function scanProviders(): Promise<ScanResult>;
14
+ export declare function scanProvider(providerId: string, apiKey?: string): Promise<DetectedProvider | null>;
15
+ export declare function toEndpointConfig(detected: DetectedProvider): EndpointConfig;
16
+ export declare function fetchModels(provider: ProviderDefinition, apiKey: string): Promise<ModelInfo[]>;
17
+ //# sourceMappingURL=auto-detect.d.ts.map
@@ -0,0 +1,123 @@
1
+ import axios from 'axios';
2
+ import { PROVIDERS, buildAuthHeaders } from './providers.js';
3
+ export async function scanProviders() {
4
+ const detected = [];
5
+ const notFound = [];
6
+ for (const provider of PROVIDERS.filter((p) => !p.isLocal)) {
7
+ const apiKey = findEnvVar(provider.envVars);
8
+ if (apiKey) {
9
+ const models = await fetchModels(provider, apiKey).catch(() => []);
10
+ detected.push({
11
+ provider,
12
+ apiKey,
13
+ source: provider.envVars.find((v) => process.env[v]) || provider.envVars[0] || provider.id,
14
+ discoveredModels: models.length > 0 ? models : fallbackModels(provider),
15
+ });
16
+ }
17
+ else {
18
+ notFound.push(provider.id);
19
+ }
20
+ }
21
+ const localProbes = PROVIDERS.filter((p) => p.isLocal).map(async (provider) => {
22
+ const running = await probeLocal(provider);
23
+ if (running) {
24
+ const models = await fetchModels(provider, '').catch(() => []);
25
+ detected.push({
26
+ provider,
27
+ source: 'local-probe',
28
+ discoveredModels: models.length > 0 ? models : fallbackModels(provider),
29
+ });
30
+ }
31
+ else {
32
+ notFound.push(provider.id);
33
+ }
34
+ });
35
+ await Promise.all(localProbes);
36
+ return { detected, notFound };
37
+ }
38
+ export async function scanProvider(providerId, apiKey) {
39
+ const provider = PROVIDERS.find((p) => p.id === providerId);
40
+ if (!provider)
41
+ return null;
42
+ const key = apiKey || findEnvVar(provider.envVars) || '';
43
+ if (provider.requiresApiKey && !key)
44
+ return null;
45
+ if (provider.isLocal) {
46
+ const running = await probeLocal(provider);
47
+ if (!running)
48
+ return null;
49
+ }
50
+ const models = await fetchModels(provider, key).catch(() => []);
51
+ return {
52
+ provider,
53
+ apiKey: key || undefined,
54
+ source: apiKey ? 'manual' : provider.isLocal ? 'local-probe' : (provider.envVars.find((v) => process.env[v]) || 'manual'),
55
+ discoveredModels: models.length > 0 ? models : fallbackModels(provider),
56
+ };
57
+ }
58
+ export function toEndpointConfig(detected) {
59
+ return {
60
+ id: `ep_${detected.provider.id}_${Date.now()}`,
61
+ name: detected.provider.name,
62
+ baseUrl: detected.provider.baseUrl,
63
+ apiKey: detected.apiKey,
64
+ provider: detected.provider.id,
65
+ models: detected.discoveredModels,
66
+ createdAt: new Date(),
67
+ updatedAt: new Date(),
68
+ };
69
+ }
70
+ function findEnvVar(envVars) {
71
+ for (const name of envVars) {
72
+ const value = process.env[name];
73
+ if (value && value.trim().length > 0)
74
+ return value.trim();
75
+ }
76
+ return undefined;
77
+ }
78
+ async function probeLocal(provider) {
79
+ const url = provider.healthCheckPath || `${provider.baseUrl}/models`;
80
+ try {
81
+ const resp = await axios.get(url, { timeout: 2000 });
82
+ return resp.status >= 200 && resp.status < 400;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ export async function fetchModels(provider, apiKey) {
89
+ if (!provider.modelsEndpoint)
90
+ return [];
91
+ const url = `${provider.baseUrl}${provider.modelsEndpoint}`;
92
+ const headers = buildAuthHeaders(provider, apiKey);
93
+ const resp = await axios.get(url, { headers, timeout: 10000 });
94
+ const data = resp.data;
95
+ const models = data?.data || data?.models || [];
96
+ if (!Array.isArray(models) || models.length === 0)
97
+ return [];
98
+ return models
99
+ .filter((m) => {
100
+ const id = (m.id || m.model || '').toLowerCase();
101
+ if (/embed|tts|whisper|dall-e|moderation|audio/.test(id))
102
+ return false;
103
+ return true;
104
+ })
105
+ .slice(0, 50)
106
+ .map((m) => ({
107
+ id: m.id || m.model || m.name,
108
+ name: m.name || m.display_name || m.id || m.model,
109
+ maxTokens: m.context_length || m.context_window || m.max_tokens || 4096,
110
+ enabled: true,
111
+ healthStatus: 'healthy',
112
+ }));
113
+ }
114
+ function fallbackModels(provider) {
115
+ return provider.knownModels.map((m) => ({
116
+ id: m.id,
117
+ name: m.name,
118
+ maxTokens: m.maxTokens,
119
+ enabled: true,
120
+ healthStatus: 'healthy',
121
+ }));
122
+ }
123
+ //# sourceMappingURL=auto-detect.js.map
@@ -0,0 +1,29 @@
1
+ export type AuthMethod = 'bearer' | 'x-api-key' | 'none';
2
+ export type ModelCapability = 'vision' | 'tools' | 'json_mode' | 'extended_thinking' | 'streaming';
3
+ export interface KnownModel {
4
+ id: string;
5
+ name: string;
6
+ maxTokens: number;
7
+ capabilities?: ModelCapability[];
8
+ }
9
+ export interface ProviderDefinition {
10
+ id: string;
11
+ name: string;
12
+ baseUrl: string;
13
+ authMethod: AuthMethod;
14
+ envVars: string[];
15
+ requiresApiKey: boolean;
16
+ isLocal: boolean;
17
+ localPort?: number;
18
+ modelsEndpoint?: string;
19
+ knownModels: KnownModel[];
20
+ openaiCompatible: boolean;
21
+ extraHeaders?: Record<string, string>;
22
+ healthCheckPath?: string;
23
+ }
24
+ export declare const PROVIDERS: ProviderDefinition[];
25
+ export declare function getProvider(id: string): ProviderDefinition | undefined;
26
+ export declare function detectProviderFromUrl(baseUrl: string): ProviderDefinition | undefined;
27
+ export declare function modelHasCapability(providerId: string, modelId: string, capability: ModelCapability): boolean;
28
+ export declare function buildAuthHeaders(provider: ProviderDefinition, apiKey: string): Record<string, string>;
29
+ //# sourceMappingURL=providers.d.ts.map
@@ -0,0 +1,277 @@
1
+ export const PROVIDERS = [
2
+ {
3
+ id: 'openai',
4
+ name: 'OpenAI',
5
+ baseUrl: 'https://api.openai.com/v1',
6
+ authMethod: 'bearer',
7
+ envVars: ['OPENAI_API_KEY'],
8
+ requiresApiKey: true,
9
+ isLocal: false,
10
+ modelsEndpoint: '/models',
11
+ openaiCompatible: true,
12
+ knownModels: [
13
+ { id: 'gpt-4o', name: 'GPT-4o', maxTokens: 128000, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
14
+ { id: 'gpt-4o-mini', name: 'GPT-4o Mini', maxTokens: 128000, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
15
+ { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', maxTokens: 128000, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
16
+ { id: 'o1', name: 'o1', maxTokens: 200000, capabilities: ['extended_thinking', 'streaming'] },
17
+ { id: 'o1-mini', name: 'o1 Mini', maxTokens: 128000, capabilities: ['extended_thinking', 'streaming'] },
18
+ { id: 'o3-mini', name: 'o3 Mini', maxTokens: 200000, capabilities: ['extended_thinking', 'tools', 'streaming'] },
19
+ ],
20
+ },
21
+ {
22
+ id: 'anthropic',
23
+ name: 'Anthropic',
24
+ baseUrl: 'https://api.anthropic.com/v1',
25
+ authMethod: 'x-api-key',
26
+ envVars: ['ANTHROPIC_API_KEY'],
27
+ requiresApiKey: true,
28
+ isLocal: false,
29
+ modelsEndpoint: '/models',
30
+ openaiCompatible: false,
31
+ extraHeaders: { 'anthropic-version': '2023-06-01' },
32
+ knownModels: [
33
+ { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
34
+ { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
35
+ { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', maxTokens: 200000, capabilities: ['vision', 'tools', 'streaming'] },
36
+ { id: 'claude-sonnet-4-5-20250514', name: 'Claude Sonnet 4.5', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
37
+ ],
38
+ },
39
+ {
40
+ id: 'google',
41
+ name: 'Google Gemini',
42
+ baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
43
+ authMethod: 'bearer',
44
+ envVars: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
45
+ requiresApiKey: true,
46
+ isLocal: false,
47
+ modelsEndpoint: '/models',
48
+ openaiCompatible: true,
49
+ knownModels: [
50
+ { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', maxTokens: 1048576, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
51
+ { id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro', maxTokens: 1048576, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
52
+ { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', maxTokens: 2097152, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
53
+ { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', maxTokens: 1048576, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
54
+ ],
55
+ },
56
+ {
57
+ id: 'mistral',
58
+ name: 'Mistral AI',
59
+ baseUrl: 'https://api.mistral.ai/v1',
60
+ authMethod: 'bearer',
61
+ envVars: ['MISTRAL_API_KEY'],
62
+ requiresApiKey: true,
63
+ isLocal: false,
64
+ modelsEndpoint: '/models',
65
+ openaiCompatible: true,
66
+ knownModels: [
67
+ { id: 'mistral-large-latest', name: 'Mistral Large', maxTokens: 128000, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
68
+ { id: 'mistral-small-latest', name: 'Mistral Small', maxTokens: 128000, capabilities: ['tools', 'json_mode', 'streaming'] },
69
+ { id: 'codestral-latest', name: 'Codestral', maxTokens: 256000, capabilities: ['tools', 'streaming'] },
70
+ { id: 'magistral-small-latest', name: 'Magistral Small', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
71
+ { id: 'magistral-medium-latest', name: 'Magistral Medium', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
72
+ ],
73
+ },
74
+ {
75
+ id: 'groq',
76
+ name: 'Groq',
77
+ baseUrl: 'https://api.groq.com/openai/v1',
78
+ authMethod: 'bearer',
79
+ envVars: ['GROQ_API_KEY'],
80
+ requiresApiKey: true,
81
+ isLocal: false,
82
+ modelsEndpoint: '/models',
83
+ openaiCompatible: true,
84
+ knownModels: [
85
+ { id: 'llama-3.3-70b-versatile', name: 'Llama 3.3 70B', maxTokens: 128000, capabilities: ['tools', 'json_mode', 'streaming'] },
86
+ { id: 'llama-3.1-8b-instant', name: 'Llama 3.1 8B', maxTokens: 128000, capabilities: ['tools', 'json_mode', 'streaming'] },
87
+ { id: 'mixtral-8x7b-32768', name: 'Mixtral 8x7B', maxTokens: 32768, capabilities: ['tools', 'streaming'] },
88
+ { id: 'gemma2-9b-it', name: 'Gemma 2 9B', maxTokens: 8192, capabilities: ['streaming'] },
89
+ ],
90
+ },
91
+ {
92
+ id: 'together',
93
+ name: 'Together AI',
94
+ baseUrl: 'https://api.together.xyz/v1',
95
+ authMethod: 'bearer',
96
+ envVars: ['TOGETHER_API_KEY'],
97
+ requiresApiKey: true,
98
+ isLocal: false,
99
+ modelsEndpoint: '/models',
100
+ openaiCompatible: true,
101
+ knownModels: [
102
+ { id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', name: 'Llama 3.3 70B Turbo', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
103
+ { id: 'Qwen/Qwen2.5-72B-Instruct-Turbo', name: 'Qwen 2.5 72B Turbo', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
104
+ { id: 'deepseek-ai/DeepSeek-V3', name: 'DeepSeek V3', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
105
+ { id: 'mistralai/Mixtral-8x22B-Instruct-v0.1', name: 'Mixtral 8x22B', maxTokens: 65536, capabilities: ['tools', 'streaming'] },
106
+ ],
107
+ },
108
+ {
109
+ id: 'fireworks',
110
+ name: 'Fireworks AI',
111
+ baseUrl: 'https://api.fireworks.ai/inference/v1',
112
+ authMethod: 'bearer',
113
+ envVars: ['FIREWORKS_API_KEY'],
114
+ requiresApiKey: true,
115
+ isLocal: false,
116
+ openaiCompatible: true,
117
+ knownModels: [
118
+ { id: 'accounts/fireworks/models/llama-v3p3-70b-instruct', name: 'Llama 3.3 70B', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
119
+ { id: 'accounts/fireworks/models/deepseek-v3', name: 'DeepSeek V3', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
120
+ { id: 'accounts/fireworks/models/qwen2p5-72b-instruct', name: 'Qwen 2.5 72B', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
121
+ ],
122
+ },
123
+ {
124
+ id: 'deepseek',
125
+ name: 'DeepSeek',
126
+ baseUrl: 'https://api.deepseek.com/v1',
127
+ authMethod: 'bearer',
128
+ envVars: ['DEEPSEEK_API_KEY'],
129
+ requiresApiKey: true,
130
+ isLocal: false,
131
+ modelsEndpoint: '/models',
132
+ openaiCompatible: true,
133
+ knownModels: [
134
+ { id: 'deepseek-chat', name: 'DeepSeek Chat (V3)', maxTokens: 128000, capabilities: ['tools', 'json_mode', 'streaming'] },
135
+ { id: 'deepseek-reasoner', name: 'DeepSeek Reasoner', maxTokens: 64000, capabilities: ['extended_thinking', 'streaming'] },
136
+ ],
137
+ },
138
+ {
139
+ id: 'xai',
140
+ name: 'xAI (Grok)',
141
+ baseUrl: 'https://api.x.ai/v1',
142
+ authMethod: 'bearer',
143
+ envVars: ['XAI_API_KEY'],
144
+ requiresApiKey: true,
145
+ isLocal: false,
146
+ modelsEndpoint: '/models',
147
+ openaiCompatible: true,
148
+ knownModels: [
149
+ { id: 'grok-4', name: 'Grok 4', maxTokens: 131072, capabilities: ['vision', 'tools', 'streaming'] },
150
+ { id: 'grok-3', name: 'Grok 3', maxTokens: 131072, capabilities: ['vision', 'tools', 'streaming'] },
151
+ { id: 'grok-3-mini', name: 'Grok 3 Mini', maxTokens: 131072, capabilities: ['tools', 'streaming'] },
152
+ ],
153
+ },
154
+ {
155
+ id: 'perplexity',
156
+ name: 'Perplexity',
157
+ baseUrl: 'https://api.perplexity.ai',
158
+ authMethod: 'bearer',
159
+ envVars: ['PERPLEXITY_API_KEY'],
160
+ requiresApiKey: true,
161
+ isLocal: false,
162
+ openaiCompatible: true,
163
+ knownModels: [
164
+ { id: 'sonar-pro', name: 'Sonar Pro', maxTokens: 200000, capabilities: ['streaming'] },
165
+ { id: 'sonar', name: 'Sonar', maxTokens: 128000, capabilities: ['streaming'] },
166
+ { id: 'sonar-reasoning-pro', name: 'Sonar Reasoning Pro', maxTokens: 128000, capabilities: ['extended_thinking', 'streaming'] },
167
+ { id: 'sonar-reasoning', name: 'Sonar Reasoning', maxTokens: 128000, capabilities: ['extended_thinking', 'streaming'] },
168
+ ],
169
+ },
170
+ {
171
+ id: 'openrouter',
172
+ name: 'OpenRouter',
173
+ baseUrl: 'https://openrouter.ai/api/v1',
174
+ authMethod: 'bearer',
175
+ envVars: ['OPENROUTER_API_KEY'],
176
+ requiresApiKey: true,
177
+ isLocal: false,
178
+ modelsEndpoint: '/models',
179
+ openaiCompatible: true,
180
+ knownModels: [
181
+ { id: 'anthropic/claude-sonnet-4', name: 'Claude Sonnet 4', maxTokens: 200000, capabilities: ['vision', 'tools', 'streaming'] },
182
+ { id: 'openai/gpt-4o', name: 'GPT-4o', maxTokens: 128000, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
183
+ { id: 'google/gemini-2.0-flash-001', name: 'Gemini 2.0 Flash', maxTokens: 1048576, capabilities: ['vision', 'tools', 'streaming'] },
184
+ { id: 'meta-llama/llama-3.3-70b-instruct', name: 'Llama 3.3 70B', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
185
+ { id: 'deepseek/deepseek-chat', name: 'DeepSeek Chat', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
186
+ ],
187
+ },
188
+ {
189
+ id: 'ollama',
190
+ name: 'Ollama',
191
+ baseUrl: 'http://localhost:11434/v1',
192
+ authMethod: 'none',
193
+ envVars: [],
194
+ requiresApiKey: false,
195
+ isLocal: true,
196
+ localPort: 11434,
197
+ modelsEndpoint: '/models',
198
+ openaiCompatible: true,
199
+ healthCheckPath: 'http://localhost:11434/',
200
+ knownModels: [
201
+ { id: 'llama3.3', name: 'Llama 3.3', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
202
+ { id: 'codellama', name: 'Code Llama', maxTokens: 16384, capabilities: ['streaming'] },
203
+ { id: 'mistral', name: 'Mistral 7B', maxTokens: 32000, capabilities: ['tools', 'streaming'] },
204
+ { id: 'deepseek-coder-v2', name: 'DeepSeek Coder V2', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
205
+ { id: 'qwen2.5-coder', name: 'Qwen 2.5 Coder', maxTokens: 128000, capabilities: ['tools', 'streaming'] },
206
+ ],
207
+ },
208
+ {
209
+ id: 'lmstudio',
210
+ name: 'LM Studio',
211
+ baseUrl: 'http://localhost:1234/v1',
212
+ authMethod: 'none',
213
+ envVars: [],
214
+ requiresApiKey: false,
215
+ isLocal: true,
216
+ localPort: 1234,
217
+ modelsEndpoint: '/models',
218
+ openaiCompatible: true,
219
+ knownModels: [],
220
+ },
221
+ {
222
+ id: 'vllm',
223
+ name: 'vLLM',
224
+ baseUrl: 'http://localhost:8000/v1',
225
+ authMethod: 'none',
226
+ envVars: [],
227
+ requiresApiKey: false,
228
+ isLocal: true,
229
+ localPort: 8000,
230
+ modelsEndpoint: '/models',
231
+ openaiCompatible: true,
232
+ knownModels: [],
233
+ },
234
+ ];
235
+ export function getProvider(id) {
236
+ return PROVIDERS.find((p) => p.id === id);
237
+ }
238
+ export function detectProviderFromUrl(baseUrl) {
239
+ const url = baseUrl.toLowerCase();
240
+ for (const provider of PROVIDERS) {
241
+ const providerHost = new URL(provider.baseUrl).hostname;
242
+ if (url.includes(providerHost))
243
+ return provider;
244
+ }
245
+ for (const provider of PROVIDERS.filter((p) => p.isLocal && p.localPort)) {
246
+ if (url.includes(`:${provider.localPort}`))
247
+ return provider;
248
+ }
249
+ return undefined;
250
+ }
251
+ export function modelHasCapability(providerId, modelId, capability) {
252
+ const provider = getProvider(providerId);
253
+ if (!provider)
254
+ return false;
255
+ const model = provider.knownModels.find((m) => m.id === modelId);
256
+ return model?.capabilities?.includes(capability) ?? false;
257
+ }
258
+ export function buildAuthHeaders(provider, apiKey) {
259
+ const headers = { 'Content-Type': 'application/json' };
260
+ switch (provider.authMethod) {
261
+ case 'bearer':
262
+ if (apiKey)
263
+ headers['Authorization'] = `Bearer ${apiKey}`;
264
+ break;
265
+ case 'x-api-key':
266
+ if (apiKey)
267
+ headers['x-api-key'] = apiKey;
268
+ break;
269
+ case 'none':
270
+ break;
271
+ }
272
+ if (provider.extraHeaders) {
273
+ Object.assign(headers, provider.extraHeaders);
274
+ }
275
+ return headers;
276
+ }
277
+ //# sourceMappingURL=providers.js.map
@@ -1,5 +1,6 @@
1
1
  import axios from 'axios';
2
2
  import { configManager } from '../config/config-manager.js';
3
+ import { getProvider, buildAuthHeaders } from '../config/providers.js';
3
4
  import { NetworkError, APIError, TimeoutError, ConnectionError, } from '../../errors/network.js';
4
5
  import { LLMError, TokenLimitError, RateLimitError, ContextLengthError, } from '../../errors/llm.js';
5
6
  import { logger, isLLMLogEnabled } from '../../utils/logger.js';
@@ -23,12 +24,20 @@ export class LLMClient {
23
24
  this.apiKey = endpoint.apiKey || '';
24
25
  this.model = currentModel.id;
25
26
  this.modelName = currentModel.name;
26
- this.axiosInstance = axios.create({
27
- baseURL: this.baseUrl,
28
- headers: {
27
+ let headers;
28
+ const providerDef = endpoint.provider ? getProvider(endpoint.provider) : undefined;
29
+ if (providerDef) {
30
+ headers = buildAuthHeaders(providerDef, this.apiKey);
31
+ }
32
+ else {
33
+ headers = {
29
34
  'Content-Type': 'application/json',
30
35
  ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
31
- },
36
+ };
37
+ }
38
+ this.axiosInstance = axios.create({
39
+ baseURL: this.baseUrl,
40
+ headers,
32
41
  timeout: 600000,
33
42
  });
34
43
  }
@@ -151,7 +160,14 @@ export class LLMClient {
151
160
  throw new Error('INTERRUPTED');
152
161
  }
153
162
  if (currentAttempt < maxRetries && this.isRetryableError(error)) {
154
- const delay = Math.pow(2, currentAttempt - 1) * 1000;
163
+ let delay = Math.pow(2, currentAttempt - 1) * 1000;
164
+ if (axios.isAxiosError(error) && error.response?.status === 429) {
165
+ const retryAfter = error.response.headers['retry-after'];
166
+ if (retryAfter) {
167
+ const retryAfterMs = (parseInt(retryAfter, 10) || 1) * 1000;
168
+ delay = Math.max(delay, retryAfterMs);
169
+ }
170
+ }
155
171
  logger.debug(`LLM call failed (${currentAttempt}/${maxRetries}), retrying after ${delay}ms...`, {
156
172
  error: error.message,
157
173
  attempt: currentAttempt,
@@ -123,7 +123,12 @@ export async function pushConfigsToOrquesta() {
123
123
  Authorization: `Bearer ${orquestaConfig.token}`,
124
124
  'Content-Type': 'application/json',
125
125
  },
126
- body: JSON.stringify({ endpoints }),
126
+ body: JSON.stringify({
127
+ endpoints: endpoints.map(ep => ({
128
+ ...ep,
129
+ metadata: { provider: ep.provider },
130
+ })),
131
+ }),
127
132
  });
128
133
  if (!response.ok) {
129
134
  const errorData = (await response.json());
@@ -1,7 +1,6 @@
1
1
  export declare class OrquestaConnection {
2
2
  private token;
3
- private supabase;
4
- private channel;
3
+ private socket;
5
4
  private connectionInfo;
6
5
  private connected;
7
6
  private heartbeatInterval;
@@ -22,6 +21,7 @@ export declare class OrquestaConnection {
22
21
  data: Record<string, unknown>;
23
22
  }): Promise<void>;
24
23
  private validateToken;
24
+ private connectSocket;
25
25
  private subscribeToChannel;
26
26
  private startHeartbeat;
27
27
  }
@@ -1,14 +1,10 @@
1
- import { createClient } from '@supabase/supabase-js';
1
+ import { io } from 'socket.io-client';
2
2
  import * as os from 'os';
3
- import WebSocket from 'ws';
4
- if (typeof globalThis.WebSocket === 'undefined') {
5
- globalThis.WebSocket = WebSocket;
6
- }
7
3
  const ORQUESTA_API = process.env['ORQUESTA_API_URL'] || 'https://orquesta.live';
4
+ const ORQUESTA_WS = process.env['ORQUESTA_WS_URL'] || 'wss://ws.orquesta.live';
8
5
  export class OrquestaConnection {
9
6
  token;
10
- supabase = null;
11
- channel = null;
7
+ socket = null;
12
8
  connectionInfo = null;
13
9
  connected = false;
14
10
  heartbeatInterval = null;
@@ -18,7 +14,7 @@ export class OrquestaConnection {
18
14
  async connect() {
19
15
  try {
20
16
  const validation = await this.validateToken();
21
- if (!validation.valid || !validation.supabaseUrl || !validation.supabaseAnonKey) {
17
+ if (!validation.valid) {
22
18
  return {
23
19
  success: false,
24
20
  error: validation.error || 'Invalid response from server'
@@ -28,19 +24,9 @@ export class OrquestaConnection {
28
24
  projectId: validation.projectId,
29
25
  projectName: validation.projectName,
30
26
  tokenName: validation.tokenName,
31
- supabaseUrl: validation.supabaseUrl,
32
- supabaseAnonKey: validation.supabaseAnonKey,
33
27
  channelName: validation.channelName,
34
28
  };
35
- this.supabase = createClient(validation.supabaseUrl, validation.supabaseAnonKey, {
36
- realtime: {
37
- params: {
38
- eventsPerSecond: 50,
39
- },
40
- timeout: 30000,
41
- heartbeatIntervalMs: 15000,
42
- },
43
- });
29
+ await this.connectSocket();
44
30
  await this.subscribeToChannel();
45
31
  this.startHeartbeat();
46
32
  this.connected = true;
@@ -63,9 +49,13 @@ export class OrquestaConnection {
63
49
  clearInterval(this.heartbeatInterval);
64
50
  this.heartbeatInterval = null;
65
51
  }
66
- if (this.channel) {
67
- await this.channel.unsubscribe();
68
- this.channel = null;
52
+ if (this.socket) {
53
+ if (this.connectionInfo) {
54
+ this.socket.emit('unsubscribe', { channel: this.connectionInfo.channelName });
55
+ }
56
+ this.socket.removeAllListeners();
57
+ this.socket.disconnect();
58
+ this.socket = null;
69
59
  }
70
60
  if (this.token) {
71
61
  try {
@@ -91,17 +81,18 @@ export class OrquestaConnection {
91
81
  };
92
82
  }
93
83
  async sendSessionEvent(event) {
94
- if (!this.channel || !this.connected) {
84
+ if (!this.socket || !this.connected || !this.connectionInfo) {
95
85
  console.warn('Not connected to Orquesta - session event not sent');
96
86
  return;
97
87
  }
98
- await this.channel.send({
99
- type: 'broadcast',
88
+ this.socket.emit('broadcast', {
89
+ channel: this.connectionInfo.channelName,
100
90
  event: event.type,
101
91
  payload: {
102
92
  ...event.data,
103
93
  timestamp: Date.now(),
104
94
  },
95
+ self: true,
105
96
  });
106
97
  }
107
98
  async validateToken() {
@@ -123,9 +114,39 @@ export class OrquestaConnection {
123
114
  }
124
115
  return data;
125
116
  }
117
+ connectSocket() {
118
+ return new Promise((resolve, reject) => {
119
+ this.socket = io(ORQUESTA_WS, {
120
+ auth: { agentToken: this.token },
121
+ transports: ['websocket'],
122
+ reconnection: true,
123
+ reconnectionAttempts: Infinity,
124
+ reconnectionDelay: 1000,
125
+ reconnectionDelayMax: 30000,
126
+ });
127
+ const onConnect = () => {
128
+ this.socket?.off('connect_error', onError);
129
+ resolve();
130
+ };
131
+ const onError = (err) => {
132
+ this.socket?.off('connect', onConnect);
133
+ this.socket?.disconnect();
134
+ this.socket = null;
135
+ reject(new Error(`Socket.io connection failed: ${err.message}`));
136
+ };
137
+ this.socket.once('connect', onConnect);
138
+ this.socket.once('connect_error', onError);
139
+ this.socket.on('reconnect', () => {
140
+ if (this.connectionInfo) {
141
+ this.subscribeToChannel().catch(() => {
142
+ });
143
+ }
144
+ });
145
+ });
146
+ }
126
147
  async subscribeToChannel() {
127
- if (!this.supabase || !this.connectionInfo) {
128
- throw new Error('Supabase client not initialized');
148
+ if (!this.socket || !this.connectionInfo) {
149
+ throw new Error('Socket not initialized');
129
150
  }
130
151
  const agentInfo = {
131
152
  hostname: os.hostname(),
@@ -134,28 +155,16 @@ export class OrquestaConnection {
134
155
  type: 'orquesta-cli',
135
156
  workingDirectory: process.cwd(),
136
157
  };
137
- this.channel = this.supabase.channel(this.connectionInfo.channelName, {
138
- config: {
139
- broadcast: { self: true, ack: true },
140
- presence: { key: agentInfo.hostname },
141
- },
142
- });
143
- this.channel.on('broadcast', { event: 'team_message' }, ({ payload }) => {
158
+ this.socket.emit('subscribe', { channel: this.connectionInfo.channelName });
159
+ this.socket.on('team_message', (payload) => {
144
160
  console.log('Team message:', payload);
145
161
  });
146
- return new Promise((resolve, reject) => {
147
- this.channel.subscribe(async (status) => {
148
- if (status === 'SUBSCRIBED') {
149
- await this.channel.track({
150
- ...agentInfo,
151
- connectedAt: new Date().toISOString(),
152
- });
153
- resolve();
154
- }
155
- else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
156
- reject(new Error(`Channel subscription failed: ${status}`));
157
- }
158
- });
162
+ this.socket.emit('presence:join', {
163
+ channel: this.connectionInfo.channelName,
164
+ user: {
165
+ ...agentInfo,
166
+ connectedAt: new Date().toISOString(),
167
+ },
159
168
  });
160
169
  }
161
170
  startHeartbeat() {
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { configManager } from '../core/config/config-manager.js';
4
4
  import { syncOrquestaConfigs, fetchOrquestaProjects } from '../orquesta/config-sync.js';
5
+ import { scanProviders, toEndpointConfig } from '../core/config/auto-detect.js';
5
6
  export function needsFirstRunSetup() {
6
7
  return !configManager.hasEndpoints() && !configManager.hasOrquestaConnection();
7
8
  }
@@ -9,8 +10,39 @@ export async function runFirstRunSetup(token) {
9
10
  if (token) {
10
11
  return await connectWithToken(token);
11
12
  }
13
+ if (!configManager.hasEndpoints()) {
14
+ await autoDetectProviders();
15
+ }
12
16
  return { connected: false, skipped: true };
13
17
  }
18
+ async function autoDetectProviders() {
19
+ const spinner = ora({
20
+ text: chalk.cyan('Scanning for LLM providers...'),
21
+ color: 'cyan',
22
+ }).start();
23
+ try {
24
+ const result = await scanProviders();
25
+ if (result.detected.length === 0) {
26
+ spinner.info(chalk.yellow('No LLM providers detected. Use /config to add endpoints or --scan for details.'));
27
+ return;
28
+ }
29
+ let addedCount = 0;
30
+ for (const detected of result.detected) {
31
+ const endpoint = toEndpointConfig(detected);
32
+ await configManager.addEndpoint(endpoint);
33
+ addedCount++;
34
+ if (addedCount === 1 && endpoint.models.length > 0) {
35
+ await configManager.setCurrentEndpoint(endpoint.id);
36
+ await configManager.setCurrentModel(endpoint.models[0].id);
37
+ }
38
+ }
39
+ const names = result.detected.map(d => d.provider.name).join(', ');
40
+ spinner.succeed(chalk.green(`Auto-detected ${addedCount} provider(s): ${names}`));
41
+ }
42
+ catch {
43
+ spinner.info(chalk.dim('Provider auto-detection skipped (network error).'));
44
+ }
45
+ }
14
46
  export async function connectWithToken(token, projectId) {
15
47
  const spinner = ora({
16
48
  text: chalk.cyan('Validating token...'),
@@ -3,6 +3,7 @@ export interface EndpointConfig {
3
3
  name: string;
4
4
  baseUrl: string;
5
5
  apiKey?: string;
6
+ provider?: string;
6
7
  models: ModelInfo[];
7
8
  healthCheckInterval?: number;
8
9
  priority?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquesta-cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Orquesta CLI - AI-powered coding assistant with team collaboration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -88,7 +88,6 @@
88
88
  },
89
89
  "dependencies": {
90
90
  "@anthropic-ai/sdk": "^0.32.1",
91
- "@supabase/supabase-js": "^2.39.0",
92
91
  "axios": "^1.6.2",
93
92
  "chalk": "^4.1.2",
94
93
  "commander": "^11.1.0",
@@ -99,6 +98,7 @@
99
98
  "inquirer": "^8.2.6",
100
99
  "ora": "^5.4.1",
101
100
  "semver": "^7.7.3",
101
+ "socket.io-client": "^4.8.3",
102
102
  "ws": "^8.18.3"
103
103
  },
104
104
  "devDependencies": {