codecritique 2.2.5 → 2.3.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.
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/index.js +1 -1
- package/src/llm.js +29 -3
- package/src/llm.test.js +113 -4
- package/src/project-analyzer.js +1 -0
- package/src/project-analyzer.test.js +7 -1
- package/src/prompt-cache.js +6 -5
- package/src/prompt-cache.test.js +61 -0
- package/src/rag-analyzer.js +1 -2
- package/src/rag-analyzer.test.js +6 -0
package/README.md
CHANGED
|
@@ -415,7 +415,7 @@ jobs:
|
|
|
415
415
|
| `anthropic-api-key` | Anthropic API key for Claude models | **Yes** | - |
|
|
416
416
|
| `skip-label` | Label name to skip AI review | No | `ai-review-disabled` |
|
|
417
417
|
| `verbose` | Show verbose output | No | `false` |
|
|
418
|
-
| `model` | LLM model to use (e.g., `claude-sonnet-
|
|
418
|
+
| `model` | LLM model to use (e.g., `claude-sonnet-5`) | No | Auto-selected |
|
|
419
419
|
| `max-tokens` | Maximum tokens for LLM response | No | Auto-calculated |
|
|
420
420
|
| `cache-ttl` | Cache TTL for LLM prompts: "5m" (default, no extra cost) or "1h" (extended, extra cost for cache writes) | No | `5m` |
|
|
421
421
|
| `batch` | Use Anthropic Message Batches for lower-cost, asynchronous reviews | No | `false` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecritique",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "AI-powered code review tool for any programming language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"author": "cosmocoder",
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@anthropic-ai/sdk": "0.
|
|
54
|
+
"@anthropic-ai/sdk": "0.125.0",
|
|
55
55
|
"@huggingface/transformers": "3.8.0",
|
|
56
56
|
"@lancedb/lancedb": "0.38.0",
|
|
57
57
|
"@octokit/rest": "22.0.1",
|
package/src/index.js
CHANGED
|
@@ -63,7 +63,7 @@ program
|
|
|
63
63
|
.option('--output-file <file>', 'Save output to file (useful with --output json)')
|
|
64
64
|
.option('--no-color', 'Disable colored output')
|
|
65
65
|
.option('--verbose', 'Show verbose output')
|
|
66
|
-
.option('--model <model>', 'LLM model to use (e.g., claude-sonnet-
|
|
66
|
+
.option('--model <model>', 'LLM model to use (e.g., claude-sonnet-5)')
|
|
67
67
|
.option('--temperature <number>', 'LLM temperature', parseFloat, 0.2)
|
|
68
68
|
.option('--max-tokens <number>', 'LLM max tokens', parseIntegerOption, 8192)
|
|
69
69
|
.option('--cache-ttl <ttl>', 'Cache TTL for LLM prompts: "5m" (default, no extra cost) or "1h" (extended, extra cost for writes)', '5m')
|
package/src/llm.js
CHANGED
|
@@ -42,7 +42,11 @@ function getAnthropicClient() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Default model
|
|
45
|
-
const DEFAULT_MODEL = 'claude-sonnet-
|
|
45
|
+
const DEFAULT_MODEL = 'claude-sonnet-5';
|
|
46
|
+
|
|
47
|
+
// Models after the 4.6 generation reject `temperature` with a 400. Only send it
|
|
48
|
+
// for the older families; unknown models fall back to the API default.
|
|
49
|
+
const SAMPLING_MODEL_PATTERN = /^claude-(?:3|(?:opus|sonnet|haiku)-4-[0-6])/;
|
|
46
50
|
|
|
47
51
|
// Maximum tokens for response
|
|
48
52
|
const MAX_TOKENS = 4096;
|
|
@@ -138,6 +142,8 @@ async function createBatchedMessage(client, requestParams, options) {
|
|
|
138
142
|
* @param {string} options.system - System prompt (will be cached for cost optimization)
|
|
139
143
|
* @param {Array<string|Object>} options.cachedSystemBlocks - Additional stable system blocks to cache when possible
|
|
140
144
|
* @param {Object} options.jsonSchema - JSON schema for structured output
|
|
145
|
+
* @param {boolean} [options.strict=false] - Enforce the schema on the tool input. Every object in the
|
|
146
|
+
* schema must set `additionalProperties: false`, and `additionalProperties` accepts no other value.
|
|
141
147
|
* @param {string} options.cacheTtl - Cache TTL: '5m' (default, no extra cost) or '1h' (extended, extra cost for writes)
|
|
142
148
|
* @param {boolean} [options.batch=false] - Use the asynchronous Message Batches API
|
|
143
149
|
* @returns {Promise<Object>} The response from Claude with structured data
|
|
@@ -150,6 +156,7 @@ async function sendPromptToClaude(prompt, options = {}) {
|
|
|
150
156
|
system = '',
|
|
151
157
|
cachedSystemBlocks = [],
|
|
152
158
|
jsonSchema = null,
|
|
159
|
+
strict = false,
|
|
153
160
|
cacheTtl = '5m',
|
|
154
161
|
} = options;
|
|
155
162
|
|
|
@@ -169,7 +176,7 @@ async function sendPromptToClaude(prompt, options = {}) {
|
|
|
169
176
|
const requestParams = {
|
|
170
177
|
model,
|
|
171
178
|
max_tokens: maxTokens,
|
|
172
|
-
temperature,
|
|
179
|
+
...(SAMPLING_MODEL_PATTERN.test(model) ? { temperature } : {}),
|
|
173
180
|
system: systemContent,
|
|
174
181
|
messages: [
|
|
175
182
|
{
|
|
@@ -186,6 +193,9 @@ async function sendPromptToClaude(prompt, options = {}) {
|
|
|
186
193
|
name: 'return_json',
|
|
187
194
|
description: 'Return the final answer strictly as JSON matching the schema.',
|
|
188
195
|
input_schema: jsonSchema,
|
|
196
|
+
// Omitted rather than sent as false so the tool block stays byte-identical
|
|
197
|
+
// for callers that do not use it, which keeps their cached prefix valid.
|
|
198
|
+
...(strict ? { strict: true } : {}),
|
|
189
199
|
},
|
|
190
200
|
];
|
|
191
201
|
requestParams.tool_choice = { type: 'tool', name: 'return_json' };
|
|
@@ -199,6 +209,22 @@ async function sendPromptToClaude(prompt, options = {}) {
|
|
|
199
209
|
verboseLog(options, chalk.gray(` Response stop_reason: ${response.stop_reason}`));
|
|
200
210
|
verboseLog(options, chalk.gray(` Response content blocks: ${response.content?.length || 0}`));
|
|
201
211
|
|
|
212
|
+
// A refusal arrives as HTTP 200 with no tool_use and no text, so it must be caught
|
|
213
|
+
// here or it surfaces as a missing-output error that hides the real cause.
|
|
214
|
+
if (response.stop_reason === 'refusal') {
|
|
215
|
+
const { category, explanation } = response.stop_details || {};
|
|
216
|
+
const reason = explanation || 'no explanation provided';
|
|
217
|
+
throw new Error(`Claude declined the request${category ? ` (${category})` : ''}: ${reason}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Truncation also arrives as HTTP 200. Without this the tool path reports missing
|
|
221
|
+
// output and the text path returns a severed answer that reads as a complete one.
|
|
222
|
+
if (response.stop_reason === 'max_tokens' || response.stop_reason === 'model_context_window_exceeded') {
|
|
223
|
+
const remedy =
|
|
224
|
+
response.stop_reason === 'max_tokens' ? 'Increase the configured output limit or continue the response.' : 'Reduce the input size.';
|
|
225
|
+
throw new Error(`Claude's response was truncated (${response.stop_reason}). ${remedy}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
202
228
|
// Process response based on whether we used tool calling
|
|
203
229
|
if (jsonSchema) {
|
|
204
230
|
const toolUse = response.content.find((block) => block.type === 'tool_use' && block.name === 'return_json');
|
|
@@ -221,7 +247,7 @@ async function sendPromptToClaude(prompt, options = {}) {
|
|
|
221
247
|
}
|
|
222
248
|
else {
|
|
223
249
|
return {
|
|
224
|
-
content: response.content
|
|
250
|
+
content: response.content?.find((block) => block.type === 'text')?.text || '',
|
|
225
251
|
model: response.model,
|
|
226
252
|
usage: response.usage,
|
|
227
253
|
};
|
package/src/llm.test.js
CHANGED
|
@@ -65,13 +65,122 @@ describe('sendPromptToClaude', () => {
|
|
|
65
65
|
|
|
66
66
|
expect(mockMessagesCreate).toHaveBeenCalledWith(
|
|
67
67
|
expect.objectContaining({
|
|
68
|
-
model: 'claude-sonnet-
|
|
68
|
+
model: 'claude-sonnet-5',
|
|
69
69
|
max_tokens: 4096,
|
|
70
|
-
temperature: 0.7,
|
|
71
70
|
})
|
|
72
71
|
);
|
|
73
72
|
});
|
|
74
73
|
|
|
74
|
+
it('should surface the refusal explanation instead of a missing-output error', async () => {
|
|
75
|
+
mockMessagesCreate.mockResolvedValue({
|
|
76
|
+
stop_reason: 'refusal',
|
|
77
|
+
stop_details: { type: 'refusal', category: 'cyber', explanation: 'Declined: offensive tooling.' },
|
|
78
|
+
content: [],
|
|
79
|
+
model: 'claude-sonnet-5',
|
|
80
|
+
usage: {},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await expect(sendPromptToClaude('Test prompt', { jsonSchema: { type: 'object' } })).rejects.toThrow(
|
|
84
|
+
'Claude declined the request (cyber): Declined: offensive tooling.'
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it.each([
|
|
89
|
+
['max_tokens', 'Increase the configured output limit or continue the response.'],
|
|
90
|
+
['model_context_window_exceeded', 'Reduce the input size.'],
|
|
91
|
+
])('should report a truncated response for stop_reason %s', async (stopReason, remedy) => {
|
|
92
|
+
mockMessagesCreate.mockResolvedValue({
|
|
93
|
+
stop_reason: stopReason,
|
|
94
|
+
content: [],
|
|
95
|
+
model: 'claude-sonnet-5',
|
|
96
|
+
usage: {},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await expect(sendPromptToClaude('Test prompt', { jsonSchema: { type: 'object' } })).rejects.toThrow(
|
|
100
|
+
`Claude's response was truncated (${stopReason}). ${remedy}`
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should report truncation rather than returning a severed plain-text answer', async () => {
|
|
105
|
+
mockMessagesCreate.mockResolvedValue({
|
|
106
|
+
stop_reason: 'max_tokens',
|
|
107
|
+
content: [{ type: 'text', text: 'partial answer cut off mid-' }],
|
|
108
|
+
model: 'claude-sonnet-5',
|
|
109
|
+
usage: {},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await expect(sendPromptToClaude('Test prompt')).rejects.toThrow('was truncated (max_tokens)');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should read the text block even when a thinking block precedes it', async () => {
|
|
116
|
+
mockMessagesCreate.mockResolvedValue({
|
|
117
|
+
stop_reason: 'end_turn',
|
|
118
|
+
content: [
|
|
119
|
+
{ type: 'thinking', thinking: '' },
|
|
120
|
+
{ type: 'text', text: 'The real answer' },
|
|
121
|
+
],
|
|
122
|
+
model: 'claude-sonnet-5',
|
|
123
|
+
usage: {},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const result = await sendPromptToClaude('Test prompt');
|
|
127
|
+
|
|
128
|
+
expect(result.content).toBe('The real answer');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should omit temperature for models that reject sampling parameters', async () => {
|
|
132
|
+
mockMessagesCreate.mockResolvedValue({
|
|
133
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
134
|
+
model: 'claude-sonnet-5',
|
|
135
|
+
usage: {},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await sendPromptToClaude('Test prompt', { temperature: 0.5 });
|
|
139
|
+
|
|
140
|
+
expect(mockMessagesCreate.mock.calls[0][0]).not.toHaveProperty('temperature');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should set strict on the return_json tool when requested', async () => {
|
|
144
|
+
mockMessagesCreate.mockResolvedValue({
|
|
145
|
+
content: [{ type: 'tool_use', name: 'return_json', input: { ok: true } }],
|
|
146
|
+
model: 'claude-sonnet-5',
|
|
147
|
+
usage: {},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await sendPromptToClaude('Test prompt', {
|
|
151
|
+
jsonSchema: { type: 'object', properties: {}, required: [], additionalProperties: false },
|
|
152
|
+
strict: true,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(mockMessagesCreate.mock.calls[0][0].tools[0]).toHaveProperty('strict', true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should omit strict from the return_json tool by default', async () => {
|
|
159
|
+
mockMessagesCreate.mockResolvedValue({
|
|
160
|
+
content: [{ type: 'tool_use', name: 'return_json', input: { ok: true } }],
|
|
161
|
+
model: 'claude-sonnet-5',
|
|
162
|
+
usage: {},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await sendPromptToClaude('Test prompt', {
|
|
166
|
+
jsonSchema: { type: 'object', properties: {}, required: [], additionalProperties: false },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(mockMessagesCreate.mock.calls[0][0].tools[0]).not.toHaveProperty('strict');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should keep temperature for models that still accept it', async () => {
|
|
173
|
+
mockMessagesCreate.mockResolvedValue({
|
|
174
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
175
|
+
model: 'claude-haiku-4-5',
|
|
176
|
+
usage: {},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await sendPromptToClaude('Test prompt', { model: 'claude-haiku-4-5', temperature: 0.1 });
|
|
180
|
+
|
|
181
|
+
expect(mockMessagesCreate).toHaveBeenCalledWith(expect.objectContaining({ temperature: 0.1 }));
|
|
182
|
+
});
|
|
183
|
+
|
|
75
184
|
it('should use custom options', async () => {
|
|
76
185
|
mockMessagesCreate.mockResolvedValue({
|
|
77
186
|
content: [{ type: 'text', text: 'Response' }],
|
|
@@ -105,7 +214,7 @@ describe('sendPromptToClaude', () => {
|
|
|
105
214
|
it('should retry polling and unwrap a successful batch request', async () => {
|
|
106
215
|
const message = {
|
|
107
216
|
content: [{ type: 'text', text: 'Batched response' }],
|
|
108
|
-
model: 'claude-sonnet-
|
|
217
|
+
model: 'claude-sonnet-5',
|
|
109
218
|
usage: { input_tokens: 100, output_tokens: 50 },
|
|
110
219
|
};
|
|
111
220
|
mockBatchesCreate.mockResolvedValue({ id: 'msgbatch_123', processing_status: 'in_progress' });
|
|
@@ -127,7 +236,7 @@ describe('sendPromptToClaude', () => {
|
|
|
127
236
|
{
|
|
128
237
|
custom_id: 'codecritique-review',
|
|
129
238
|
params: expect.objectContaining({
|
|
130
|
-
model: 'claude-sonnet-
|
|
239
|
+
model: 'claude-sonnet-5',
|
|
131
240
|
messages: [{ role: 'user', content: 'Review this code' }],
|
|
132
241
|
}),
|
|
133
242
|
},
|
package/src/project-analyzer.js
CHANGED
|
@@ -592,6 +592,7 @@ Select files following the criteria in the system instructions.`;
|
|
|
592
592
|
temperature: 0.1,
|
|
593
593
|
maxTokens: 1000,
|
|
594
594
|
jsonSchema: fileSelectionSchema,
|
|
595
|
+
strict: true,
|
|
595
596
|
});
|
|
596
597
|
|
|
597
598
|
verboseLog({}, chalk.gray(' 📄 LLM Response preview:'), response.content.substring(0, 200));
|
|
@@ -556,7 +556,10 @@ describe('ProjectAnalyzer', () => {
|
|
|
556
556
|
|
|
557
557
|
const result = await analyzer.selectFinalKeyFiles(candidates, mockProjectPath);
|
|
558
558
|
|
|
559
|
-
expect(llm.sendPromptToClaude).toHaveBeenCalledWith(
|
|
559
|
+
expect(llm.sendPromptToClaude).toHaveBeenCalledWith(
|
|
560
|
+
expect.any(String),
|
|
561
|
+
expect.objectContaining({ model: 'claude-haiku-4-5', strict: true })
|
|
562
|
+
);
|
|
560
563
|
expect(result.length).toBe(1);
|
|
561
564
|
expect(result[0].relativePath).toBe('package.json');
|
|
562
565
|
});
|
|
@@ -673,6 +676,9 @@ describe('ProjectAnalyzer', () => {
|
|
|
673
676
|
const result = await analyzer.generateProjectSummary(mockKeyFiles, mockProjectPath);
|
|
674
677
|
|
|
675
678
|
expect(llm.sendPromptToClaude).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ model: 'claude-haiku-4-5' }));
|
|
679
|
+
// projectSummarySchema sets additionalProperties: false on its top level only, and
|
|
680
|
+
// the documented strict subset requires it on every object.
|
|
681
|
+
expect(llm.sendPromptToClaude.mock.calls[0][1]).not.toHaveProperty('strict');
|
|
676
682
|
expect(result.projectName).toBe('test-project');
|
|
677
683
|
expect(result.analysisDate).toBeDefined();
|
|
678
684
|
expect(result.projectPath).toBe(mockProjectPath);
|
package/src/prompt-cache.js
CHANGED
|
@@ -248,9 +248,9 @@ You must respond with EXACTLY this JSON structure, with no additional text:
|
|
|
248
248
|
{
|
|
249
249
|
"type": "bug | improvement | convention | architecture",
|
|
250
250
|
"severity": "critical | high | medium",
|
|
251
|
-
"
|
|
251
|
+
"message": "Detailed description of an issue that spans multiple files...",
|
|
252
252
|
"suggestion": "Actionable suggestion to resolve the cross-file issue.",
|
|
253
|
-
"
|
|
253
|
+
"files": ["path/to/file1.js", "path/to/file2.ts"]
|
|
254
254
|
}
|
|
255
255
|
],
|
|
256
256
|
"fileSpecificIssues": {
|
|
@@ -272,9 +272,10 @@ You must respond with EXACTLY this JSON structure, with no additional text:
|
|
|
272
272
|
},
|
|
273
273
|
"recommendations": [
|
|
274
274
|
{
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
275
|
+
"category": "refactoring | testing | documentation",
|
|
276
|
+
"suggestion": "A high-level recommendation for improving the codebase...",
|
|
277
|
+
"priority": "high | medium | low",
|
|
278
|
+
"impact": "Expected benefit of applying this recommendation."
|
|
278
279
|
}
|
|
279
280
|
]
|
|
280
281
|
}`;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { PR_REVIEW_JSON_SCHEMA } from './prompt-cache.js';
|
|
3
|
+
import { collectPRLevelFindings } from './utils/review-results.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The prompt tells the model which field names to emit. `review-results.js` reads
|
|
7
|
+
* those names back. A divergence drops data silently, so these tests pin the
|
|
8
|
+
* prompt to the names the consumers actually read.
|
|
9
|
+
*/
|
|
10
|
+
describe('PR_REVIEW_JSON_SCHEMA', () => {
|
|
11
|
+
it('should name the cross-file issue fields that normalizePRLevelIssue reads', () => {
|
|
12
|
+
expect(PR_REVIEW_JSON_SCHEMA).toContain('"message"');
|
|
13
|
+
expect(PR_REVIEW_JSON_SCHEMA).toContain('"files"');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should name the recommendation fields that normalizeRecommendation reads', () => {
|
|
17
|
+
// "suggestion" appears in three blocks, so bound the slice at both ends: a slice
|
|
18
|
+
// that ran to the end of the schema would pass on any later block instead.
|
|
19
|
+
const start = PR_REVIEW_JSON_SCHEMA.indexOf('"recommendations"');
|
|
20
|
+
const recommendations = PR_REVIEW_JSON_SCHEMA.slice(start, PR_REVIEW_JSON_SCHEMA.indexOf(']', start) + 1);
|
|
21
|
+
|
|
22
|
+
expect(recommendations).toContain('"category"');
|
|
23
|
+
expect(recommendations).toContain('"suggestion"');
|
|
24
|
+
expect(recommendations).toContain('"impact"');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should not name filesInvolved, which no consumer reads', () => {
|
|
28
|
+
expect(PR_REVIEW_JSON_SCHEMA).not.toContain('filesInvolved');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('collectPRLevelFindings with prompt-shaped input', () => {
|
|
33
|
+
const holisticAnalysis = {
|
|
34
|
+
results: {
|
|
35
|
+
summary: 'PR summary',
|
|
36
|
+
crossFileIssues: [
|
|
37
|
+
{
|
|
38
|
+
type: 'architecture',
|
|
39
|
+
severity: 'high',
|
|
40
|
+
message: 'Duplicated retry logic',
|
|
41
|
+
suggestion: 'Extract a shared helper',
|
|
42
|
+
files: ['a.js', 'b.js'],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
recommendations: [{ category: 'testing', suggestion: 'Cover the retry path', impact: 'Fewer regressions' }],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
it('should keep the file list from a cross-file issue', () => {
|
|
50
|
+
const findings = collectPRLevelFindings([], { holisticAnalysis });
|
|
51
|
+
|
|
52
|
+
expect(findings.issues[0].files).toEqual(['a.js', 'b.js']);
|
|
53
|
+
expect(findings.issues[0].description).toBe('Duplicated retry logic');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should render a recommendation as text rather than raw JSON', () => {
|
|
57
|
+
const findings = collectPRLevelFindings([], { holisticAnalysis });
|
|
58
|
+
|
|
59
|
+
expect(findings.recommendations[0]).toBe('testing: Cover the retry path: Fewer regressions');
|
|
60
|
+
});
|
|
61
|
+
});
|
package/src/rag-analyzer.js
CHANGED
|
@@ -764,7 +764,6 @@ function prepareContextForLLM(filePath, content, language, finalCodeExamples, fi
|
|
|
764
764
|
async function callLLMForAnalysis(context, options = {}) {
|
|
765
765
|
try {
|
|
766
766
|
let prompt;
|
|
767
|
-
const model = options.model || 'claude-sonnet-4-6';
|
|
768
767
|
const maxTokens = options.maxTokens || 8192; // Default to a safe limit
|
|
769
768
|
|
|
770
769
|
if (options.isHolisticPRReview) {
|
|
@@ -778,7 +777,7 @@ async function callLLMForAnalysis(context, options = {}) {
|
|
|
778
777
|
const llmResponse = await sendPromptToLLM(prompt, {
|
|
779
778
|
temperature: 0,
|
|
780
779
|
maxTokens: maxTokens,
|
|
781
|
-
model: model,
|
|
780
|
+
model: options.model,
|
|
782
781
|
isJsonMode: true, // Standardize on using JSON mode if available
|
|
783
782
|
verbose: options.verbose || false, // Pass verbose flag for cache statistics
|
|
784
783
|
cacheTtl: options.cacheTtl || '5m', // Pass cache TTL option (default: 5m, no extra cost)
|
package/src/rag-analyzer.test.js
CHANGED
|
@@ -242,6 +242,12 @@ describe('rag-analyzer', () => {
|
|
|
242
242
|
expect(result.success).toBe(true);
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
+
it('should forward an explicit model option to the LLM', async () => {
|
|
246
|
+
await runAnalysis('/test/file.js', { model: 'claude-3-opus' });
|
|
247
|
+
|
|
248
|
+
expect(llm.sendPromptToClaude).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ model: 'claude-3-opus' }));
|
|
249
|
+
});
|
|
250
|
+
|
|
245
251
|
it('should handle diff-only mode', async () => {
|
|
246
252
|
const result = await runAnalysis('/test/file.js', {
|
|
247
253
|
diffOnly: true,
|