codereview-aia 0.1.4 → 0.2.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.
- package/dist/clients/openRouterClient.js +308 -466
- package/dist/clients/openRouterClient.js.map +1 -1
- package/dist/clients/utils/promptFormatter.js +39 -3
- package/dist/clients/utils/promptFormatter.js.map +1 -1
- package/dist/clients/utils/tokenCounter.js +10 -35
- package/dist/clients/utils/tokenCounter.js.map +1 -1
- package/dist/core/ConfigurationService.js +2 -2
- package/dist/core/ConfigurationService.js.map +1 -1
- package/dist/runtime/cliEntry.js +11 -1
- package/dist/runtime/cliEntry.js.map +1 -1
- package/dist/runtime/fileCollector.js +18 -17
- package/dist/runtime/fileCollector.js.map +1 -1
- package/dist/runtime/preprod/batchStreamer.d.ts +54 -0
- package/dist/runtime/preprod/batchStreamer.js +159 -0
- package/dist/runtime/preprod/batchStreamer.js.map +1 -0
- package/dist/runtime/preprod/crEdgeServiceClient.d.ts +30 -0
- package/dist/runtime/preprod/crEdgeServiceClient.js +133 -0
- package/dist/runtime/preprod/crEdgeServiceClient.js.map +1 -0
- package/dist/runtime/preprod/progressTracker.d.ts +21 -0
- package/dist/runtime/preprod/progressTracker.js +80 -0
- package/dist/runtime/preprod/progressTracker.js.map +1 -0
- package/dist/runtime/preprod/webCheck.d.ts +1 -1
- package/dist/runtime/preprod/webCheck.js +7 -3
- package/dist/runtime/preprod/webCheck.js.map +1 -1
- package/dist/runtime/reviewPipeline.d.ts +14 -0
- package/dist/runtime/reviewPipeline.js +88 -18
- package/dist/runtime/reviewPipeline.js.map +1 -1
- package/dist/runtime/runAiCodeReview.d.ts +8 -0
- package/dist/runtime/runAiCodeReview.js +56 -20
- package/dist/runtime/runAiCodeReview.js.map +1 -1
- package/dist/runtime/runtimeConfig.d.ts +1 -1
- package/dist/runtime/runtimeConfig.js +1 -1
- package/dist/runtime/runtimeConfig.js.map +1 -1
- package/dist/runtime/ui/RuntimeApp.js +114 -11
- package/dist/runtime/ui/RuntimeApp.js.map +1 -1
- package/dist/runtime/ui/screens/ModeSelection.d.ts +1 -0
- package/dist/runtime/ui/screens/ModeSelection.js +97 -28
- package/dist/runtime/ui/screens/ModeSelection.js.map +1 -1
- package/dist/runtime/ui/screens/ProgressScreen.d.ts +11 -2
- package/dist/runtime/ui/screens/ProgressScreen.js +28 -6
- package/dist/runtime/ui/screens/ProgressScreen.js.map +1 -1
- package/dist/runtime/ui/screens/ResultsScreen.js +6 -1
- package/dist/runtime/ui/screens/ResultsScreen.js.map +1 -1
- package/dist/utils/fileFilters.js +13 -1
- package/dist/utils/fileFilters.js.map +1 -1
- package/package.json +2 -1
|
@@ -65,6 +65,9 @@ const promptLoader_1 = require("./utils/promptLoader");
|
|
|
65
65
|
const tokenCounter_1 = require("./utils/tokenCounter");
|
|
66
66
|
// Track if we've initialized a model successfully
|
|
67
67
|
let modelInitialized = false;
|
|
68
|
+
const OPENROUTER_FALLBACK_MODEL = process.env.AI_CODE_REVIEW_FALLBACK_MODEL || 'openrouter:x-ai/grok-4-fast';
|
|
69
|
+
const MAX_ATTEMPTS_PER_MODEL = 3;
|
|
70
|
+
const RETRY_BASE_DELAY_MS = 750;
|
|
68
71
|
/**
|
|
69
72
|
* Determine appropriate max_tokens based on review type and context
|
|
70
73
|
*/
|
|
@@ -153,6 +156,261 @@ async function initializeAnyOpenRouterModel() {
|
|
|
153
156
|
return false;
|
|
154
157
|
}
|
|
155
158
|
}
|
|
159
|
+
function extractModelName(identifier) {
|
|
160
|
+
const trimmed = identifier.trim();
|
|
161
|
+
if (!trimmed) {
|
|
162
|
+
return trimmed;
|
|
163
|
+
}
|
|
164
|
+
return trimmed.includes(':') ? trimmed.split(':', 2)[1] : trimmed;
|
|
165
|
+
}
|
|
166
|
+
function getModelPriorityList(options) {
|
|
167
|
+
const configured = options?.model || (0, ConfigurationService_1.getConfig)().model || 'openrouter:moonshotai/kimi-k2-thinking';
|
|
168
|
+
const preferred = extractModelName(configured);
|
|
169
|
+
const fallback = extractModelName(OPENROUTER_FALLBACK_MODEL);
|
|
170
|
+
const models = [preferred];
|
|
171
|
+
if (fallback && !models.includes(fallback)) {
|
|
172
|
+
models.push(fallback);
|
|
173
|
+
}
|
|
174
|
+
return models.filter(Boolean);
|
|
175
|
+
}
|
|
176
|
+
function shouldEnableReasoning(modelName) {
|
|
177
|
+
return modelName.toLowerCase().includes('moonshotai/kimi');
|
|
178
|
+
}
|
|
179
|
+
function isRetryableError(error) {
|
|
180
|
+
if (!error) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (error instanceof apiErrorHandler_1.TokenLimitError) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (error instanceof apiErrorHandler_1.ApiError) {
|
|
187
|
+
if (typeof error.statusCode === 'number' && error.statusCode >= 500) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
const detailString = typeof error.details === 'string'
|
|
191
|
+
? error.details.toLowerCase()
|
|
192
|
+
: error.details
|
|
193
|
+
? JSON.stringify(error.details).toLowerCase()
|
|
194
|
+
: '';
|
|
195
|
+
const message = error.message?.toLowerCase?.() ?? '';
|
|
196
|
+
if (detailString.includes('upstream request failed') || message.includes('upstream request failed')) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
if (error instanceof Error) {
|
|
202
|
+
const message = error.message.toLowerCase();
|
|
203
|
+
return (message.includes('network') ||
|
|
204
|
+
message.includes('fetch failed') ||
|
|
205
|
+
message.includes('upstream request failed') ||
|
|
206
|
+
message.includes('timeout'));
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
function delay(ms) {
|
|
211
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
212
|
+
}
|
|
213
|
+
async function executeWithRetries(buildPayload, meta) {
|
|
214
|
+
const models = getModelPriorityList(meta.options);
|
|
215
|
+
let lastError;
|
|
216
|
+
for (let modelIdx = 0; modelIdx < models.length; modelIdx += 1) {
|
|
217
|
+
const modelName = models[modelIdx];
|
|
218
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS_PER_MODEL; attempt += 1) {
|
|
219
|
+
try {
|
|
220
|
+
const payload = buildPayload(modelName);
|
|
221
|
+
return await performOpenRouterRequest(modelName, payload, meta);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (error instanceof apiErrorHandler_1.TokenLimitError) {
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
const shouldRetry = isRetryableError(error) && attempt < MAX_ATTEMPTS_PER_MODEL;
|
|
228
|
+
if (!shouldRetry) {
|
|
229
|
+
lastError = error;
|
|
230
|
+
logger_1.default.warn(`[OpenRouter] ${modelName} failed (attempt ${attempt}/${MAX_ATTEMPTS_PER_MODEL}): ${error instanceof Error ? error.message : String(error)}`);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
const delayMs = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
234
|
+
logger_1.default.warn(`[OpenRouter] ${modelName} failed (attempt ${attempt}/${MAX_ATTEMPTS_PER_MODEL}): ${error instanceof Error ? error.message : String(error)}. Retrying in ${Math.round(delayMs)}ms`);
|
|
235
|
+
await delay(delayMs);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (modelIdx < models.length - 1) {
|
|
239
|
+
logger_1.default.warn(`[OpenRouter] Falling back to secondary model after repeated failures with ${models[modelIdx]}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
throw lastError instanceof Error
|
|
243
|
+
? lastError
|
|
244
|
+
: new apiErrorHandler_1.ApiError('All configured OpenRouter models failed after retries');
|
|
245
|
+
}
|
|
246
|
+
async function performOpenRouterRequest(modelName, payload, meta) {
|
|
247
|
+
let cost;
|
|
248
|
+
const finalPayload = (0, openrouterProxy_1.withProxyMetadata)({
|
|
249
|
+
...payload,
|
|
250
|
+
model: modelName,
|
|
251
|
+
provider: {
|
|
252
|
+
sort: 'throughput',
|
|
253
|
+
},
|
|
254
|
+
...(shouldEnableReasoning(modelName) ? { reasoning: { enabled: true } } : {}),
|
|
255
|
+
});
|
|
256
|
+
const response = await fetch((0, openrouterProxy_1.resolveOpenRouterProxyUrl)(), {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: (0, openrouterProxy_1.buildOpenRouterProxyHeaders)(),
|
|
259
|
+
body: JSON.stringify(finalPayload),
|
|
260
|
+
});
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
let errorData;
|
|
263
|
+
try {
|
|
264
|
+
errorData = await response.json();
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// ignore
|
|
268
|
+
}
|
|
269
|
+
const errorMessage = JSON.stringify(errorData || {});
|
|
270
|
+
const lowered = errorMessage.toLowerCase();
|
|
271
|
+
if (lowered.includes('token') &&
|
|
272
|
+
(lowered.includes('limit') || lowered.includes('exceed') || lowered.includes('too long') || lowered.includes('too many'))) {
|
|
273
|
+
const { countTokens } = await Promise.resolve().then(() => __importStar(require('../tokenizers')));
|
|
274
|
+
const tokenCount = countTokens(meta.prompt, modelName);
|
|
275
|
+
throw new apiErrorHandler_1.TokenLimitError(`Token limit exceeded for model ${modelName}. Content has ${tokenCount.toLocaleString()} tokens. Consider using --multi-pass flag for large codebases.`, tokenCount, undefined, response.status, errorData);
|
|
276
|
+
}
|
|
277
|
+
throw new apiErrorHandler_1.ApiError(`OpenRouter API error: ${errorMessage}`, response.status, errorData);
|
|
278
|
+
}
|
|
279
|
+
const data = await response.json();
|
|
280
|
+
const finishReason = data.choices?.[0]?.finish_reason;
|
|
281
|
+
const responseContent = data.choices?.[0]?.message?.content || '';
|
|
282
|
+
const truncated = isResponseTruncated(responseContent, finishReason);
|
|
283
|
+
logger_1.default.debug(`[OpenRouter] API Response structure:`, {
|
|
284
|
+
hasChoices: !!data.choices,
|
|
285
|
+
choicesLength: data.choices?.length || 0,
|
|
286
|
+
firstChoiceExists: !!(data.choices && data.choices[0]),
|
|
287
|
+
firstChoiceMessage: data.choices?.[0]?.message ? 'exists' : 'missing',
|
|
288
|
+
contentExists: !!responseContent,
|
|
289
|
+
contentLength: responseContent.length,
|
|
290
|
+
contentPreview: responseContent.substring(0, 100) || 'N/A',
|
|
291
|
+
finishReason: finishReason || 'unknown',
|
|
292
|
+
isTruncated: truncated,
|
|
293
|
+
maxTokensUsed: getMaxTokensForReviewType(meta.reviewType, meta.isConsolidation) || 'unlimited',
|
|
294
|
+
fullResponse: JSON.stringify(data).substring(0, 500) + '...',
|
|
295
|
+
});
|
|
296
|
+
if (!responseContent || responseContent.trim().length === 0) {
|
|
297
|
+
logger_1.default.error(`[OpenRouter] CRITICAL: API returned successful response but content is empty!`);
|
|
298
|
+
logger_1.default.error(`[OpenRouter] Response details:`, {
|
|
299
|
+
modelName,
|
|
300
|
+
promptLength: meta.prompt.length,
|
|
301
|
+
responseStatus: response.status,
|
|
302
|
+
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
303
|
+
fullApiResponse: JSON.stringify(data, null, 2),
|
|
304
|
+
});
|
|
305
|
+
throw new apiErrorHandler_1.ApiError(`OpenRouter API returned empty content for model ${modelName}`);
|
|
306
|
+
}
|
|
307
|
+
if (finishReason === 'length' || finishReason === 'MAX_TOKENS') {
|
|
308
|
+
logger_1.default.warn(`[OpenRouter] WARNING: Response truncated due to token limit!`);
|
|
309
|
+
logger_1.default.warn(`[OpenRouter] Truncation details:`, {
|
|
310
|
+
modelName,
|
|
311
|
+
finishReason,
|
|
312
|
+
contentLength: responseContent.length,
|
|
313
|
+
maxTokensRequested: getMaxTokensForReviewType(meta.reviewType, meta.isConsolidation) || 'unlimited',
|
|
314
|
+
reviewType: meta.reviewType,
|
|
315
|
+
isConsolidation: meta.isConsolidation || false,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
else if (truncated) {
|
|
319
|
+
logger_1.default.debug(`[OpenRouter] Response appears truncated by heuristics, but finishReason is ${finishReason}. Proceeding to JSON parse.`);
|
|
320
|
+
}
|
|
321
|
+
logger_1.default.debug(`Successfully generated review with OpenRouter ${modelName}`);
|
|
322
|
+
try {
|
|
323
|
+
cost = (0, tokenCounter_1.getCostInfoFromText)(meta.prompt, responseContent, `openrouter:${modelName}`);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
logger_1.default.warn(`Failed to calculate cost information: ${error instanceof Error ? error.message : String(error)}`);
|
|
327
|
+
}
|
|
328
|
+
let structuredData = null;
|
|
329
|
+
try {
|
|
330
|
+
const jsonExtractionStrategies = [
|
|
331
|
+
() => {
|
|
332
|
+
const patterns = [
|
|
333
|
+
/```(?:json)?\s*([\s\S]*?)\s*```/,
|
|
334
|
+
/```(?:typescript|javascript|ts|js)?\s*([\s\S]*?)\s*```/,
|
|
335
|
+
];
|
|
336
|
+
for (const pattern of patterns) {
|
|
337
|
+
const match = responseContent.match(pattern);
|
|
338
|
+
if (match && match[1]) {
|
|
339
|
+
const extracted = match[1].trim();
|
|
340
|
+
if (extracted.startsWith('{') && extracted.endsWith('}')) {
|
|
341
|
+
return extracted;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
},
|
|
347
|
+
() => {
|
|
348
|
+
const startIdx = responseContent.indexOf('{');
|
|
349
|
+
if (startIdx === -1)
|
|
350
|
+
return null;
|
|
351
|
+
let depth = 0;
|
|
352
|
+
let inString = false;
|
|
353
|
+
let escapeNext = false;
|
|
354
|
+
for (let i = startIdx; i < responseContent.length; i += 1) {
|
|
355
|
+
const char = responseContent[i];
|
|
356
|
+
if (escapeNext) {
|
|
357
|
+
escapeNext = false;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (char === '\\') {
|
|
361
|
+
escapeNext = true;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (char === '"' && !escapeNext) {
|
|
365
|
+
inString = !inString;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (!inString) {
|
|
369
|
+
if (char === '{')
|
|
370
|
+
depth += 1;
|
|
371
|
+
else if (char === '}') {
|
|
372
|
+
depth -= 1;
|
|
373
|
+
if (depth === 0) {
|
|
374
|
+
return responseContent.substring(startIdx, i + 1);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (inString) {
|
|
380
|
+
return responseContent.substring(startIdx) + '"}';
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
},
|
|
384
|
+
() => responseContent,
|
|
385
|
+
];
|
|
386
|
+
for (const strategy of jsonExtractionStrategies) {
|
|
387
|
+
try {
|
|
388
|
+
const extracted = strategy();
|
|
389
|
+
if (extracted) {
|
|
390
|
+
structuredData = JSON.parse(extracted);
|
|
391
|
+
logger_1.default.debug('Successfully extracted and parsed JSON');
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
logger_1.default.debug(`JSON extraction strategy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (structuredData && !structuredData.summary && !Array.isArray(structuredData.issues)) {
|
|
400
|
+
logger_1.default.warn('Response is valid JSON but does not have the expected structure');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (parseError) {
|
|
404
|
+
logger_1.default.warn(`Failed to parse response as JSON after all recovery attempts: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
405
|
+
structuredData = null;
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
content: responseContent,
|
|
409
|
+
structuredData,
|
|
410
|
+
cost,
|
|
411
|
+
modelName,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
156
414
|
/**
|
|
157
415
|
* Generate a code review using the OpenRouter API
|
|
158
416
|
* @param fileContent Content of the file to review
|
|
@@ -164,46 +422,16 @@ async function initializeAnyOpenRouterModel() {
|
|
|
164
422
|
*/
|
|
165
423
|
async function generateOpenRouterReview(fileContent, filePath, reviewType, projectDocs, options) {
|
|
166
424
|
try {
|
|
167
|
-
// Initialize the model if we haven't already
|
|
168
425
|
if (!modelInitialized) {
|
|
169
426
|
await initializeAnyOpenRouterModel();
|
|
170
427
|
}
|
|
171
|
-
// Use the imported dependencies
|
|
172
|
-
// Get API key from environment variables
|
|
173
|
-
let content;
|
|
174
|
-
let cost;
|
|
175
|
-
// Get the language from the file extension
|
|
176
|
-
// const language = getLanguageFromExtension(filePath); // Currently unused
|
|
177
|
-
// Load the appropriate prompt template
|
|
178
428
|
const promptTemplate = await (0, promptLoader_1.loadPromptTemplate)(reviewType, options);
|
|
179
|
-
// Format the prompt
|
|
180
429
|
const prompt = (0, promptFormatter_1.formatSingleFileReviewPrompt)(promptTemplate, fileContent, filePath, projectDocs, options?.runContext);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const [, model] = consolidationModel.includes(':')
|
|
187
|
-
? consolidationModel.split(':')
|
|
188
|
-
: ['openrouter', consolidationModel];
|
|
189
|
-
modelName = model;
|
|
190
|
-
logger_1.default.debug(`[OpenRouter] Using consolidation model for single file: ${modelName}`);
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
// Regular review - use the configured model
|
|
194
|
-
const result = isOpenRouterModel();
|
|
195
|
-
modelName = result.modelName;
|
|
196
|
-
logger_1.default.debug(`[OpenRouter] Using review model for single file: ${modelName}`);
|
|
197
|
-
}
|
|
198
|
-
try {
|
|
199
|
-
logger_1.default.debug(`Generating review with OpenRouter ${modelName}...`);
|
|
200
|
-
const requestMaxTokens = getMaxTokensForReviewType(reviewType, options?.isConsolidation);
|
|
201
|
-
const payload = (0, openrouterProxy_1.withProxyMetadata)({
|
|
202
|
-
model: modelName,
|
|
203
|
-
messages: [
|
|
204
|
-
{
|
|
205
|
-
role: 'system',
|
|
206
|
-
content: `You are an expert code reviewer. Focus on providing actionable feedback. IMPORTANT: DO NOT REPEAT THE INSTRUCTIONS IN YOUR RESPONSE. DO NOT ASK FOR CODE TO REVIEW. ASSUME THE CODE IS ALREADY PROVIDED IN THE USER MESSAGE. FOCUS ONLY ON PROVIDING THE CODE REVIEW CONTENT.
|
|
430
|
+
const requestMaxTokens = getMaxTokensForReviewType(reviewType, options?.isConsolidation);
|
|
431
|
+
const messages = [
|
|
432
|
+
{
|
|
433
|
+
role: 'system',
|
|
434
|
+
content: `You are an expert code reviewer. Focus on providing actionable feedback. IMPORTANT: DO NOT REPEAT THE INSTRUCTIONS IN YOUR RESPONSE. DO NOT ASK FOR CODE TO REVIEW. ASSUME THE CODE IS ALREADY PROVIDED IN THE USER MESSAGE. FOCUS ONLY ON PROVIDING THE CODE REVIEW CONTENT.
|
|
207
435
|
|
|
208
436
|
IMPORTANT: Your response MUST be in the following JSON format:
|
|
209
437
|
|
|
@@ -233,209 +461,30 @@ IMPORTANT: Your response MUST be in the following JSON format:
|
|
|
233
461
|
}
|
|
234
462
|
|
|
235
463
|
Ensure your response is valid JSON. Do not include any text outside the JSON structure.`,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const errorData = await response.json();
|
|
253
|
-
// Check for token limit errors
|
|
254
|
-
const errorMessage = JSON.stringify(errorData).toLowerCase();
|
|
255
|
-
if (errorMessage.includes('token') &&
|
|
256
|
-
(errorMessage.includes('limit') ||
|
|
257
|
-
errorMessage.includes('exceed') ||
|
|
258
|
-
errorMessage.includes('too long') ||
|
|
259
|
-
errorMessage.includes('too many'))) {
|
|
260
|
-
// Extract token count from prompt if possible
|
|
261
|
-
const { countTokens } = await Promise.resolve().then(() => __importStar(require('../tokenizers')));
|
|
262
|
-
const tokenCount = countTokens(prompt, modelName);
|
|
263
|
-
throw new apiErrorHandler_1.TokenLimitError(`Token limit exceeded for model ${modelName}. Content has ${tokenCount.toLocaleString()} tokens. Consider using --multi-pass flag for large codebases.`, tokenCount, undefined, response.status, errorData);
|
|
264
|
-
}
|
|
265
|
-
throw new Error(`OpenRouter API error: ${JSON.stringify(errorData)}`);
|
|
266
|
-
}
|
|
267
|
-
const data = await response.json();
|
|
268
|
-
// Enhanced logging for debugging empty content issue
|
|
269
|
-
const finishReason = data.choices?.[0]?.finish_reason;
|
|
270
|
-
const responseContent = data.choices?.[0]?.message?.content || '';
|
|
271
|
-
const isTruncated = isResponseTruncated(responseContent, finishReason);
|
|
272
|
-
logger_1.default.debug(`[OpenRouter] API Response structure:`, {
|
|
273
|
-
hasChoices: !!data.choices,
|
|
274
|
-
choicesLength: data.choices?.length || 0,
|
|
275
|
-
firstChoiceExists: !!(data.choices && data.choices[0]),
|
|
276
|
-
firstChoiceMessage: data.choices?.[0]?.message ? 'exists' : 'missing',
|
|
277
|
-
contentExists: !!responseContent,
|
|
278
|
-
contentLength: responseContent.length,
|
|
279
|
-
contentPreview: responseContent.substring(0, 100) || 'N/A',
|
|
280
|
-
finishReason: finishReason || 'unknown',
|
|
281
|
-
isTruncated: isTruncated,
|
|
282
|
-
maxTokensUsed: getMaxTokensForReviewType(reviewType, options?.isConsolidation) || 'unlimited',
|
|
283
|
-
fullResponse: JSON.stringify(data).substring(0, 500) + '...',
|
|
284
|
-
});
|
|
285
|
-
if (data.choices && data.choices.length > 0) {
|
|
286
|
-
content = data.choices[0].message.content;
|
|
287
|
-
// Critical check for empty content
|
|
288
|
-
if (!content || content.trim().length === 0) {
|
|
289
|
-
logger_1.default.error(`[OpenRouter] CRITICAL: API returned successful response but content is empty!`);
|
|
290
|
-
logger_1.default.error(`[OpenRouter] Response details:`, {
|
|
291
|
-
modelName,
|
|
292
|
-
promptLength: prompt.length,
|
|
293
|
-
responseStatus: response.status,
|
|
294
|
-
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
295
|
-
fullApiResponse: JSON.stringify(data, null, 2),
|
|
296
|
-
});
|
|
297
|
-
throw new Error(`OpenRouter API returned empty content for model ${modelName}`);
|
|
298
|
-
}
|
|
299
|
-
// Check for truncated responses
|
|
300
|
-
if (isTruncated) {
|
|
301
|
-
logger_1.default.warn(`[OpenRouter] WARNING: Response appears to be truncated!`);
|
|
302
|
-
logger_1.default.warn(`[OpenRouter] Truncation details:`, {
|
|
303
|
-
modelName,
|
|
304
|
-
finishReason,
|
|
305
|
-
contentLength: content.length,
|
|
306
|
-
maxTokensRequested: getMaxTokensForReviewType(reviewType, options?.isConsolidation) || 'unlimited',
|
|
307
|
-
reviewType,
|
|
308
|
-
isConsolidation: options?.isConsolidation || false,
|
|
309
|
-
});
|
|
310
|
-
// For now, we'll continue with the truncated response but log it
|
|
311
|
-
// In the future, we could implement retry logic with shorter prompts
|
|
312
|
-
}
|
|
313
|
-
logger_1.default.debug(`Successfully generated review with OpenRouter ${modelName}`);
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
logger_1.default.error(`[OpenRouter] Invalid response format:`, JSON.stringify(data, null, 2));
|
|
317
|
-
throw new Error(`Invalid response format from OpenRouter ${modelName}`);
|
|
318
|
-
}
|
|
319
|
-
// Calculate cost information
|
|
320
|
-
try {
|
|
321
|
-
cost = (0, tokenCounter_1.getCostInfoFromText)(prompt, content, `openrouter:${modelName}`);
|
|
322
|
-
}
|
|
323
|
-
catch (error) {
|
|
324
|
-
logger_1.default.warn(`Failed to calculate cost information: ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch (error) {
|
|
328
|
-
// Re-throw TokenLimitError with additional context
|
|
329
|
-
if (error instanceof apiErrorHandler_1.TokenLimitError) {
|
|
330
|
-
throw error;
|
|
331
|
-
}
|
|
332
|
-
throw new apiErrorHandler_1.ApiError(`Failed to generate review with OpenRouter ${modelName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
333
|
-
}
|
|
334
|
-
// Try to parse the response as JSON with robust error recovery
|
|
335
|
-
let structuredData = null;
|
|
336
|
-
try {
|
|
337
|
-
// Try multiple strategies to extract JSON from the response
|
|
338
|
-
const jsonExtractionStrategies = [
|
|
339
|
-
// Strategy 1: Look for JSON in markdown code blocks
|
|
340
|
-
() => {
|
|
341
|
-
const patterns = [
|
|
342
|
-
/```(?:json)?\s*([\s\S]*?)\s*```/,
|
|
343
|
-
/```(?:typescript|javascript|ts|js)?\s*([\s\S]*?)\s*```/,
|
|
344
|
-
];
|
|
345
|
-
for (const pattern of patterns) {
|
|
346
|
-
const match = content.match(pattern);
|
|
347
|
-
if (match && match[1]) {
|
|
348
|
-
const extracted = match[1].trim();
|
|
349
|
-
if (extracted.startsWith('{') && extracted.endsWith('}')) {
|
|
350
|
-
return extracted;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return null;
|
|
355
|
-
},
|
|
356
|
-
// Strategy 2: Find balanced JSON object
|
|
357
|
-
() => {
|
|
358
|
-
const startIdx = content.indexOf('{');
|
|
359
|
-
if (startIdx === -1)
|
|
360
|
-
return null;
|
|
361
|
-
let depth = 0;
|
|
362
|
-
let inString = false;
|
|
363
|
-
let escapeNext = false;
|
|
364
|
-
for (let i = startIdx; i < content.length; i++) {
|
|
365
|
-
const char = content[i];
|
|
366
|
-
if (escapeNext) {
|
|
367
|
-
escapeNext = false;
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
if (char === '\\') {
|
|
371
|
-
escapeNext = true;
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
if (char === '"' && !escapeNext) {
|
|
375
|
-
inString = !inString;
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
if (!inString) {
|
|
379
|
-
if (char === '{')
|
|
380
|
-
depth++;
|
|
381
|
-
else if (char === '}') {
|
|
382
|
-
depth--;
|
|
383
|
-
if (depth === 0) {
|
|
384
|
-
return content.substring(startIdx, i + 1);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
// If we couldn't find balanced braces, try to fix unterminated strings
|
|
390
|
-
if (inString) {
|
|
391
|
-
// Add a closing quote and try to close the object
|
|
392
|
-
return content.substring(startIdx) + '"}';
|
|
393
|
-
}
|
|
394
|
-
return null;
|
|
395
|
-
},
|
|
396
|
-
// Strategy 3: Use the content as-is
|
|
397
|
-
() => content,
|
|
398
|
-
];
|
|
399
|
-
let jsonContent = null;
|
|
400
|
-
for (const strategy of jsonExtractionStrategies) {
|
|
401
|
-
try {
|
|
402
|
-
const extracted = strategy();
|
|
403
|
-
if (extracted) {
|
|
404
|
-
// Try to parse the extracted content
|
|
405
|
-
structuredData = JSON.parse(extracted);
|
|
406
|
-
// Validate that it has the expected structure
|
|
407
|
-
if (structuredData && typeof structuredData === 'object') {
|
|
408
|
-
jsonContent = extracted;
|
|
409
|
-
logger_1.default.debug('Successfully extracted and parsed JSON');
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
catch (err) {
|
|
415
|
-
// Continue to next strategy
|
|
416
|
-
logger_1.default.debug(`JSON extraction strategy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
// If we successfully parsed JSON, validate its structure
|
|
420
|
-
if (structuredData && !structuredData.summary && !Array.isArray(structuredData.issues)) {
|
|
421
|
-
logger_1.default.warn('Response is valid JSON but does not have the expected structure');
|
|
422
|
-
// Still keep the structured data as it might have partial information
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
catch (parseError) {
|
|
426
|
-
logger_1.default.warn(`Failed to parse response as JSON after all recovery attempts: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
427
|
-
// Keep the original response as content
|
|
428
|
-
structuredData = null;
|
|
429
|
-
}
|
|
430
|
-
// Return the review result
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
role: 'user',
|
|
467
|
+
content: prompt,
|
|
468
|
+
},
|
|
469
|
+
];
|
|
470
|
+
const callResult = await executeWithRetries(() => ({
|
|
471
|
+
messages,
|
|
472
|
+
temperature: 0.2,
|
|
473
|
+
...(requestMaxTokens ? { max_tokens: requestMaxTokens } : {}),
|
|
474
|
+
}), {
|
|
475
|
+
prompt,
|
|
476
|
+
reviewType,
|
|
477
|
+
isConsolidation: options?.isConsolidation,
|
|
478
|
+
options,
|
|
479
|
+
});
|
|
431
480
|
return {
|
|
432
|
-
content,
|
|
433
|
-
cost,
|
|
434
|
-
modelUsed: `openrouter:${modelName}`,
|
|
481
|
+
content: callResult.content,
|
|
482
|
+
cost: callResult.cost,
|
|
483
|
+
modelUsed: `openrouter:${callResult.modelName}`,
|
|
435
484
|
filePath,
|
|
436
485
|
reviewType,
|
|
437
486
|
timestamp: new Date().toISOString(),
|
|
438
|
-
structuredData,
|
|
487
|
+
structuredData: callResult.structuredData,
|
|
439
488
|
};
|
|
440
489
|
}
|
|
441
490
|
catch (error) {
|
|
@@ -454,52 +503,20 @@ Ensure your response is valid JSON. Do not include any text outside the JSON str
|
|
|
454
503
|
*/
|
|
455
504
|
async function generateOpenRouterConsolidatedReview(files, projectName, reviewType, projectDocs, options) {
|
|
456
505
|
try {
|
|
457
|
-
// Initialize the model if we haven't already
|
|
458
506
|
if (!modelInitialized) {
|
|
459
507
|
await initializeAnyOpenRouterModel();
|
|
460
508
|
}
|
|
461
|
-
// Use the imported dependencies
|
|
462
|
-
// Get API key from environment variables
|
|
463
|
-
let content;
|
|
464
|
-
let cost;
|
|
465
|
-
// Load the appropriate prompt template
|
|
466
509
|
const promptTemplate = await (0, promptLoader_1.loadPromptTemplate)(reviewType, options);
|
|
467
|
-
// Format the prompt
|
|
468
510
|
const prompt = (0, promptFormatter_1.formatConsolidatedReviewPrompt)(promptTemplate, projectName, files.map((file) => ({
|
|
469
511
|
relativePath: file.relativePath || '',
|
|
470
512
|
content: file.content,
|
|
471
513
|
sizeInBytes: file.content.length,
|
|
472
514
|
})), projectDocs, options?.runContext);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const [, model] = consolidationModel.includes(':')
|
|
479
|
-
? consolidationModel.split(':')
|
|
480
|
-
: ['openrouter', consolidationModel];
|
|
481
|
-
modelName = model;
|
|
482
|
-
logger_1.default.debug(`[OpenRouter] Using consolidation model: ${modelName}`);
|
|
483
|
-
}
|
|
484
|
-
else {
|
|
485
|
-
// Regular review - use the configured model
|
|
486
|
-
const result = isOpenRouterModel();
|
|
487
|
-
modelName = result.modelName;
|
|
488
|
-
logger_1.default.debug(`[OpenRouter] Using review model: ${modelName}`);
|
|
489
|
-
}
|
|
490
|
-
// Validate that we have a non-empty model name
|
|
491
|
-
if (!modelName || modelName.trim() === '') {
|
|
492
|
-
throw new Error(`Invalid or empty model name: '${modelName}'. Check your model configuration.`);
|
|
493
|
-
}
|
|
494
|
-
try {
|
|
495
|
-
logger_1.default.debug(`Generating consolidated review with OpenRouter ${modelName}...`);
|
|
496
|
-
const consolidationMaxTokens = getMaxTokensForReviewType(reviewType, options?.isConsolidation);
|
|
497
|
-
const payload = (0, openrouterProxy_1.withProxyMetadata)({
|
|
498
|
-
model: modelName,
|
|
499
|
-
messages: [
|
|
500
|
-
{
|
|
501
|
-
role: 'system',
|
|
502
|
-
content: `You are an expert code reviewer. Focus on providing actionable feedback. IMPORTANT: DO NOT REPEAT THE INSTRUCTIONS IN YOUR RESPONSE. DO NOT ASK FOR CODE TO REVIEW. ASSUME THE CODE IS ALREADY PROVIDED IN THE USER MESSAGE. FOCUS ONLY ON PROVIDING THE CODE REVIEW CONTENT.
|
|
515
|
+
const requestMaxTokens = getMaxTokensForReviewType(reviewType, options?.isConsolidation);
|
|
516
|
+
const messages = [
|
|
517
|
+
{
|
|
518
|
+
role: 'system',
|
|
519
|
+
content: `You are an expert code reviewer. Focus on providing actionable feedback. IMPORTANT: DO NOT REPEAT THE INSTRUCTIONS IN YOUR RESPONSE. DO NOT ASK FOR CODE TO REVIEW. ASSUME THE CODE IS ALREADY PROVIDED IN THE USER MESSAGE. FOCUS ONLY ON PROVIDING THE CODE REVIEW CONTENT.
|
|
503
520
|
|
|
504
521
|
IMPORTANT: Your response MUST be in the following JSON format:
|
|
505
522
|
|
|
@@ -529,205 +546,30 @@ IMPORTANT: Your response MUST be in the following JSON format:
|
|
|
529
546
|
}
|
|
530
547
|
|
|
531
548
|
Ensure your response is valid JSON. Do not include any text outside the JSON structure.`,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const errorData = await response.json();
|
|
549
|
-
// Check for token limit errors
|
|
550
|
-
const errorMessage = JSON.stringify(errorData).toLowerCase();
|
|
551
|
-
if (errorMessage.includes('token') &&
|
|
552
|
-
(errorMessage.includes('limit') ||
|
|
553
|
-
errorMessage.includes('exceed') ||
|
|
554
|
-
errorMessage.includes('too long') ||
|
|
555
|
-
errorMessage.includes('too many'))) {
|
|
556
|
-
// Extract token count from prompt if possible
|
|
557
|
-
const { countTokens } = await Promise.resolve().then(() => __importStar(require('../tokenizers')));
|
|
558
|
-
const tokenCount = countTokens(prompt, modelName);
|
|
559
|
-
throw new apiErrorHandler_1.TokenLimitError(`Token limit exceeded for model ${modelName}. Content has ${tokenCount.toLocaleString()} tokens. Consider using --multi-pass flag for large codebases.`, tokenCount, undefined, response.status, errorData);
|
|
560
|
-
}
|
|
561
|
-
throw new Error(`OpenRouter API error: ${JSON.stringify(errorData)}`);
|
|
562
|
-
}
|
|
563
|
-
const data = await response.json();
|
|
564
|
-
// Enhanced logging for debugging empty content issue
|
|
565
|
-
const finishReason = data.choices?.[0]?.finish_reason;
|
|
566
|
-
const responseContent = data.choices?.[0]?.message?.content || '';
|
|
567
|
-
const isTruncated = isResponseTruncated(responseContent, finishReason);
|
|
568
|
-
logger_1.default.debug(`[OpenRouter] API Response structure:`, {
|
|
569
|
-
hasChoices: !!data.choices,
|
|
570
|
-
choicesLength: data.choices?.length || 0,
|
|
571
|
-
firstChoiceExists: !!(data.choices && data.choices[0]),
|
|
572
|
-
firstChoiceMessage: data.choices?.[0]?.message ? 'exists' : 'missing',
|
|
573
|
-
contentExists: !!responseContent,
|
|
574
|
-
contentLength: responseContent.length,
|
|
575
|
-
contentPreview: responseContent.substring(0, 100) || 'N/A',
|
|
576
|
-
finishReason: finishReason || 'unknown',
|
|
577
|
-
isTruncated: isTruncated,
|
|
578
|
-
maxTokensUsed: getMaxTokensForReviewType(reviewType, options?.isConsolidation) || 'unlimited',
|
|
579
|
-
fullResponse: JSON.stringify(data).substring(0, 500) + '...',
|
|
580
|
-
});
|
|
581
|
-
if (data.choices && data.choices.length > 0) {
|
|
582
|
-
content = data.choices[0].message.content;
|
|
583
|
-
// Critical check for empty content
|
|
584
|
-
if (!content || content.trim().length === 0) {
|
|
585
|
-
logger_1.default.error(`[OpenRouter] CRITICAL: API returned successful response but content is empty!`);
|
|
586
|
-
logger_1.default.error(`[OpenRouter] Response details:`, {
|
|
587
|
-
modelName,
|
|
588
|
-
promptLength: prompt.length,
|
|
589
|
-
responseStatus: response.status,
|
|
590
|
-
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
591
|
-
fullApiResponse: JSON.stringify(data, null, 2),
|
|
592
|
-
});
|
|
593
|
-
throw new Error(`OpenRouter API returned empty content for model ${modelName}`);
|
|
594
|
-
}
|
|
595
|
-
// Check for truncated responses
|
|
596
|
-
if (isTruncated) {
|
|
597
|
-
logger_1.default.warn(`[OpenRouter] WARNING: Response appears to be truncated!`);
|
|
598
|
-
logger_1.default.warn(`[OpenRouter] Truncation details:`, {
|
|
599
|
-
modelName,
|
|
600
|
-
finishReason,
|
|
601
|
-
contentLength: content.length,
|
|
602
|
-
maxTokensRequested: getMaxTokensForReviewType(reviewType, options?.isConsolidation) || 'unlimited',
|
|
603
|
-
reviewType,
|
|
604
|
-
isConsolidation: options?.isConsolidation || false,
|
|
605
|
-
});
|
|
606
|
-
// For now, we'll continue with the truncated response but log it
|
|
607
|
-
// In the future, we could implement retry logic with shorter prompts
|
|
608
|
-
}
|
|
609
|
-
logger_1.default.debug(`Successfully generated review with OpenRouter ${modelName}`);
|
|
610
|
-
}
|
|
611
|
-
else {
|
|
612
|
-
logger_1.default.error(`[OpenRouter] Invalid response format:`, JSON.stringify(data, null, 2));
|
|
613
|
-
throw new Error(`Invalid response format from OpenRouter ${modelName}`);
|
|
614
|
-
}
|
|
615
|
-
// Calculate cost information
|
|
616
|
-
try {
|
|
617
|
-
cost = (0, tokenCounter_1.getCostInfoFromText)(prompt, content, `openrouter:${modelName}`);
|
|
618
|
-
}
|
|
619
|
-
catch (error) {
|
|
620
|
-
logger_1.default.warn(`Failed to calculate cost information: ${error instanceof Error ? error.message : String(error)}`);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
catch (error) {
|
|
624
|
-
throw new apiErrorHandler_1.ApiError(`Failed to generate consolidated review with OpenRouter ${modelName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
625
|
-
}
|
|
626
|
-
// Try to parse the response as JSON with robust error recovery
|
|
627
|
-
let structuredData = null;
|
|
628
|
-
try {
|
|
629
|
-
// Try multiple strategies to extract JSON from the response
|
|
630
|
-
const jsonExtractionStrategies = [
|
|
631
|
-
// Strategy 1: Look for JSON in markdown code blocks
|
|
632
|
-
() => {
|
|
633
|
-
const patterns = [
|
|
634
|
-
/```(?:json)?\s*([\s\S]*?)\s*```/,
|
|
635
|
-
/```(?:typescript|javascript|ts|js)?\s*([\s\S]*?)\s*```/,
|
|
636
|
-
];
|
|
637
|
-
for (const pattern of patterns) {
|
|
638
|
-
const match = content.match(pattern);
|
|
639
|
-
if (match && match[1]) {
|
|
640
|
-
const extracted = match[1].trim();
|
|
641
|
-
if (extracted.startsWith('{') && extracted.endsWith('}')) {
|
|
642
|
-
return extracted;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return null;
|
|
647
|
-
},
|
|
648
|
-
// Strategy 2: Find balanced JSON object
|
|
649
|
-
() => {
|
|
650
|
-
const startIdx = content.indexOf('{');
|
|
651
|
-
if (startIdx === -1)
|
|
652
|
-
return null;
|
|
653
|
-
let depth = 0;
|
|
654
|
-
let inString = false;
|
|
655
|
-
let escapeNext = false;
|
|
656
|
-
for (let i = startIdx; i < content.length; i++) {
|
|
657
|
-
const char = content[i];
|
|
658
|
-
if (escapeNext) {
|
|
659
|
-
escapeNext = false;
|
|
660
|
-
continue;
|
|
661
|
-
}
|
|
662
|
-
if (char === '\\') {
|
|
663
|
-
escapeNext = true;
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
if (char === '"' && !escapeNext) {
|
|
667
|
-
inString = !inString;
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
if (!inString) {
|
|
671
|
-
if (char === '{')
|
|
672
|
-
depth++;
|
|
673
|
-
else if (char === '}') {
|
|
674
|
-
depth--;
|
|
675
|
-
if (depth === 0) {
|
|
676
|
-
return content.substring(startIdx, i + 1);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
// If we couldn't find balanced braces, try to fix unterminated strings
|
|
682
|
-
if (inString) {
|
|
683
|
-
// Add a closing quote and try to close the object
|
|
684
|
-
return content.substring(startIdx) + '"}';
|
|
685
|
-
}
|
|
686
|
-
return null;
|
|
687
|
-
},
|
|
688
|
-
// Strategy 3: Use the content as-is
|
|
689
|
-
() => content,
|
|
690
|
-
];
|
|
691
|
-
let jsonContent = null;
|
|
692
|
-
for (const strategy of jsonExtractionStrategies) {
|
|
693
|
-
try {
|
|
694
|
-
const extracted = strategy();
|
|
695
|
-
if (extracted) {
|
|
696
|
-
// Try to parse the extracted content
|
|
697
|
-
structuredData = JSON.parse(extracted);
|
|
698
|
-
// Validate that it has the expected structure
|
|
699
|
-
if (structuredData && typeof structuredData === 'object') {
|
|
700
|
-
jsonContent = extracted;
|
|
701
|
-
logger_1.default.debug('Successfully extracted and parsed JSON');
|
|
702
|
-
break;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
catch (err) {
|
|
707
|
-
// Continue to next strategy
|
|
708
|
-
logger_1.default.debug(`JSON extraction strategy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
// If we successfully parsed JSON, validate its structure
|
|
712
|
-
if (structuredData && !structuredData.summary && !Array.isArray(structuredData.issues)) {
|
|
713
|
-
logger_1.default.warn('Response is valid JSON but does not have the expected structure');
|
|
714
|
-
// Still keep the structured data as it might have partial information
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
catch (parseError) {
|
|
718
|
-
logger_1.default.warn(`Failed to parse response as JSON after all recovery attempts: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
719
|
-
// Keep the original response as content
|
|
720
|
-
structuredData = null;
|
|
721
|
-
}
|
|
722
|
-
// Return the review result
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
role: 'user',
|
|
552
|
+
content: prompt,
|
|
553
|
+
},
|
|
554
|
+
];
|
|
555
|
+
const callResult = await executeWithRetries(() => ({
|
|
556
|
+
messages,
|
|
557
|
+
temperature: 0.2,
|
|
558
|
+
...(requestMaxTokens ? { max_tokens: requestMaxTokens } : {}),
|
|
559
|
+
}), {
|
|
560
|
+
prompt,
|
|
561
|
+
reviewType,
|
|
562
|
+
isConsolidation: options?.isConsolidation,
|
|
563
|
+
options,
|
|
564
|
+
});
|
|
723
565
|
return {
|
|
724
|
-
content,
|
|
725
|
-
cost,
|
|
726
|
-
modelUsed: `openrouter:${modelName}`,
|
|
566
|
+
content: callResult.content,
|
|
567
|
+
cost: callResult.cost,
|
|
568
|
+
modelUsed: `openrouter:${callResult.modelName}`,
|
|
727
569
|
filePath: 'consolidated',
|
|
728
570
|
reviewType,
|
|
729
571
|
timestamp: new Date().toISOString(),
|
|
730
|
-
structuredData,
|
|
572
|
+
structuredData: callResult.structuredData,
|
|
731
573
|
};
|
|
732
574
|
}
|
|
733
575
|
catch (error) {
|