codesummary 1.2.1 → 1.2.2
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/CHANGELOG.md +26 -213
- package/README.md +61 -395
- package/features.md +25 -386
- package/package.json +13 -17
- package/src/ai/errors.js +85 -0
- package/src/ai/featureFlags.js +8 -0
- package/src/ai/promptTemplates.js +337 -0
- package/src/ai/providerClient.js +81 -0
- package/src/ai/providers/ollama.js +92 -0
- package/src/ai/providers/openaiCompatible.js +96 -0
- package/src/analysis/repositorySignals.js +196 -0
- package/src/cli.js +819 -77
- package/src/configManager.js +21 -0
- package/src/graph/adapters/baseAdapter.js +24 -0
- package/src/graph/adapters/javascriptAdapter.js +53 -0
- package/src/graph/adapters/pythonAdapter.js +77 -0
- package/src/graph/graphEngine.js +151 -0
- package/src/graph/graphMetrics.js +79 -0
- package/src/graph/graphSchema.js +30 -0
- package/src/graph/universalExtractor.js +29 -0
- package/src/llmGenerator.js +723 -8
- package/src/pdfGenerator.js +1189 -275
- package/src/renderers/llmSummaryRenderer.js +14 -0
- package/src/renderers/pdfThemeRenderer.js +685 -0
- package/src/scanner.js +115 -8
- package/rag-schema.json +0 -114
- package/src/ragConfig.js +0 -369
- package/src/ragGenerator.js +0 -1740
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
export function buildSemanticClustersPrompt(context) {
|
|
2
|
+
const compactClusters = context.semanticClusters.map(cluster => ({
|
|
3
|
+
name: cluster.name,
|
|
4
|
+
files: cluster.files.slice(0, 12)
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
role: 'system',
|
|
10
|
+
content:
|
|
11
|
+
'You are a software architecture assistant. ' +
|
|
12
|
+
'Return only valid JSON. Do not include markdown fences.'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
role: 'user',
|
|
16
|
+
content: JSON.stringify({
|
|
17
|
+
task: 'Refine semantic repository clusters',
|
|
18
|
+
constraints: {
|
|
19
|
+
keepClusterCountCloseToInput: true,
|
|
20
|
+
maxWordsPerDescription: 24,
|
|
21
|
+
preserveFilePaths: true,
|
|
22
|
+
outputFormat: {
|
|
23
|
+
clusters: [
|
|
24
|
+
{
|
|
25
|
+
name: 'string',
|
|
26
|
+
description: 'string',
|
|
27
|
+
files: ['path1', 'path2']
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
input: {
|
|
33
|
+
projectName: context.projectName,
|
|
34
|
+
entrypoints: context.entrypoints.slice(0, 12),
|
|
35
|
+
coreModules: context.coreModules.slice(0, 12).map(item => item.path || item),
|
|
36
|
+
semanticClusters: compactClusters
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildPdfProjectIntroPrompt(context) {
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
role: 'system',
|
|
47
|
+
content:
|
|
48
|
+
'You are a software architecture assistant. ' +
|
|
49
|
+
'Return only valid JSON. Do not include markdown fences.'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
role: 'user',
|
|
53
|
+
content: JSON.stringify({
|
|
54
|
+
task: 'Generate a concise project context block for a technical PDF report',
|
|
55
|
+
constraints: {
|
|
56
|
+
projectAgnostic: true,
|
|
57
|
+
evidenceOnly: true,
|
|
58
|
+
avoidDomainAssumptions: true,
|
|
59
|
+
fallbackWhenMissingEvidence: 'Not detected from available repository signals',
|
|
60
|
+
maxWordsPerField: 80,
|
|
61
|
+
tone: 'neutral technical',
|
|
62
|
+
avoidMarketingLanguage: true,
|
|
63
|
+
outputFormat: {
|
|
64
|
+
overview: 'string',
|
|
65
|
+
primaryPurpose: 'string',
|
|
66
|
+
keyComponents: ['string'],
|
|
67
|
+
suggestedReadingPath: ['string']
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
input: {
|
|
71
|
+
projectName: context.projectName,
|
|
72
|
+
totalFiles: context.totalFiles,
|
|
73
|
+
selectedExtensions: context.selectedExtensions,
|
|
74
|
+
topExtensions: context.topExtensions,
|
|
75
|
+
entrypoints: context.entrypoints || [],
|
|
76
|
+
coreModules: context.coreModules || [],
|
|
77
|
+
semanticClusters: context.semanticClusters || []
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildPdfArchitectureInsightsPrompt(context) {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
role: 'system',
|
|
88
|
+
content:
|
|
89
|
+
'You are a software architecture assistant. ' +
|
|
90
|
+
'Return only valid JSON. Do not include markdown fences.'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
role: 'user',
|
|
94
|
+
content: JSON.stringify({
|
|
95
|
+
task: 'Generate Architecture & Design Patterns insights for a technical PDF report',
|
|
96
|
+
constraints: {
|
|
97
|
+
projectAgnostic: true,
|
|
98
|
+
evidenceOnly: true,
|
|
99
|
+
avoidDomainAssumptions: true,
|
|
100
|
+
fallbackWhenMissingEvidence: 'Not detected from available repository signals',
|
|
101
|
+
language: 'English',
|
|
102
|
+
maxSentenceLength: 220,
|
|
103
|
+
maxItemsPerList: 8,
|
|
104
|
+
avoidSpeculationWithoutEvidence: true,
|
|
105
|
+
outputFormat: {
|
|
106
|
+
structuralPatterns: ['string'],
|
|
107
|
+
implementationParadigm: ['string'],
|
|
108
|
+
couplingPoints: ['string']
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
input: context
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildPdfOperationsInsightsPrompt(context) {
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
role: 'system',
|
|
121
|
+
content:
|
|
122
|
+
'You are a software architecture assistant. ' +
|
|
123
|
+
'Return only valid JSON. Do not include markdown fences.'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
role: 'user',
|
|
127
|
+
content: JSON.stringify({
|
|
128
|
+
task: 'Generate Data/State lifecycle + configuration strategy + language breakdown insights for a technical PDF report',
|
|
129
|
+
constraints: {
|
|
130
|
+
projectAgnostic: true,
|
|
131
|
+
evidenceOnly: true,
|
|
132
|
+
avoidDomainAssumptions: true,
|
|
133
|
+
fallbackWhenMissingEvidence: 'Not detected from available repository signals',
|
|
134
|
+
language: 'English',
|
|
135
|
+
maxSentenceLength: 220,
|
|
136
|
+
maxItemsPerList: 10,
|
|
137
|
+
outputFormat: {
|
|
138
|
+
dataAndStateLifecycle: {
|
|
139
|
+
inboundOutbound: ['string'],
|
|
140
|
+
criticalTransformations: ['string'],
|
|
141
|
+
stateManagement: ['string']
|
|
142
|
+
},
|
|
143
|
+
configurationAndEnvironmentStrategy: {
|
|
144
|
+
configurationHierarchy: ['string'],
|
|
145
|
+
infrastructureDependencies: ['string']
|
|
146
|
+
},
|
|
147
|
+
languageSpecificBreakdown: {
|
|
148
|
+
responsibilityDistribution: ['string'],
|
|
149
|
+
interoperability: ['string']
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
input: context
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildPdfMaintenanceAndSecurityPrompt(context) {
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
role: 'system',
|
|
163
|
+
content:
|
|
164
|
+
'You are a software architecture assistant. ' +
|
|
165
|
+
'Return only valid JSON. Do not include markdown fences.'
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
role: 'user',
|
|
169
|
+
content: JSON.stringify({
|
|
170
|
+
task: 'Generate Onboarding/Maintenance guidance plus Security/Compliance insights for a technical PDF report',
|
|
171
|
+
constraints: {
|
|
172
|
+
projectAgnostic: true,
|
|
173
|
+
evidenceOnly: true,
|
|
174
|
+
avoidDomainAssumptions: true,
|
|
175
|
+
fallbackWhenMissingEvidence: 'Not detected from available repository signals',
|
|
176
|
+
language: 'English',
|
|
177
|
+
maxSentenceLength: 220,
|
|
178
|
+
maxItemsPerList: 10,
|
|
179
|
+
outputFormat: {
|
|
180
|
+
onboardingAndMaintenanceGuide: {
|
|
181
|
+
complexityHotspots: ['string'],
|
|
182
|
+
modificationGuide: ['string'],
|
|
183
|
+
domainGlossary: ['string']
|
|
184
|
+
},
|
|
185
|
+
securityAndComplianceSurface: {
|
|
186
|
+
exposureSurface: ['string'],
|
|
187
|
+
sensitiveDataHandling: ['string']
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
input: context
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function buildPdfVisualInsightsPrompt(context) {
|
|
198
|
+
return [
|
|
199
|
+
{
|
|
200
|
+
role: 'system',
|
|
201
|
+
content:
|
|
202
|
+
'You are a software architecture assistant. ' +
|
|
203
|
+
'Return only valid JSON. Do not include markdown fences.'
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
role: 'user',
|
|
207
|
+
content: JSON.stringify({
|
|
208
|
+
task: 'Generate text-based visualisations for a technical PDF report',
|
|
209
|
+
constraints: {
|
|
210
|
+
projectAgnostic: true,
|
|
211
|
+
evidenceOnly: true,
|
|
212
|
+
avoidDomainAssumptions: true,
|
|
213
|
+
fallbackWhenMissingEvidence: 'Not detected from available repository signals',
|
|
214
|
+
language: 'English',
|
|
215
|
+
maxLinesPerBlock: 40,
|
|
216
|
+
outputFormat: {
|
|
217
|
+
semanticDirectoryTree: ['string'],
|
|
218
|
+
dependencyTextGraph: ['string']
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
input: context
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function buildPdfApplicabilityPrompt(context) {
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
role: 'system',
|
|
231
|
+
content:
|
|
232
|
+
'You are a software architecture assistant. ' +
|
|
233
|
+
'Return only valid JSON. Do not include markdown fences.'
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
role: 'user',
|
|
237
|
+
content: JSON.stringify({
|
|
238
|
+
task: 'Explain why the generated architecture analysis is useful across project categories',
|
|
239
|
+
constraints: {
|
|
240
|
+
projectAgnostic: true,
|
|
241
|
+
evidenceOnly: true,
|
|
242
|
+
avoidDomainAssumptions: true,
|
|
243
|
+
fallbackWhenMissingEvidence: 'Not detected from available repository signals',
|
|
244
|
+
language: 'English',
|
|
245
|
+
maxSentenceLength: 220,
|
|
246
|
+
maxItemsPerList: 8,
|
|
247
|
+
outputFormat: {
|
|
248
|
+
whyItHelpsAcrossProjects: {
|
|
249
|
+
infrastructureAsCode: ['string'],
|
|
250
|
+
dataPlatforms: ['string'],
|
|
251
|
+
frontendApplications: ['string'],
|
|
252
|
+
backendServices: ['string'],
|
|
253
|
+
genericTransferableValue: ['string']
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
input: context
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function buildPdfExecutiveBriefPrompt(context) {
|
|
264
|
+
return [
|
|
265
|
+
{
|
|
266
|
+
role: 'system',
|
|
267
|
+
content:
|
|
268
|
+
'You are a Principal Technical Writer, elite Software Architect, and editorial design director. ' +
|
|
269
|
+
'Return only valid JSON. Do not include markdown fences.'
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
role: 'user',
|
|
273
|
+
content: JSON.stringify({
|
|
274
|
+
task: 'Generate a high-impact, infographic-ready technical executive brief for a PDF',
|
|
275
|
+
constraints: {
|
|
276
|
+
language: 'English',
|
|
277
|
+
projectAgnostic: true,
|
|
278
|
+
evidenceOnly: true,
|
|
279
|
+
avoidDomainAssumptions: true,
|
|
280
|
+
style: 'clinical, concise, high-signal',
|
|
281
|
+
paragraphLimitLines: 3,
|
|
282
|
+
maxItemsPerList: 5,
|
|
283
|
+
fallbackWhenMissingEvidence: 'Not detected from available repository signals',
|
|
284
|
+
sections: [
|
|
285
|
+
'Executive Summary',
|
|
286
|
+
'Architecture Blueprint',
|
|
287
|
+
'Data Flow',
|
|
288
|
+
'Onboarding Quick Guide',
|
|
289
|
+
'Risk & Hotspots'
|
|
290
|
+
],
|
|
291
|
+
outputFormat: {
|
|
292
|
+
executiveSummary: {
|
|
293
|
+
purpose: 'string (exactly 2 sentences)',
|
|
294
|
+
killerFeatures: ['string (exactly 3 items)']
|
|
295
|
+
},
|
|
296
|
+
architectureBlueprint: [
|
|
297
|
+
{
|
|
298
|
+
pattern: 'string',
|
|
299
|
+
primaryModule: 'string',
|
|
300
|
+
value: 'string'
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
dataFlow: {
|
|
304
|
+
primaryFlow: 'string (A -> B -> C format)',
|
|
305
|
+
keyFlows: ['string']
|
|
306
|
+
},
|
|
307
|
+
onboardingQuickGuide: [
|
|
308
|
+
{
|
|
309
|
+
goal: 'string',
|
|
310
|
+
modify: 'string'
|
|
311
|
+
}
|
|
312
|
+
],
|
|
313
|
+
riskHotspots: [
|
|
314
|
+
{
|
|
315
|
+
severity: 'string',
|
|
316
|
+
file: 'string',
|
|
317
|
+
reason: 'string'
|
|
318
|
+
}
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
input: context
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export default {
|
|
329
|
+
buildSemanticClustersPrompt,
|
|
330
|
+
buildPdfProjectIntroPrompt,
|
|
331
|
+
buildPdfArchitectureInsightsPrompt,
|
|
332
|
+
buildPdfOperationsInsightsPrompt,
|
|
333
|
+
buildPdfMaintenanceAndSecurityPrompt,
|
|
334
|
+
buildPdfVisualInsightsPrompt,
|
|
335
|
+
buildPdfApplicabilityPrompt,
|
|
336
|
+
buildPdfExecutiveBriefPrompt
|
|
337
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import OpenAiCompatibleProvider from './providers/openaiCompatible.js';
|
|
2
|
+
import OllamaProvider from './providers/ollama.js';
|
|
3
|
+
import { AiProviderError, normalizeAiError } from './errors.js';
|
|
4
|
+
|
|
5
|
+
export default class ProviderClient {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.provider = this.createProvider(config);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
createProvider(config) {
|
|
12
|
+
const providerName = (config.provider || 'openai-compatible').toLowerCase();
|
|
13
|
+
if (providerName === 'ollama') {
|
|
14
|
+
return new OllamaProvider(config);
|
|
15
|
+
}
|
|
16
|
+
return new OpenAiCompatibleProvider(config);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async chat(messages, options = {}) {
|
|
20
|
+
const maxRetries = Number.isInteger(options.maxRetries)
|
|
21
|
+
? options.maxRetries
|
|
22
|
+
: (Number.isInteger(this.config.maxRetries) ? this.config.maxRetries : 2);
|
|
23
|
+
const initialBackoffMs = Number.isInteger(options.retryBackoffMs)
|
|
24
|
+
? options.retryBackoffMs
|
|
25
|
+
: (Number.isInteger(this.config.retryBackoffMs) ? this.config.retryBackoffMs : 500);
|
|
26
|
+
const maxBackoffMs = Number.isInteger(options.maxBackoffMs)
|
|
27
|
+
? options.maxBackoffMs
|
|
28
|
+
: (Number.isInteger(options.maxBackoffMaxMs)
|
|
29
|
+
? options.maxBackoffMaxMs
|
|
30
|
+
: (Number.isInteger(this.config.maxBackoffMs) ? this.config.maxBackoffMs : 5000));
|
|
31
|
+
|
|
32
|
+
const totalAttempts = Math.max(1, maxRetries + 1);
|
|
33
|
+
let lastError = null;
|
|
34
|
+
|
|
35
|
+
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
36
|
+
try {
|
|
37
|
+
const response = await this.provider.chat(messages, options);
|
|
38
|
+
return {
|
|
39
|
+
...response,
|
|
40
|
+
attempts: attempt
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const normalized = normalizeAiError(error, { provider: this.getModelInfo().provider });
|
|
44
|
+
lastError = normalized;
|
|
45
|
+
const canRetry = normalized.retryable && attempt < totalAttempts;
|
|
46
|
+
|
|
47
|
+
if (!canRetry) {
|
|
48
|
+
throw new AiProviderError(normalized.message, {
|
|
49
|
+
...normalized,
|
|
50
|
+
details: {
|
|
51
|
+
...(normalized.details || {}),
|
|
52
|
+
attempts: attempt
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const delay = Math.min(maxBackoffMs, initialBackoffMs * Math.pow(2, attempt - 1));
|
|
58
|
+
const jitter = Math.floor(delay * 0.2 * Math.random());
|
|
59
|
+
await this.sleep(delay + jitter);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw lastError || new AiProviderError('AI request failed', {
|
|
64
|
+
code: 'ai_error',
|
|
65
|
+
retryable: false,
|
|
66
|
+
provider: this.getModelInfo().provider
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async healthCheck() {
|
|
71
|
+
return this.provider.healthCheck();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getModelInfo() {
|
|
75
|
+
return this.provider.getModelInfo();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
sleep(ms) {
|
|
79
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { AiProviderError, createHttpAiError, normalizeAiError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
export default class OllamaProvider {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
this.providerName = 'ollama';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async chat(messages, options = {}) {
|
|
10
|
+
const timeoutMs = options.timeoutMs || this.config.timeoutMs || 30000;
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
13
|
+
const baseUrl = (this.config.baseUrl || 'http://localhost:11434').replace(/\/+$/, '');
|
|
14
|
+
const url = `${baseUrl}/api/chat`;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
signal: controller.signal,
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json'
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
model: options.model || this.config.model || 'llama3.1',
|
|
25
|
+
messages,
|
|
26
|
+
stream: false
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const errorText = await response.text();
|
|
32
|
+
throw createHttpAiError(this.providerName, response.status, errorText);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await response.json();
|
|
36
|
+
const content = data?.message?.content;
|
|
37
|
+
if (!content) {
|
|
38
|
+
throw new AiProviderError('Ollama response missing content', {
|
|
39
|
+
code: 'invalid_response',
|
|
40
|
+
retryable: false,
|
|
41
|
+
provider: this.providerName
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { content, raw: data };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw normalizeAiError(error, { provider: this.providerName });
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async healthCheck() {
|
|
53
|
+
const timeoutMs = Math.min(this.config.timeoutMs || 30000, 7000);
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
56
|
+
const baseUrl = (this.config.baseUrl || 'http://localhost:11434').replace(/\/+$/, '');
|
|
57
|
+
const url = `${baseUrl}/api/tags`;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(url, {
|
|
61
|
+
method: 'GET',
|
|
62
|
+
signal: controller.signal
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
provider: this.providerName,
|
|
70
|
+
error: createHttpAiError(this.providerName, response.status, text)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { ok: true, provider: this.providerName };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
provider: this.providerName,
|
|
79
|
+
error: normalizeAiError(error, { provider: this.providerName })
|
|
80
|
+
};
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getModelInfo() {
|
|
87
|
+
return {
|
|
88
|
+
provider: 'ollama',
|
|
89
|
+
model: this.config.model || 'llama3.1'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { AiProviderError, createHttpAiError, normalizeAiError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
export default class OpenAiCompatibleProvider {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
this.providerName = 'openai-compatible';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async chat(messages, options = {}) {
|
|
10
|
+
const timeoutMs = options.timeoutMs || this.config.timeoutMs || 30000;
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
13
|
+
const baseUrl = (this.config.baseUrl || '').replace(/\/+$/, '');
|
|
14
|
+
const url = `${baseUrl}/chat/completions`;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
signal: controller.signal,
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {})
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
model: options.model || this.config.model,
|
|
26
|
+
messages,
|
|
27
|
+
temperature: options.temperature ?? 0.2
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const errorText = await response.text();
|
|
33
|
+
throw createHttpAiError(this.providerName, response.status, errorText);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
const content = data?.choices?.[0]?.message?.content;
|
|
38
|
+
if (!content) {
|
|
39
|
+
throw new AiProviderError('OpenAI-compatible response missing content', {
|
|
40
|
+
code: 'invalid_response',
|
|
41
|
+
retryable: false,
|
|
42
|
+
provider: this.providerName
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return { content, raw: data };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw normalizeAiError(error, { provider: this.providerName });
|
|
48
|
+
} finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async healthCheck() {
|
|
54
|
+
const timeoutMs = Math.min(this.config.timeoutMs || 30000, 7000);
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
57
|
+
const baseUrl = (this.config.baseUrl || '').replace(/\/+$/, '');
|
|
58
|
+
const url = `${baseUrl}/models`;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(url, {
|
|
62
|
+
method: 'GET',
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
headers: {
|
|
65
|
+
...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {})
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const text = await response.text();
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
provider: this.providerName,
|
|
74
|
+
error: createHttpAiError(this.providerName, response.status, text)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { ok: true, provider: this.providerName };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
provider: this.providerName,
|
|
83
|
+
error: normalizeAiError(error, { provider: this.providerName })
|
|
84
|
+
};
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getModelInfo() {
|
|
91
|
+
return {
|
|
92
|
+
provider: 'openai-compatible',
|
|
93
|
+
model: this.config.model || 'unknown'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|