@loxia-labs/loxia-autopilot-one 1.0.1

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.
Files changed (80) hide show
  1. package/LICENSE +267 -0
  2. package/README.md +509 -0
  3. package/bin/cli.js +117 -0
  4. package/package.json +94 -0
  5. package/scripts/install-scanners.js +236 -0
  6. package/src/analyzers/CSSAnalyzer.js +297 -0
  7. package/src/analyzers/ConfigValidator.js +690 -0
  8. package/src/analyzers/ESLintAnalyzer.js +320 -0
  9. package/src/analyzers/JavaScriptAnalyzer.js +261 -0
  10. package/src/analyzers/PrettierFormatter.js +247 -0
  11. package/src/analyzers/PythonAnalyzer.js +266 -0
  12. package/src/analyzers/SecurityAnalyzer.js +729 -0
  13. package/src/analyzers/TypeScriptAnalyzer.js +247 -0
  14. package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
  15. package/src/analyzers/codeCloneDetector/detector.js +203 -0
  16. package/src/analyzers/codeCloneDetector/index.js +160 -0
  17. package/src/analyzers/codeCloneDetector/parser.js +199 -0
  18. package/src/analyzers/codeCloneDetector/reporter.js +148 -0
  19. package/src/analyzers/codeCloneDetector/scanner.js +59 -0
  20. package/src/core/agentPool.js +1474 -0
  21. package/src/core/agentScheduler.js +2147 -0
  22. package/src/core/contextManager.js +709 -0
  23. package/src/core/messageProcessor.js +732 -0
  24. package/src/core/orchestrator.js +548 -0
  25. package/src/core/stateManager.js +877 -0
  26. package/src/index.js +631 -0
  27. package/src/interfaces/cli.js +549 -0
  28. package/src/interfaces/webServer.js +2162 -0
  29. package/src/modules/fileExplorer/controller.js +280 -0
  30. package/src/modules/fileExplorer/index.js +37 -0
  31. package/src/modules/fileExplorer/middleware.js +92 -0
  32. package/src/modules/fileExplorer/routes.js +125 -0
  33. package/src/modules/fileExplorer/types.js +44 -0
  34. package/src/services/aiService.js +1232 -0
  35. package/src/services/apiKeyManager.js +164 -0
  36. package/src/services/benchmarkService.js +366 -0
  37. package/src/services/budgetService.js +539 -0
  38. package/src/services/contextInjectionService.js +247 -0
  39. package/src/services/conversationCompactionService.js +637 -0
  40. package/src/services/errorHandler.js +810 -0
  41. package/src/services/fileAttachmentService.js +544 -0
  42. package/src/services/modelRouterService.js +366 -0
  43. package/src/services/modelsService.js +322 -0
  44. package/src/services/qualityInspector.js +796 -0
  45. package/src/services/tokenCountingService.js +536 -0
  46. package/src/tools/agentCommunicationTool.js +1344 -0
  47. package/src/tools/agentDelayTool.js +485 -0
  48. package/src/tools/asyncToolManager.js +604 -0
  49. package/src/tools/baseTool.js +800 -0
  50. package/src/tools/browserTool.js +920 -0
  51. package/src/tools/cloneDetectionTool.js +621 -0
  52. package/src/tools/dependencyResolverTool.js +1215 -0
  53. package/src/tools/fileContentReplaceTool.js +875 -0
  54. package/src/tools/fileSystemTool.js +1107 -0
  55. package/src/tools/fileTreeTool.js +853 -0
  56. package/src/tools/imageTool.js +901 -0
  57. package/src/tools/importAnalyzerTool.js +1060 -0
  58. package/src/tools/jobDoneTool.js +248 -0
  59. package/src/tools/seekTool.js +956 -0
  60. package/src/tools/staticAnalysisTool.js +1778 -0
  61. package/src/tools/taskManagerTool.js +2873 -0
  62. package/src/tools/terminalTool.js +2304 -0
  63. package/src/tools/webTool.js +1430 -0
  64. package/src/types/agent.js +519 -0
  65. package/src/types/contextReference.js +972 -0
  66. package/src/types/conversation.js +730 -0
  67. package/src/types/toolCommand.js +747 -0
  68. package/src/utilities/attachmentValidator.js +292 -0
  69. package/src/utilities/configManager.js +582 -0
  70. package/src/utilities/constants.js +722 -0
  71. package/src/utilities/directoryAccessManager.js +535 -0
  72. package/src/utilities/fileProcessor.js +307 -0
  73. package/src/utilities/logger.js +436 -0
  74. package/src/utilities/tagParser.js +1246 -0
  75. package/src/utilities/toolConstants.js +317 -0
  76. package/web-ui/build/index.html +15 -0
  77. package/web-ui/build/logo.png +0 -0
  78. package/web-ui/build/logo2.png +0 -0
  79. package/web-ui/build/static/index-CjkkcnFA.js +344 -0
  80. package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * ModelRouterService - Uses PHI-4 to make intelligent model routing decisions
