crawlforge-mcp-server 3.0.0

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 (75) hide show
  1. package/CLAUDE.md +315 -0
  2. package/LICENSE +21 -0
  3. package/README.md +181 -0
  4. package/package.json +115 -0
  5. package/server.js +1963 -0
  6. package/setup.js +112 -0
  7. package/src/constants/config.js +615 -0
  8. package/src/core/ActionExecutor.js +1104 -0
  9. package/src/core/AlertNotificationSystem.js +601 -0
  10. package/src/core/AuthManager.js +315 -0
  11. package/src/core/ChangeTracker.js +2306 -0
  12. package/src/core/JobManager.js +687 -0
  13. package/src/core/LLMsTxtAnalyzer.js +753 -0
  14. package/src/core/LocalizationManager.js +1615 -0
  15. package/src/core/PerformanceManager.js +828 -0
  16. package/src/core/ResearchOrchestrator.js +1327 -0
  17. package/src/core/SnapshotManager.js +1037 -0
  18. package/src/core/StealthBrowserManager.js +1795 -0
  19. package/src/core/WebhookDispatcher.js +745 -0
  20. package/src/core/analysis/ContentAnalyzer.js +749 -0
  21. package/src/core/analysis/LinkAnalyzer.js +972 -0
  22. package/src/core/cache/CacheManager.js +821 -0
  23. package/src/core/connections/ConnectionPool.js +553 -0
  24. package/src/core/crawlers/BFSCrawler.js +845 -0
  25. package/src/core/integrations/PerformanceIntegration.js +377 -0
  26. package/src/core/llm/AnthropicProvider.js +135 -0
  27. package/src/core/llm/LLMManager.js +415 -0
  28. package/src/core/llm/LLMProvider.js +97 -0
  29. package/src/core/llm/OpenAIProvider.js +127 -0
  30. package/src/core/processing/BrowserProcessor.js +986 -0
  31. package/src/core/processing/ContentProcessor.js +505 -0
  32. package/src/core/processing/PDFProcessor.js +448 -0
  33. package/src/core/processing/StreamProcessor.js +673 -0
  34. package/src/core/queue/QueueManager.js +98 -0
  35. package/src/core/workers/WorkerPool.js +585 -0
  36. package/src/core/workers/worker.js +743 -0
  37. package/src/monitoring/healthCheck.js +600 -0
  38. package/src/monitoring/metrics.js +761 -0
  39. package/src/optimization/wave3-optimizations.js +932 -0
  40. package/src/security/security-patches.js +120 -0
  41. package/src/security/security-tests.js +355 -0
  42. package/src/security/wave3-security.js +652 -0
  43. package/src/tools/advanced/BatchScrapeTool.js +1089 -0
  44. package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
  45. package/src/tools/crawl/crawlDeep.js +449 -0
  46. package/src/tools/crawl/mapSite.js +400 -0
  47. package/src/tools/extract/analyzeContent.js +624 -0
  48. package/src/tools/extract/extractContent.js +329 -0
  49. package/src/tools/extract/processDocument.js +503 -0
  50. package/src/tools/extract/summarizeContent.js +376 -0
  51. package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
  52. package/src/tools/research/deepResearch.js +706 -0
  53. package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
  54. package/src/tools/search/adapters/googleSearch.js +236 -0
  55. package/src/tools/search/adapters/searchProviderFactory.js +96 -0
  56. package/src/tools/search/queryExpander.js +543 -0
  57. package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
  58. package/src/tools/search/ranking/ResultRanker.js +497 -0
  59. package/src/tools/search/searchWeb.js +482 -0
  60. package/src/tools/tracking/trackChanges.js +1355 -0
  61. package/src/utils/CircuitBreaker.js +515 -0
  62. package/src/utils/ErrorHandlingConfig.js +342 -0
  63. package/src/utils/HumanBehaviorSimulator.js +569 -0
  64. package/src/utils/Logger.js +568 -0
  65. package/src/utils/MemoryMonitor.js +173 -0
  66. package/src/utils/RetryManager.js +386 -0
  67. package/src/utils/contentUtils.js +588 -0
  68. package/src/utils/domainFilter.js +612 -0
  69. package/src/utils/inputValidation.js +766 -0
  70. package/src/utils/rateLimiter.js +196 -0
  71. package/src/utils/robotsChecker.js +91 -0
  72. package/src/utils/securityMiddleware.js +416 -0
  73. package/src/utils/sitemapParser.js +678 -0
  74. package/src/utils/ssrfProtection.js +640 -0
  75. package/src/utils/urlNormalizer.js +168 -0
