hedgequantx 2.7.20 → 2.7.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.7.20",
3
+ "version": "2.7.21",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -10,9 +10,10 @@ const os = require('os');
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
12
 
13
+ const ora = require('ora');
13
14
  const { getLogoWidth, centerText, visibleLength } = require('../ui');
14
15
  const { prompts } = require('../utils');
15
- const { getModelsForProvider, getModelById } = require('./ai-models');
16
+ const { fetchModelsFromApi } = require('./ai-models');
16
17
 
17
18
  // Config file path
18
19
  const CONFIG_DIR = path.join(os.homedir(), '.hqx');
@@ -135,18 +136,27 @@ const drawModelsTable = (provider, models, boxWidth) => {
135
136
  };
136
137
 
137
138
  /**
138
- * Select a model for a provider
139
+ * Select a model for a provider (fetches from API)
139
140
  * @param {Object} provider - Provider object
140
- * @returns {Object|null} Selected model or null if cancelled
141
+ * @param {string} apiKey - API key for fetching models
142
+ * @returns {Object|null} Selected model or null if cancelled/failed
141
143
  */
142
- const selectModel = async (provider) => {
144
+ const selectModel = async (provider, apiKey) => {
143
145
  const boxWidth = getLogoWidth();
144
- const models = getModelsForProvider(provider.id);
145
146
 
146
- if (models.length === 0) {
147
+ // Fetch models from API
148
+ const spinner = ora({ text: 'Fetching models from API...', color: 'yellow' }).start();
149
+ const result = await fetchModelsFromApi(provider.id, apiKey);
150
+
151
+ if (!result.success || result.models.length === 0) {
152
+ spinner.fail(result.error || 'No models available');
153
+ await prompts.waitForEnter();
147
154
  return null;
148
155
  }
149
156
 
157
+ spinner.succeed(`Found ${result.models.length} models`);
158
+ const models = result.models;
159
+
150
160
  while (true) {
151
161
  console.clear();
152
162
  drawModelsTable(provider, models, boxWidth);
@@ -294,9 +304,11 @@ const handleProviderConfig = async (provider, config) => {
294
304
  }
295
305
 
296
306
  if (choice === '1') {
297
- // CLIProxy connection - select model first
298
- const selectedModel = await selectModel(provider);
299
- if (!selectedModel) continue;
307
+ // CLIProxy connection - models will be fetched via proxy
308
+ console.log();
309
+ console.log(chalk.cyan(' CLIProxy uses your paid plan subscription.'));
310
+ console.log(chalk.gray(' Model selection will be available after connecting.'));
311
+ console.log();
300
312
 
301
313
  // Deactivate all other providers
302
314
  Object.keys(config.providers).forEach(id => {
@@ -305,26 +317,22 @@ const handleProviderConfig = async (provider, config) => {
305
317
 
306
318
  if (!config.providers[provider.id]) config.providers[provider.id] = {};
307
319
  config.providers[provider.id].connectionType = 'cliproxy';
308
- config.providers[provider.id].modelId = selectedModel.id;
309
- config.providers[provider.id].modelName = selectedModel.name;
320
+ config.providers[provider.id].modelId = null;
321
+ config.providers[provider.id].modelName = 'N/A';
310
322
  config.providers[provider.id].active = true;
311
323
  config.providers[provider.id].configuredAt = new Date().toISOString();
312
324
 
313
325
  if (saveConfig(config)) {
314
- console.log(chalk.green(`\n ✓ ${provider.name} connected via CLIProxy.`));
315
- console.log(chalk.cyan(` Model: ${selectedModel.name}`));
326
+ console.log(chalk.green(` ✓ ${provider.name} connected via CLIProxy.`));
316
327
  } else {
317
- console.log(chalk.red('\n Failed to save config.'));
328
+ console.log(chalk.red(' Failed to save config.'));
318
329
  }
319
330
  await prompts.waitForEnter();
320
331
  continue;
321
332
  }
322
333
 
323
334
  if (choice === '2') {
324
- // API Key connection - select model first
325
- const selectedModel = await selectModel(provider);
326
- if (!selectedModel) continue;
327
-
335
+ // API Key connection - get key first, then fetch models
328
336
  console.clear();
329
337
  console.log(chalk.yellow(`\n Enter your ${provider.name} API key:`));
330
338
  console.log(chalk.gray(' (Press Enter to cancel)'));
@@ -344,6 +352,10 @@ const handleProviderConfig = async (provider, config) => {
344
352
  continue;
345
353
  }
346
354
 
355
+ // Fetch models from API with the provided key
356
+ const selectedModel = await selectModel(provider, apiKey.trim());
357
+ if (!selectedModel) continue;
358
+
347
359
  // Deactivate all other providers
348
360
  Object.keys(config.providers).forEach(id => {
349
361
  if (config.providers[id]) config.providers[id].active = false;
@@ -1,84 +1,224 @@
1
1
  /**
2
- * AI Models Configuration
2
+ * AI Models - Fetch from provider APIs
3
3
  *
4
- * Lists available models for each AI provider.
5
- * These are technical configuration values, not trading data.
4
+ * Models are fetched dynamically from each provider's API.
5
+ * No hardcoded model lists - data comes from real APIs only.
6
6
  */
7
7
 
8
- // Models by provider ID
9
- const PROVIDER_MODELS = {
10
- anthropic: [
11
- { id: 'claude-opus-4-20250514', name: 'Claude Opus 4', tier: 'flagship' },
12
- { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', tier: 'balanced' },
13
- { id: 'claude-3-5-sonnet-20241022', name: 'Claude Sonnet 3.5', tier: 'balanced' },
14
- { id: 'claude-3-5-haiku-20241022', name: 'Claude Haiku 3.5', tier: 'fast' },
15
- ],
16
- openai: [
17
- { id: 'gpt-4o', name: 'GPT-4o', tier: 'flagship' },
18
- { id: 'gpt-4o-mini', name: 'GPT-4o Mini', tier: 'fast' },
19
- { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', tier: 'balanced' },
20
- { id: 'o1', name: 'o1', tier: 'reasoning' },
21
- { id: 'o1-mini', name: 'o1-mini', tier: 'reasoning' },
22
- { id: 'o3-mini', name: 'o3-mini', tier: 'reasoning' },
23
- ],
24
- google: [
25
- { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', tier: 'flagship' },
26
- { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', tier: 'balanced' },
27
- { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', tier: 'fast' },
28
- { id: 'gemini-1.0-pro', name: 'Gemini 1.0 Pro', tier: 'legacy' },
29
- ],
30
- mistral: [
31
- { id: 'mistral-large-latest', name: 'Mistral Large', tier: 'flagship' },
32
- { id: 'mistral-medium-latest', name: 'Mistral Medium', tier: 'balanced' },
33
- { id: 'mistral-small-latest', name: 'Mistral Small', tier: 'fast' },
34
- { id: 'codestral-latest', name: 'Codestral', tier: 'code' },
35
- ],
36
- groq: [
37
- { id: 'llama-3.3-70b-versatile', name: 'Llama 3.3 70B', tier: 'flagship' },
38
- { id: 'llama-3.1-8b-instant', name: 'Llama 3.1 8B', tier: 'fast' },
39
- { id: 'mixtral-8x7b-32768', name: 'Mixtral 8x7B', tier: 'balanced' },
40
- { id: 'gemma2-9b-it', name: 'Gemma 2 9B', tier: 'fast' },
41
- ],
42
- xai: [
43
- { id: 'grok-2', name: 'Grok 2', tier: 'flagship' },
44
- { id: 'grok-2-mini', name: 'Grok 2 Mini', tier: 'fast' },
45
- { id: 'grok-beta', name: 'Grok Beta', tier: 'beta' },
46
- ],
47
- perplexity: [
48
- { id: 'sonar-pro', name: 'Sonar Pro', tier: 'flagship' },
49
- { id: 'sonar', name: 'Sonar', tier: 'balanced' },
50
- { id: 'sonar-reasoning', name: 'Sonar Reasoning', tier: 'reasoning' },
51
- ],
52
- openrouter: [
53
- { id: 'anthropic/claude-opus-4', name: 'Claude Opus 4', tier: 'flagship' },
54
- { id: 'openai/gpt-4o', name: 'GPT-4o', tier: 'flagship' },
55
- { id: 'google/gemini-2.0-flash', name: 'Gemini 2.0 Flash', tier: 'flagship' },
56
- { id: 'meta-llama/llama-3.3-70b', name: 'Llama 3.3 70B', tier: 'open' },
57
- ],
8
+ const https = require('https');
9
+
10
+ /**
11
+ * API endpoints for fetching models
12
+ */
13
+ const API_ENDPOINTS = {
14
+ anthropic: 'https://api.anthropic.com/v1/models',
15
+ openai: 'https://api.openai.com/v1/models',
16
+ google: 'https://generativelanguage.googleapis.com/v1/models',
17
+ mistral: 'https://api.mistral.ai/v1/models',
18
+ groq: 'https://api.groq.com/openai/v1/models',
19
+ xai: 'https://api.x.ai/v1/models',
20
+ perplexity: 'https://api.perplexity.ai/models',
21
+ openrouter: 'https://openrouter.ai/api/v1/models',
22
+ };
23
+
24
+ /**
25
+ * Make HTTPS request
26
+ * @param {string} url - API URL
27
+ * @param {Object} headers - Request headers
28
+ * @param {number} timeout - Timeout in ms (default 60000 per RULES.md #15)
29
+ * @returns {Promise<Object>} Response data
30
+ */
31
+ const fetchApi = (url, headers = {}, timeout = 60000) => {
32
+ return new Promise((resolve, reject) => {
33
+ const urlObj = new URL(url);
34
+ const options = {
35
+ hostname: urlObj.hostname,
36
+ path: urlObj.pathname + urlObj.search,
37
+ method: 'GET',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ ...headers
41
+ },
42
+ timeout
43
+ };
44
+
45
+ const req = https.request(options, (res) => {
46
+ let data = '';
47
+ res.on('data', chunk => data += chunk);
48
+ res.on('end', () => {
49
+ try {
50
+ if (res.statusCode >= 200 && res.statusCode < 300) {
51
+ resolve({ success: true, data: JSON.parse(data) });
52
+ } else {
53
+ resolve({ success: false, error: `HTTP ${res.statusCode}` });
54
+ }
55
+ } catch (error) {
56
+ resolve({ success: false, error: 'Invalid JSON response' });
57
+ }
58
+ });
59
+ });
60
+
61
+ req.on('error', (error) => {
62
+ resolve({ success: false, error: error.message });
63
+ });
64
+
65
+ req.on('timeout', () => {
66
+ req.destroy();
67
+ resolve({ success: false, error: 'Request timeout' });
68
+ });
69
+
70
+ req.end();
71
+ });
58
72
  };
59
73
 
60
74
  /**
61
- * Get models for a provider
75
+ * Get auth headers for provider
62
76
  * @param {string} providerId - Provider ID
63
- * @returns {Array} List of models
77
+ * @param {string} apiKey - API key
78
+ * @returns {Object} Headers object
64
79
  */
65
- const getModelsForProvider = (providerId) => {
66
- return PROVIDER_MODELS[providerId] || [];
80
+ const getAuthHeaders = (providerId, apiKey) => {
81
+ if (!apiKey) return {};
82
+
83
+ switch (providerId) {
84
+ case 'anthropic':
85
+ return { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' };
86
+ case 'openai':
87
+ case 'groq':
88
+ case 'xai':
89
+ case 'perplexity':
90
+ case 'openrouter':
91
+ return { 'Authorization': `Bearer ${apiKey}` };
92
+ case 'google':
93
+ return {}; // Google uses query param
94
+ case 'mistral':
95
+ return { 'Authorization': `Bearer ${apiKey}` };
96
+ default:
97
+ return { 'Authorization': `Bearer ${apiKey}` };
98
+ }
67
99
  };
68
100
 
69
101
  /**
70
- * Get model by ID
102
+ * Parse models response based on provider
71
103
  * @param {string} providerId - Provider ID
104
+ * @param {Object} data - API response data
105
+ * @returns {Array} Parsed models list
106
+ */
107
+ const parseModelsResponse = (providerId, data) => {
108
+ if (!data) return [];
109
+
110
+ try {
111
+ switch (providerId) {
112
+ case 'anthropic':
113
+ // Anthropic returns { data: [{ id, display_name, ... }] }
114
+ return (data.data || []).map(m => ({
115
+ id: m.id,
116
+ name: m.display_name || m.id
117
+ }));
118
+
119
+ case 'openai':
120
+ case 'groq':
121
+ case 'xai':
122
+ // OpenAI format: { data: [{ id, ... }] }
123
+ return (data.data || [])
124
+ .filter(m => m.id && !m.id.includes('whisper') && !m.id.includes('tts') && !m.id.includes('dall-e'))
125
+ .map(m => ({
126
+ id: m.id,
127
+ name: m.id
128
+ }));
129
+
130
+ case 'google':
131
+ // Google format: { models: [{ name, displayName, ... }] }
132
+ return (data.models || []).map(m => ({
133
+ id: m.name?.replace('models/', '') || m.name,
134
+ name: m.displayName || m.name
135
+ }));
136
+
137
+ case 'mistral':
138
+ // Mistral format: { data: [{ id, ... }] }
139
+ return (data.data || []).map(m => ({
140
+ id: m.id,
141
+ name: m.id
142
+ }));
143
+
144
+ case 'perplexity':
145
+ // Perplexity format varies
146
+ return (data.models || data.data || []).map(m => ({
147
+ id: m.id || m.model,
148
+ name: m.id || m.model
149
+ }));
150
+
151
+ case 'openrouter':
152
+ // OpenRouter format: { data: [{ id, name, ... }] }
153
+ return (data.data || []).map(m => ({
154
+ id: m.id,
155
+ name: m.name || m.id
156
+ }));
157
+
158
+ default:
159
+ return [];
160
+ }
161
+ } catch (error) {
162
+ return [];
163
+ }
164
+ };
165
+
166
+ /**
167
+ * Fetch models from provider API
168
+ * @param {string} providerId - Provider ID
169
+ * @param {string} apiKey - API key (required for most providers)
170
+ * @returns {Promise<Object>} { success, models, error }
171
+ */
172
+ const fetchModelsFromApi = async (providerId, apiKey) => {
173
+ const endpoint = API_ENDPOINTS[providerId];
174
+ if (!endpoint) {
175
+ return { success: false, models: [], error: 'Unknown provider' };
176
+ }
177
+
178
+ // Build URL (Google needs API key in query)
179
+ let url = endpoint;
180
+ if (providerId === 'google' && apiKey) {
181
+ url += `?key=${apiKey}`;
182
+ }
183
+
184
+ const headers = getAuthHeaders(providerId, apiKey);
185
+ const result = await fetchApi(url, headers);
186
+
187
+ if (!result.success) {
188
+ return { success: false, models: [], error: result.error };
189
+ }
190
+
191
+ const models = parseModelsResponse(providerId, result.data);
192
+
193
+ if (models.length === 0) {
194
+ return { success: false, models: [], error: 'No models returned' };
195
+ }
196
+
197
+ return { success: true, models, error: null };
198
+ };
199
+
200
+ /**
201
+ * Get models for a provider - returns empty, use fetchModelsFromApi
202
+ * @param {string} providerId - Provider ID
203
+ * @returns {Array} Empty array
204
+ */
205
+ const getModelsForProvider = (providerId) => {
206
+ return [];
207
+ };
208
+
209
+ /**
210
+ * Get model by ID - returns null, use API data
211
+ * @param {string} providerId - Provider ID
72
212
  * @param {string} modelId - Model ID
73
- * @returns {Object|null} Model object or null
213
+ * @returns {null} Always null
74
214
  */
75
215
  const getModelById = (providerId, modelId) => {
76
- const models = PROVIDER_MODELS[providerId] || [];
77
- return models.find(m => m.id === modelId) || null;
216
+ return null;
78
217
  };
79
218
 
80
219
  module.exports = {
81
- PROVIDER_MODELS,
220
+ fetchModelsFromApi,
82
221
  getModelsForProvider,
83
- getModelById
222
+ getModelById,
223
+ API_ENDPOINTS
84
224
  };