3
+ *
4
+ * Purpose:
5
+ * - Analyze incoming messages and conversation context
6
+ * - Use PHI-4 to determine optimal model for each message
7
+ * - Provide fallback to previous model on errors
8
+ * - Consider benchmark data and recent message history
9
+ */
10
+
11
+ import { MODEL_ROUTER_CONFIG, HTTP_STATUS, MODELS } from '../utilities/constants.js';
12
+
13
+ class ModelRouterService {
14
+ constructor(config, logger, benchmarkService, aiService) {
15
+ this.config = config;
16
+ this.logger = logger;
17
+ this.benchmarkService = benchmarkService;
18
+ this.aiService = aiService;
19
+
20
+ this.routerModel = MODEL_ROUTER_CONFIG.ROUTER_MODEL;
21
+ this.contextMessagesCount = MODEL_ROUTER_CONFIG.CONTEXT_MESSAGES_COUNT;
22
+ this.requestTimeout = MODEL_ROUTER_CONFIG.REQUEST_TIMEOUT;
23
+ }
24
+
25
+ /**
26
+ * Route message to optimal model using autopilot-model-router analysis
27
+ * @param {Object} message - Current message to route
28
+ * @param {Array} recentMessages - Recent conversation history
29
+ * @param {string} currentModel - Current model being used
30
+ * @param {Array} availableModels - Models available for routing
31
+ * @param {Object} context - Message context with API keys
32
+ * @returns {Promise<string>} Selected model name
33
+ */
34
+ async routeMessage(message, recentMessages = [], currentModel = '', availableModels = [], context = {}) {
35
+ try {
36
+ this.logger.debug('Starting model routing analysis', {
37
+ messageLength: message.content?.length || 0,
38
+ recentMessagesCount: recentMessages.length,
39
+ currentModel,
40
+ availableModelsCount: availableModels.length
41
+ });
42
+
43
+ // Build context for autopilot-model-router
44
+ const routingContext = this._buildRoutingContext(
45
+ message,
46
+ recentMessages,
47
+ currentModel,
48
+ availableModels
49
+ );
50
+
51
+ // Get benchmark data
52
+ const benchmarkTable = this.benchmarkService.getBenchmarkTable();
53
+
54
+ // Create routing prompt for autopilot-model-router
55
+ const routingPrompt = this._createRoutingPrompt(routingContext, benchmarkTable);
56
+
57
+ // Send to autopilot-model-router for analysis
58
+ const routingDecision = await this._askForRouting(routingPrompt, context);
59
+
60
+ // Validate and return model selection
61
+ const selectedModel = this._validateModelSelection(routingDecision, availableModels, currentModel);
62
+
63
+ this.logger.info('Model routing completed', {
64
+ selectedModel,
65
+ previousModel: currentModel,
66
+ changed: selectedModel !== currentModel,
67
+ reasoning: routingDecision.reasoning?.substring(0, 100) || 'No reasoning provided'
68
+ });
69
+
70
+ // Return full routing result object for AgentScheduler
71
+ return {
72
+ selectedModel,
73
+ previousModel: currentModel,
74
+ changed: selectedModel !== currentModel,
75
+ reasoning: routingDecision.reasoning || 'No reasoning provided'
76
+ };
77
+
78
+ } catch (error) {
79
+ this.logger.error('Model routing failed, falling back to current model', {
80
+ error: error.message,
81
+ currentModel
82
+ });
83
+
84
+ // Fallback to current model on any error
85
+ const fallbackModel = currentModel || availableModels[0] || MODELS.ANTHROPIC_SONNET;
86
+ return {
87
+ selectedModel: fallbackModel,
88
+ previousModel: currentModel,
89
+ changed: false,
90
+ reasoning: 'Routing failed, using fallback model'
91
+ };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Build routing context from message and conversation history
97
+ * @private
98
+ */
99
+ _buildRoutingContext(message, recentMessages, currentModel, availableModels) {
100
+ // Get recent messages (configurable count)
101
+ const contextMessages = recentMessages
102
+ .slice(-this.contextMessagesCount)
103
+ .map(msg => ({
104
+ role: msg.role,
105
+ content: msg.content?.substring(0, 500) || '', // Limit content length
106
+ timestamp: msg.timestamp
107
+ }));
108
+
109
+ return {
110
+ currentMessage: {
111
+ content: message.content?.substring(0, 1000) || '', // Limit current message
112
+ role: message.role || 'user',
113
+ hasContextReferences: !!(message.contextReferences?.length),
114
+ contextTypes: message.contextReferences?.map(ref => ref.type) || []
115
+ },
116
+ recentMessages: contextMessages,
117
+ currentModel,
118
+ availableModels: availableModels.map(model => ({
119
+ name: model,
120
+ isCurrentModel: model === currentModel
121
+ })),
122
+ messageCount: recentMessages.length + 1,
123
+ timestamp: new Date().toISOString()
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Create routing prompt for autopilot-model-router
129
+ * @private
130
+ */
131
+ _createRoutingPrompt(context, benchmarkTable) {
132
+ return `You are a model routing assistant. Your job is to analyze a conversation and select the optimal AI model for the next response.
133
+
134
+ ## Current Situation
135
+ - **Current Model**: ${context.currentModel || 'None'}
136
+ - **Message Count**: ${context.messageCount}
137
+ - **Available Models**: ${context.availableModels.map(m => m.name).join(', ')}
138
+
139
+ ## Recent Conversation Context
140
+ ${this._formatRecentMessages(context.recentMessages)}
141
+
142
+ ## Current Message to Route
143
+ **Role**: ${context.currentMessage.role}
144
+ **Content**: ${context.currentMessage.content}
145
+ **Has Context References**: ${context.currentMessage.hasContextReferences}
146
+ ${context.currentMessage.contextTypes.length > 0 ? `**Context Types**: ${context.currentMessage.contextTypes.join(', ')}` : ''}
147
+
148
+ ## Model Benchmark Data
149
+ ${this._formatBenchmarkData(benchmarkTable)}
150
+
151
+ ## Routing Decision
152
+ Analyze the current message and conversation context to select the best model. Consider:
153
+
154
+ 1. **Task Type**: Infer from conversation content - is this coding, analysis, creative writing, or a quick task?
155
+ 2. **Complexity**: Does it require deep reasoning or is it straightforward?
156
+ 3. **Context**: Are there code files or technical references?
157
+ 4. **Conversation Flow**: What has been discussed recently to understand the overall task?
158
+ 5. **Efficiency**: Balance performance with cost/speed needs
159
+
160
+ Respond with JSON only:
161
+ {
162
+ "selectedModel": "model-name",
163
+ "taskType": "coding|analysis|creative|quick-tasks",
164
+ "confidence": 0.85,
165
+ "reasoning": "Brief explanation of why this model was chosen",
166
+ "factors": ["factor1", "factor2", "factor3"]
167
+ }`;
168
+ }
169
+
170
+ /**
171
+ * Format recent messages for prompt
172
+ * @private
173
+ */
174
+ _formatRecentMessages(messages) {
175
+ if (!messages.length) {
176
+ return 'No recent messages available.';
177
+ }
178
+
179
+ return messages.map((msg, i) =>
180
+ `${i + 1}. **${msg.role}**: ${msg.content}`
181
+ ).join('\n');
182
+ }
183
+
184
+ /**
185
+ * Format benchmark data for prompt
186
+ * @private
187
+ */
188
+ _formatBenchmarkData(benchmarkTable) {
189
+ const models = benchmarkTable.models || [];
190
+ const tasks = benchmarkTable.tasks || {};
191
+ const summary = benchmarkTable.summary || {};
192
+
193
+ let formatted = '### Model Performance Summary\n';
194
+
195
+ // Task recommendations
196
+ Object.entries(tasks).forEach(([taskType, taskInfo]) => {
197
+ formatted += `**${taskType.toUpperCase()}** (${taskInfo.description}): ${taskInfo.topModels?.join(', ') || 'No data'}\n`;
198
+ });
199
+
200
+ formatted += '\n### Model Details\n';
201
+
202
+ // Model details (limit to available models)
203
+ models.slice(0, 8).forEach(model => {
204
+ formatted += `**${model.name}**: `;
205
+ const taskScores = Object.entries(model.tasks || {})
206
+ .map(([task, info]) => `${task}(${info.score})`)
207
+ .join(', ');
208
+ formatted += `${taskScores} | Strengths: ${model.strengths?.join(', ') || 'N/A'}\n`;
209
+ });
210
+
211
+ return formatted;
212
+ }
213
+
214
+ /**
215
+ * Send routing request to autopilot-model-router
216
+ * @private
217
+ */
218
+ async _askForRouting(prompt, context = {}) {
219
+ try {
220
+ // Use AI service to send request to autopilot-model-router
221
+ const response = await this.aiService.sendMessage(
222
+ this.routerModel,
223
+ prompt,
224
+ {
225
+ agentId: 'model-router',
226
+ systemPrompt: 'You are a model routing expert. Analyze conversations and select optimal AI models. Always respond with valid JSON only.',
227
+ temperature: 0.3, // Low temperature for consistent routing decisions
228
+ maxTokens: 500,
229
+ timeout: this.requestTimeout,
230
+ sessionId: context.sessionId, // CRITICAL: Pass sessionId for API key retrieval
231
+ apiKey: context.apiKey || this.config.apiKey, // Fallback to API key from context if available
232
+ customApiKeys: context.customApiKeys,
233
+ platformProvided: true // autopilot-model-router is a platform model
234
+ }
235
+ );
236
+
237
+ if (!response.content) {
238
+ throw new Error('No response content from autopilot-model-router');
239
+ }
240
+
241
+ // Parse JSON response
242
+ const routingDecision = this._parseRoutingResponse(response.content);
243
+
244
+ return routingDecision;
245
+
246
+ } catch (error) {
247
+ this.logger.error('autopilot-model-router routing request failed', {
248
+ error: error.message,
249
+ routerModel: this.routerModel
250
+ });
251
+ throw error;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Parse autopilot-model-router routing response
257
+ * @private
258
+ */
259
+ _parseRoutingResponse(content) {
260
+ try {
261
+ // Try to extract JSON from response
262
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
263
+ if (!jsonMatch) {
264
+ throw new Error('No JSON found in autopilot-model-router response');
265
+ }
266
+
267
+ const parsed = JSON.parse(jsonMatch[0]);
268
+
269
+ // Validate required fields
270
+ if (!parsed.selectedModel) {
271
+ throw new Error('Missing selectedModel in autopilot-model-router response');
272
+ }
273
+
274
+ return {
275
+ selectedModel: parsed.selectedModel,
276
+ taskType: parsed.taskType || 'unknown',
277
+ confidence: parsed.confidence || 0.5,
278
+ reasoning: parsed.reasoning || 'No reasoning provided',
279
+ factors: parsed.factors || []
280
+ };
281
+
282
+ } catch (error) {
283
+ this.logger.warn('Failed to parse autopilot-model-router routing response', {
284
+ error: error.message,
285
+ content: content.substring(0, 200)
286
+ });
287
+
288
+ // Return default response on parse error
289
+ return {
290
+ selectedModel: null,
291
+ taskType: 'unknown',
292
+ confidence: 0.0,
293
+ reasoning: 'Failed to parse routing response',
294
+ factors: ['parsing-error']
295
+ };
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Validate model selection against available models
301
+ * @private
302
+ */
303
+ _validateModelSelection(routingDecision, availableModels, currentModel) {
304
+ const { selectedModel } = routingDecision;
305
+
306
+ // If no model selected or parsing failed, use current model
307
+ if (!selectedModel) {
308
+ this.logger.debug('No model selected by router, using current model');
309
+ return currentModel;
310
+ }
311
+
312
+ // Check if selected model is available
313
+ if (!availableModels.includes(selectedModel)) {
314
+ this.logger.warn('Router selected unavailable model, using current model', {
315
+ selectedModel,
316
+ availableModels
317
+ });
318
+ return currentModel;
319
+ }
320
+
321
+ // Model is valid
322
+ return selectedModel;
323
+ }
324
+
325
+ /**
326
+ * Get router service status
327
+ */
328
+ getStatus() {
329
+ return {
330
+ routerModel: this.routerModel,
331
+ contextMessagesCount: this.contextMessagesCount,
332
+ requestTimeout: this.requestTimeout,
333
+ benchmarkServiceStatus: this.benchmarkService.getStatus(),
334
+ isAvailable: true
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Test router with sample data
340
+ */
341
+ async testRouter() {
342
+ try {
343
+ const testMessage = {
344
+ content: 'Can you help me debug this JavaScript function?',
345
+ role: 'user'
346
+ };
347
+
348
+ const testResult = await this.routeMessage(
349
+ testMessage,
350
+ [],
351
+ MODELS.ANTHROPIC_SONNET,
352
+ [MODELS.ANTHROPIC_SONNET, MODELS.ANTHROPIC_OPUS, MODELS.GPT_4, MODELS.DEEPSEEK_R1],
353
+ { apiKey: 'test-key' } // Mock context for testing
354
+ );
355
+
356
+ this.logger.info('Router test completed', { selectedModel: testResult });
357
+ return { success: true, selectedModel: testResult };
358
+
359
+ } catch (error) {
360
+ this.logger.error('Router test failed', { error: error.message });
361
+ return { success: false, error: error.message };
362
+ }
363
+ }
364
+ }
365
+
366
+ export default ModelRouterService;
@@ -0,0 +1,322 @@
1
+ /**
2
+ * ModelsService - Manages available models from the backend API
3
+ *
4
+ * Purpose:
5
+ * - Fetch available models from /llm/models endpoint
6
+ * - Cache models for the session
7
+ * - Provide model names for routing
8
+ * - Handle fallback when API is unavailable
9
+ */
10
+
11
+ class ModelsService {
12
+ constructor(config, logger) {
13
+ this.config = config;
14
+ this.logger = logger;
15
+
16
+ this.models = null;
17
+ this.lastFetched = null;
18
+ this.isLoading = false;
19
+ this.cacheExpiry = 5 * 60 * 1000; // 5 minutes cache
20
+
21
+ this.backendUrl = 'https://autopilot-api.azurewebsites.net';
22
+
23
+ // API Key Manager reference (will be set by LoxiaSystem)
24
+ this.apiKeyManager = null;
25
+ }
26
+
27
+ /**
28
+ * Set API key manager instance
29
+ * @param {ApiKeyManager} apiKeyManager - API key manager instance
30
+ */
31
+ setApiKeyManager(apiKeyManager) {
32
+ this.apiKeyManager = apiKeyManager;
33
+
34
+ this.logger?.info('API key manager set for models service', {
35
+ hasManager: !!apiKeyManager
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Initialize the models service and fetch initial data
41
+ */
42
+ async initialize() {
43
+ try {
44
+ await this.fetchModels();
45
+ this.logger.info('Models service initialized', {
46
+ modelCount: this.models?.length || 0
47
+ });
48
+ } catch (error) {
49
+ this.logger.error('Failed to initialize models service', {
50
+ error: error.message
51
+ });
52
+ // Use fallback models on initialization failure
53
+ this.models = this._getFallbackModels();
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get available model names for routing
59
+ */
60
+ getAvailableModelNames() {
61
+ if (!this.models || this.models.length === 0) {
62
+ this.logger.warn('No models available, using fallback');
63
+ return this._getFallbackModelNames();
64
+ }
65
+
66
+ return this.models.map(model => model.name);
67
+ }
68
+
69
+ /**
70
+ * Get all model information
71
+ */
72
+ getModels() {
73
+ return this.models || this._getFallbackModels();
74
+ }
75
+
76
+ /**
77
+ * Fetch models from backend API
78
+ * @param {Object} context - Optional context with API key
79
+ */
80
+ async fetchModels(context = null) {
81
+ if (this.isLoading) {
82
+ this.logger.debug('Models fetch already in progress');
83
+ return;
84
+ }
85
+
86
+ this.isLoading = true;
87
+
88
+ try {
89
+ const url = `${this.backendUrl}/llm/models`;
90
+
91
+ // Get API key from multiple sources
92
+ let apiKey = null;
93
+
94
+ // First try to get from API key manager using session ID
95
+ if (this.apiKeyManager && context && context.sessionId) {
96
+ const keys = this.apiKeyManager.getKeysForRequest(context.sessionId, {
97
+ platformProvided: true, // Models endpoint uses platform key
98
+ vendor: null
99
+ });
100
+
101
+ apiKey = keys.loxiaApiKey;
102
+
103
+ this.logger?.info('Retrieved API key from session manager for models fetch', {
104
+ sessionId: context.sessionId,
105
+ hasLoxiaKey: !!apiKey
106
+ });
107
+ }
108
+
109
+ // Fallback to context, config, then environment
110
+ if (!apiKey && context && context.apiKey) {
111
+ apiKey = context.apiKey;
112
+ } else if (!apiKey && this.config.apiKey) {
113
+ apiKey = this.config.apiKey;
114
+ } else if (!apiKey && process.env.LOXIA_API_KEY) {
115
+ apiKey = process.env.LOXIA_API_KEY;
116
+ }
117
+
118
+ const fetchOptions = {
119
+ method: 'GET',
120
+ headers: {
121
+ 'Content-Type': 'application/json',
122
+ ...(apiKey && { 'Authorization': `Bearer ${apiKey}` })
123
+ },
124
+ timeout: 10000 // 10 second timeout
125
+ };
126
+
127
+ this.logger.debug('Fetching models from backend', {
128
+ url,
129
+ hasApiKey: !!apiKey,
130
+ apiKeySource: context?.apiKey ? 'context' : this.config.apiKey ? 'config' : process.env.LOXIA_API_KEY ? 'env' : 'none'
131
+ });
132
+
133
+ const response = await fetch(url, fetchOptions);
134
+
135
+ if (!response.ok) {
136
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
137
+ }
138
+
139
+ const data = await response.json();
140
+
141
+ if (data.models && Array.isArray(data.models)) {
142
+ this.models = data.models;
143
+ this.lastFetched = new Date();
144
+ this.needsRefresh = false;
145
+
146
+ this.logger.info('Models fetched from backend', {
147
+ modelCount: this.models.length,
148
+ models: this.models.map(m => m.name)
149
+ });
150
+ } else {
151
+ throw new Error('Invalid response format from models API');
152
+ }
153
+
154
+ } catch (error) {
155
+ this.logger.warn('Failed to fetch models from backend, using fallback', {
156
+ error: error.message
157
+ });
158
+
159
+ // Use fallback data on fetch error
160
+ if (!this.models) {
161
+ this.models = this._getFallbackModels();
162
+ }
163
+
164
+ } finally {
165
+ this.isLoading = false;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Check if models cache needs refresh
171
+ */
172
+ needsRefresh() {
173
+ if (!this.lastFetched) return true;
174
+
175
+ const timeSinceUpdate = Date.now() - this.lastFetched.getTime();
176
+ return timeSinceUpdate > this.cacheExpiry;
177
+ }
178
+
179
+ /**
180
+ * Force refresh models from backend
181
+ * @param {Object} context - Optional context with API key
182
+ */
183
+ async refresh(context = null) {
184
+ this.logger.info('Force refreshing models');
185
+ await this.fetchModels(context);
186
+ }
187
+
188
+ /**
189
+ * Refresh models with API key context if needed
190
+ * @param {Object} context - Context with API key
191
+ */
192
+ async refreshWithContext(context) {
193
+ // Only refresh if we haven't successfully fetched or if we need a refresh
194
+ if (this.needsRefresh || !this.lastFetched) {
195
+ this.logger.debug('Refreshing models with API key context');
196
+ await this.fetchModels(context);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get fallback models when API is unavailable
202
+ * @private
203
+ */
204
+ _getFallbackModels() {
205
+ return [
206
+ {
207
+ name: 'anthropic-sonnet',
208
+ category: 'anthropic',
209
+ type: 'chat',
210
+ maxTokens: 10000,
211
+ supportsVision: true,
212
+ supportsSystem: true,
213
+ pricing: { input: 0.003, output: 0.015, unit: '1K tokens' }
214
+ },
215
+ {
216
+ name: 'anthropic-opus',
217
+ category: 'anthropic',
218
+ type: 'chat',
219
+ maxTokens: 10000,
220
+ supportsVision: true,
221
+ supportsSystem: true,
222
+ pricing: { input: 0.015, output: 0.075, unit: '1K tokens' }
223
+ },
224
+ {
225
+ name: 'anthropic-haiku',
226
+ category: 'anthropic',
227
+ type: 'chat',
228
+ maxTokens: 10000,
229
+ supportsVision: true,
230
+ supportsSystem: true,
231
+ pricing: { input: 0.00025, output: 0.00125, unit: '1K tokens' }
232
+ },
233
+ {
234
+ name: 'gpt-4',
235
+ category: 'openai',
236
+ type: 'chat',
237
+ maxTokens: 4096,
238
+ supportsVision: false,
239
+ supportsSystem: true,
240
+ pricing: { input: 0.030, output: 0.060, unit: '1K tokens' }
241
+ },
242
+ {
243
+ name: 'gpt-4-mini',
244
+ category: 'openai',
245
+ type: 'chat',
246
+ maxTokens: 16384,
247
+ supportsVision: false,
248
+ supportsSystem: true,
249
+ pricing: { input: 0.00015, output: 0.0006, unit: '1K tokens' }
250
+ },
251
+ {
252
+ name: 'deepseek-r1',
253
+ category: 'deepseek',
254
+ type: 'chat',
255
+ maxTokens: 8192,
256
+ supportsVision: false,
257
+ supportsSystem: true,
258
+ pricing: { input: 0.014, output: 0.028, unit: '1K tokens' }
259
+ },
260
+ {
261
+ name: 'azure-ai-grok3',
262
+ category: 'azure',
263
+ type: 'chat',
264
+ maxTokens: 4096,
265
+ supportsVision: false,
266
+ supportsSystem: true,
267
+ pricing: { input: 0.01, output: 0.02, unit: '1K tokens' }
268
+ },
269
+ {
270
+ name: 'phi-4',
271
+ category: 'microsoft',
272
+ type: 'chat',
273
+ maxTokens: 2048,
274
+ supportsVision: false,
275
+ supportsSystem: true,
276
+ pricing: { input: 0.010, output: 0.020, unit: '1K tokens' }
277
+ },
278
+ {
279
+ name: 'autopilot-model-router',
280
+ category: 'azure',
281
+ type: 'chat',
282
+ maxTokens: 2048,
283
+ supportsVision: false,
284
+ supportsSystem: true,
285
+ pricing: { input: 0.001, output: 0.002, unit: '1K tokens' }
286
+ }
287
+ ];
288
+ }
289
+
290
+ /**
291
+ * Get fallback model names only
292
+ * @private
293
+ */
294
+ _getFallbackModelNames() {
295
+ return [
296
+ 'anthropic-sonnet',
297
+ 'anthropic-opus',
298
+ 'anthropic-haiku',
299
+ 'gpt-4',
300
+ 'gpt-4-mini',
301
+ 'deepseek-r1',
302
+ 'azure-ai-grok3',
303
+ 'phi-4',
304
+ 'autopilot-model-router'
305
+ ];
306
+ }
307
+
308
+ /**
309
+ * Get service status
310
+ */
311
+ getStatus() {
312
+ return {
313
+ initialized: !!this.models,
314
+ lastFetched: this.lastFetched?.toISOString() || null,
315
+ modelCount: this.models?.length || 0,
316
+ isLoading: this.isLoading,
317
+ needsRefresh: this.needsRefresh()
318
+ };
319
+ }
320
+ }
321
+
322
+ export default ModelsService;