@@ -0,0 +1,415 @@
1
+ import { OpenAIProvider } from './OpenAIProvider.js';
2
+ import { AnthropicProvider } from './AnthropicProvider.js';
3
+ import { Logger } from '../../utils/Logger.js';
4
+
5
+ /**
6
+ * LLM Manager
7
+ * Manages multiple LLM providers and provides unified interface
8
+ */
9
+ export class LLMManager {
10
+ constructor(options = {}) {
11
+ this.logger = new Logger({ component: 'LLMManager' });
12
+ this.providers = new Map();
13
+ this.defaultProvider = null;
14
+ this.fallbackProvider = null;
15
+
16
+ this.initializeProviders(options);
17
+ }
18
+
19
+ /**
20
+ * Initialize available LLM providers
21
+ */
22
+ initializeProviders(options) {
23
+ const {
24
+ openai = {},
25
+ anthropic = {},
26
+ defaultProvider = 'auto'
27
+ } = options;
28
+
29
+ // Initialize OpenAI provider
30
+ if (openai.apiKey || process.env.OPENAI_API_KEY) {
31
+ const openaiProvider = new OpenAIProvider(openai);
32
+ this.providers.set('openai', openaiProvider);
33
+ this.logger.info('OpenAI provider initialized');
34
+ }
35
+
36
+ // Initialize Anthropic provider
37
+ if (anthropic.apiKey || process.env.ANTHROPIC_API_KEY) {
38
+ const anthropicProvider = new AnthropicProvider(anthropic);
39
+ this.providers.set('anthropic', anthropicProvider);
40
+ this.logger.info('Anthropic provider initialized');
41
+ }
42
+
43
+ // Set default provider
44
+ this.setDefaultProvider(defaultProvider);
45
+ }
46
+
47
+ /**
48
+ * Set the default provider
49
+ */
50
+ setDefaultProvider(providerName) {
51
+ if (providerName === 'auto') {
52
+ // Auto-select: prefer OpenAI for embeddings, fallback to Anthropic
53
+ if (this.providers.has('openai')) {
54
+ this.defaultProvider = 'openai';
55
+ this.fallbackProvider = this.providers.has('anthropic') ? 'anthropic' : null;
56
+ } else if (this.providers.has('anthropic')) {
57
+ this.defaultProvider = 'anthropic';
58
+ this.fallbackProvider = null;
59
+ }
60
+ } else if (this.providers.has(providerName)) {
61
+ this.defaultProvider = providerName;
62
+ // Set fallback to other available provider
63
+ for (const [name, provider] of this.providers) {
64
+ if (name !== providerName) {
65
+ this.fallbackProvider = name;
66
+ break;
67
+ }
68
+ }
69
+ }
70
+
71
+ if (this.defaultProvider) {
72
+ this.logger.info(`Default LLM provider set to: ${this.defaultProvider}`);
73
+ if (this.fallbackProvider) {
74
+ this.logger.info(`Fallback LLM provider: ${this.fallbackProvider}`);
75
+ }
76
+ } else {
77
+ this.logger.warn('No LLM providers available');
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get a provider instance
83
+ */
84
+ getProvider(name = null) {
85
+ const providerName = name || this.defaultProvider;
86
+ if (!providerName) {
87
+ throw new Error('No LLM provider available');
88
+ }
89
+
90
+ const provider = this.providers.get(providerName);
91
+ if (!provider) {
92
+ throw new Error(`LLM provider '${providerName}' not found`);
93
+ }
94
+
95
+ return provider;
96
+ }
97
+
98
+ /**
99
+ * Generate completion with fallback support
100
+ */
101
+ async generateCompletion(prompt, options = {}) {
102
+ const { provider = null, ...llmOptions } = options;
103
+
104
+ try {
105
+ const llmProvider = this.getProvider(provider);
106
+ return await llmProvider.generateCompletion(prompt, llmOptions);
107
+ } catch (error) {
108
+ this.logger.warn(`Primary provider failed: ${error.message}`);
109
+
110
+ // Try fallback provider if available
111
+ if (this.fallbackProvider && (!provider || provider === this.defaultProvider)) {
112
+ try {
113
+ this.logger.info(`Trying fallback provider: ${this.fallbackProvider}`);
114
+ const fallbackLLM = this.getProvider(this.fallbackProvider);
115
+ return await fallbackLLM.generateCompletion(prompt, llmOptions);
116
+ } catch (fallbackError) {
117
+ this.logger.error(`Fallback provider also failed: ${fallbackError.message}`);
118
+ }
119
+ }
120
+
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Generate embeddings with fallback support
127
+ */
128
+ async generateEmbedding(text, options = {}) {
129
+ const { provider = null } = options;
130
+
131
+ try {
132
+ const llmProvider = this.getProvider(provider);
133
+ return await llmProvider.generateEmbedding(text);
134
+ } catch (error) {
135
+ this.logger.warn(`Primary provider embedding failed: ${error.message}`);
136
+
137
+ // Try fallback provider if available
138
+ if (this.fallbackProvider && (!provider || provider === this.defaultProvider)) {
139
+ try {
140
+ this.logger.info(`Trying fallback provider for embedding: ${this.fallbackProvider}`);
141
+ const fallbackLLM = this.getProvider(this.fallbackProvider);
142
+ return await fallbackLLM.generateEmbedding(text);
143
+ } catch (fallbackError) {
144
+ this.logger.error(`Fallback provider embedding also failed: ${fallbackError.message}`);
145
+ }
146
+ }
147
+
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Calculate semantic similarity
154
+ */
155
+ async calculateSimilarity(text1, text2, options = {}) {
156
+ const { provider = null } = options;
157
+
158
+ try {
159
+ const llmProvider = this.getProvider(provider);
160
+ return await llmProvider.calculateSimilarity(text1, text2);
161
+ } catch (error) {
162
+ this.logger.warn(`Primary provider similarity failed: ${error.message}`);
163
+
164
+ // Try fallback provider if available
165
+ if (this.fallbackProvider && (!provider || provider === this.defaultProvider)) {
166
+ try {
167
+ this.logger.info(`Trying fallback provider for similarity: ${this.fallbackProvider}`);
168
+ const fallbackLLM = this.getProvider(this.fallbackProvider);
169
+ return await fallbackLLM.calculateSimilarity(text1, text2);
170
+ } catch (fallbackError) {
171
+ this.logger.error(`Fallback provider similarity also failed: ${fallbackError.message}`);
172
+ }
173
+ }
174
+
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Generate query expansion suggestions
181
+ */
182
+ async expandQuery(query, options = {}) {
183
+ const {
184
+ maxExpansions = 5,
185
+ includeContextual = true,
186
+ includeSynonyms = true,
187
+ includeRelated = true
188
+ } = options;
189
+
190
+ const systemPrompt = `You are a query expansion expert. Generate relevant search query variations for research purposes.
191
+
192
+ Rules:
193
+ 1. Return only the query variations, one per line
194
+ 2. Focus on research-oriented variations
195
+ 3. Include different perspectives and angles
196
+ 4. Maintain semantic relevance
197
+ 5. Keep queries concise and searchable
198
+ 6. Maximum ${maxExpansions} variations`;
199
+
200
+ let prompt = `Original query: "${query}"\n\nGenerate ${maxExpansions} research-focused query variations:`;
201
+
202
+ if (includeContextual) {
203
+ prompt += '\n- Include contextual variations';
204
+ }
205
+ if (includeSynonyms) {
206
+ prompt += '\n- Include synonym-based variations';
207
+ }
208
+ if (includeRelated) {
209
+ prompt += '\n- Include related concept variations';
210
+ }
211
+
212
+ try {
213
+ const response = await this.generateCompletion(prompt, {
214
+ systemPrompt,
215
+ maxTokens: 300,
216
+ temperature: 0.8
217
+ });
218
+
219
+ return response
220
+ .split('\n')
221
+ .map(line => line.trim())
222
+ .filter(line => line && !line.startsWith('-') && !line.includes(':'))
223
+ .slice(0, maxExpansions);
224
+ } catch (error) {
225
+ this.logger.warn('LLM query expansion failed, using fallback', { error: error.message });
226
+ return this.fallbackQueryExpansion(query, maxExpansions);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Analyze content relevance to a topic
232
+ */
233
+ async analyzeRelevance(content, topic, options = {}) {
234
+ const { maxContentLength = 2000 } = options;
235
+
236
+ const truncatedContent = content.length > maxContentLength
237
+ ? content.substring(0, maxContentLength) + '...'
238
+ : content;
239
+
240
+ const systemPrompt = `You are a content relevance analyzer. Evaluate how relevant the given content is to the specified research topic.
241
+
242
+ Return a JSON object with:
243
+ {
244
+ "relevanceScore": 0.0-1.0,
245
+ "keyPoints": ["point1", "point2", ...],
246
+ "topicAlignment": "description of alignment",
247
+ "credibilityIndicators": ["indicator1", "indicator2", ...]
248
+ }`;
249
+
250
+ const prompt = `Research Topic: "${topic}"
251
+
252
+ Content to analyze:
253
+ ${truncatedContent}
254
+
255
+ Analyze the relevance of this content to the research topic:`;
256
+
257
+ try {
258
+ const response = await this.generateCompletion(prompt, {
259
+ systemPrompt,
260
+ maxTokens: 500,
261
+ temperature: 0.3
262
+ });
263
+
264
+ const analysis = JSON.parse(response);
265
+ return {
266
+ relevanceScore: Math.max(0, Math.min(1, analysis.relevanceScore || 0.5)),
267
+ keyPoints: analysis.keyPoints || [],
268
+ topicAlignment: analysis.topicAlignment || '',
269
+ credibilityIndicators: analysis.credibilityIndicators || []
270
+ };
271
+ } catch (error) {
272
+ this.logger.warn('LLM relevance analysis failed, using fallback', { error: error.message });
273
+ return this.fallbackRelevanceAnalysis(content, topic);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Generate research synthesis
279
+ */
280
+ async synthesizeFindings(findings, topic, options = {}) {
281
+ const { maxFindings = 10, includeConflicts = true } = options;
282
+
283
+ const limitedFindings = findings.slice(0, maxFindings);
284
+
285
+ const systemPrompt = `You are a research synthesis expert. Create a comprehensive synthesis of research findings on a given topic.
286
+
287
+ Generate a JSON response with:
288
+ {
289
+ "summary": "overall summary",
290
+ "keyInsights": ["insight1", "insight2", ...],
291
+ "themes": ["theme1", "theme2", ...],
292
+ "confidence": 0.0-1.0,
293
+ "gaps": ["gap1", "gap2", ...],
294
+ "recommendations": ["rec1", "rec2", ...]
295
+ }`;
296
+
297
+ const findingsText = limitedFindings
298
+ .map((finding, index) => `${index + 1}. ${finding.finding || finding.text || finding}`)
299
+ .join('\n');
300
+
301
+ const prompt = `Research Topic: "${topic}"
302
+
303
+ Research Findings:
304
+ ${findingsText}
305
+
306
+ Synthesize these findings into a comprehensive analysis:`;
307
+
308
+ try {
309
+ const response = await this.generateCompletion(prompt, {
310
+ systemPrompt,
311
+ maxTokens: 800,
312
+ temperature: 0.4
313
+ });
314
+
315
+ return JSON.parse(response);
316
+ } catch (error) {
317
+ this.logger.warn('LLM synthesis failed, using fallback', { error: error.message });
318
+ return this.fallbackSynthesis(findings, topic);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Fallback query expansion without LLM
324
+ */
325
+ fallbackQueryExpansion(query, maxExpansions) {
326
+ const variations = [];
327
+ const words = query.toLowerCase().split(/\s+/);
328
+
329
+ // Question variations
330
+ variations.push(`what is ${query}`);
331
+ variations.push(`how does ${query} work`);
332
+ variations.push(`${query} research`);
333
+ variations.push(`${query} analysis`);
334
+ variations.push(`latest ${query}`);
335
+
336
+ return variations.slice(0, maxExpansions);
337
+ }
338
+
339
+ /**
340
+ * Fallback relevance analysis without LLM
341
+ */
342
+ fallbackRelevanceAnalysis(content, topic) {
343
+ const topicWords = topic.toLowerCase().split(/\s+/);
344
+ const contentWords = content.toLowerCase().split(/\s+/);
345
+
346
+ const matches = topicWords.filter(word =>
347
+ contentWords.some(cWord => cWord.includes(word) || word.includes(cWord))
348
+ );
349
+
350
+ const relevanceScore = matches.length / topicWords.length;
351
+
352
+ return {
353
+ relevanceScore: Math.min(1, relevanceScore),
354
+ keyPoints: [content.substring(0, 100) + '...'],
355
+ topicAlignment: `Found ${matches.length}/${topicWords.length} topic keywords`,
356
+ credibilityIndicators: []
357
+ };
358
+ }
359
+
360
+ /**
361
+ * Fallback synthesis without LLM
362
+ */
363
+ fallbackSynthesis(findings, topic) {
364
+ return {
365
+ summary: `Collected ${findings.length} findings related to ${topic}`,
366
+ keyInsights: findings.slice(0, 3).map(f => f.finding || f.text || f),
367
+ themes: ['general research'],
368
+ confidence: 0.5,
369
+ gaps: ['Limited synthesis without LLM'],
370
+ recommendations: ['Use LLM provider for detailed synthesis']
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Check if any LLM provider is available
376
+ */
377
+ isAvailable() {
378
+ return this.providers.size > 0;
379
+ }
380
+
381
+ /**
382
+ * Get available providers metadata
383
+ */
384
+ getProvidersMetadata() {
385
+ const metadata = {};
386
+ for (const [name, provider] of this.providers) {
387
+ metadata[name] = provider.getMetadata();
388
+ }
389
+ return metadata;
390
+ }
391
+
392
+ /**
393
+ * Health check for all providers
394
+ */
395
+ async healthCheck() {
396
+ const health = {};
397
+
398
+ for (const [name, provider] of this.providers) {
399
+ try {
400
+ const isAvailable = await provider.isAvailable();
401
+ health[name] = {
402
+ available: isAvailable,
403
+ metadata: provider.getMetadata()
404
+ };
405
+ } catch (error) {
406
+ health[name] = {
407
+ available: false,
408
+ error: error.message
409
+ };
410
+ }
411
+ }
412
+
413
+ return health;
414
+ }
415
+ }
@@ -0,0 +1,97 @@
1
+ import { Logger } from '../../utils/Logger.js';
2
+
3
+ /**
4
+ * Base LLM Provider class
5
+ * Defines the interface that all LLM providers must implement
6
+ */
7
+ export class LLMProvider {
8
+ constructor(options = {}) {
9
+ this.logger = new Logger({ component: 'LLMProvider' });
10
+ this.config = options;
11
+ }
12
+
13
+ /**
14
+ * Generate a completion from the LLM
15
+ * @param {string} prompt - The input prompt
16
+ * @param {Object} options - Generation options
17
+ * @returns {Promise<string>} Generated text
18
+ */
19
+ async generateCompletion(prompt, options = {}) {
20
+ throw new Error('generateCompletion must be implemented by subclass');
21
+ }
22
+
23
+ /**
24
+ * Generate embeddings for semantic similarity
25
+ * @param {string} text - Text to embed
26
+ * @returns {Promise<number[]>} Embedding vector
27
+ */
28
+ async generateEmbedding(text) {
29
+ throw new Error('generateEmbedding must be implemented by subclass');
30
+ }
31
+
32
+ /**
33
+ * Calculate semantic similarity between two texts
34
+ * @param {string} text1 - First text
35
+ * @param {string} text2 - Second text
36
+ * @returns {Promise<number>} Similarity score (0-1)
37
+ */
38
+ async calculateSimilarity(text1, text2) {
39
+ const embedding1 = await this.generateEmbedding(text1);
40
+ const embedding2 = await this.generateEmbedding(text2);
41
+ return this.cosineSimilarity(embedding1, embedding2);
42
+ }
43
+
44
+ /**
45
+ * Calculate cosine similarity between two vectors
46
+ * @param {number[]} vec1 - First vector
47
+ * @param {number[]} vec2 - Second vector
48
+ * @returns {number} Similarity score (0-1)
49
+ */
50
+ cosineSimilarity(vec1, vec2) {
51
+ if (vec1.length !== vec2.length) {
52
+ throw new Error('Vectors must have the same length');
53
+ }
54
+
55
+ let dotProduct = 0;
56
+ let norm1 = 0;
57
+ let norm2 = 0;
58
+
59
+ for (let i = 0; i < vec1.length; i++) {
60
+ dotProduct += vec1[i] * vec2[i];
61
+ norm1 += vec1[i] * vec1[i];
62
+ norm2 += vec2[i] * vec2[i];
63
+ }
64
+
65
+ const magnitude = Math.sqrt(norm1) * Math.sqrt(norm2);
66
+ return magnitude > 0 ? dotProduct / magnitude : 0;
67
+ }
68
+
69
+ /**
70
+ * Check if the provider is available and configured
71
+ * @returns {Promise<boolean>} True if available
72
+ */
73
+ async isAvailable() {
74
+ try {
75
+ await this.generateCompletion('test', { maxTokens: 1 });
76
+ return true;
77
+ } catch (error) {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get provider metadata
84
+ * @returns {Object} Provider information
85
+ */
86
+ getMetadata() {
87
+ return {
88
+ name: this.constructor.name,
89
+ config: this.config,
90
+ capabilities: {
91
+ completion: true,
92
+ embedding: false,
93
+ similarity: false
94
+ }
95
+ };
96
+ }
97
+ }
@@ -0,0 +1,127 @@
1
+ import { LLMProvider } from './LLMProvider.js';
2
+
3
+ /**
4
+ * OpenAI API Provider
5
+ * Implements LLM operations using OpenAI's GPT models
6
+ */
7
+ export class OpenAIProvider extends LLMProvider {
8
+ constructor(options = {}) {
9
+ super(options);
10
+
11
+ this.apiKey = options.apiKey || process.env.OPENAI_API_KEY;
12
+ this.baseURL = options.baseURL || 'https://api.openai.com/v1';
13
+ this.model = options.model || 'gpt-3.5-turbo';
14
+ this.embeddingModel = options.embeddingModel || 'text-embedding-ada-002';
15
+ this.timeout = options.timeout || 30000;
16
+
17
+ if (!this.apiKey) {
18
+ this.logger.warn('OpenAI API key not configured');
19
+ }
20
+ }
21
+
22
+ async generateCompletion(prompt, options = {}) {
23
+ if (!this.apiKey) {
24
+ throw new Error('OpenAI API key not configured');
25
+ }
26
+
27
+ const {
28
+ maxTokens = 1000,
29
+ temperature = 0.7,
30
+ systemPrompt = null,
31
+ stopSequences = null
32
+ } = options;
33
+
34
+ const messages = [];
35
+
36
+ if (systemPrompt) {
37
+ messages.push({ role: 'system', content: systemPrompt });
38
+ }
39
+
40
+ messages.push({ role: 'user', content: prompt });
41
+
42
+ try {
43
+ const response = await fetch(`${this.baseURL}/chat/completions`, {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Authorization': `Bearer ${this.apiKey}`,
47
+ 'Content-Type': 'application/json'
48
+ },
49
+ body: JSON.stringify({
50
+ model: this.model,
51
+ messages,
52
+ max_tokens: maxTokens,
53
+ temperature,
54
+ stop: stopSequences
55
+ }),
56
+ signal: AbortSignal.timeout(this.timeout)
57
+ });
58
+
59
+ if (!response.ok) {
60
+ const errorData = await response.json().catch(() => ({}));
61
+ throw new Error(`OpenAI API error: ${response.status} - ${errorData.error?.message || response.statusText}`);
62
+ }
63
+
64
+ const data = await response.json();
65
+
66
+ if (!data.choices || data.choices.length === 0) {
67
+ throw new Error('No completion generated');
68
+ }
69
+
70
+ return data.choices[0].message.content.trim();
71
+ } catch (error) {
72
+ this.logger.error('OpenAI completion failed', { error: error.message });
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ async generateEmbedding(text) {
78
+ if (!this.apiKey) {
79
+ throw new Error('OpenAI API key not configured');
80
+ }
81
+
82
+ try {
83
+ const response = await fetch(`${this.baseURL}/embeddings`, {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Authorization': `Bearer ${this.apiKey}`,
87
+ 'Content-Type': 'application/json'
88
+ },
89
+ body: JSON.stringify({
90
+ model: this.embeddingModel,
91
+ input: text
92
+ }),
93
+ signal: AbortSignal.timeout(this.timeout)
94
+ });
95
+
96
+ if (!response.ok) {
97
+ const errorData = await response.json().catch(() => ({}));
98
+ throw new Error(`OpenAI API error: ${response.status} - ${errorData.error?.message || response.statusText}`);
99
+ }
100
+
101
+ const data = await response.json();
102
+
103
+ if (!data.data || data.data.length === 0) {
104
+ throw new Error('No embedding generated');
105
+ }
106
+
107
+ return data.data[0].embedding;
108
+ } catch (error) {
109
+ this.logger.error('OpenAI embedding failed', { error: error.message });
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ getMetadata() {
115
+ return {
116
+ ...super.getMetadata(),
117
+ name: 'OpenAI',
118
+ model: this.model,
119
+ embeddingModel: this.embeddingModel,
120
+ capabilities: {
121
+ completion: true,
122
+ embedding: true,
123
+ similarity: true
124
+ }
125
+ };
126
+ }
127
+ }