sonar-autofix 0.1.0 → 0.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/AGENTS.md +164 -0
- package/README.md +2 -2
- package/config.example.json +3 -1
- package/package.json +6 -2
- package/scripts/publish-all.js +211 -0
- package/.idea/vcs.xml +0 -6
- package/.idea/workspace.xml +0 -196
- package/packages/cli/index.js +0 -132
- package/packages/cli/package.json +0 -23
- package/packages/core/index.js +0 -475
- package/packages/core/package.json +0 -22
- package/packages/git-client/index.js +0 -371
- package/packages/git-client/package.json +0 -17
- package/packages/github-action/action.yml +0 -71
- package/packages/github-action/index.js +0 -86
- package/packages/github-action/package.json +0 -17
- package/packages/llm-client/index.js +0 -419
- package/packages/llm-client/package.json +0 -17
- package/packages/sonar-client/index.js +0 -329
- package/packages/sonar-client/package.json +0 -20
- package/packages/validator/index.js +0 -170
- package/packages/validator/package.json +0 -17
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const axios = require('axios');
|
|
4
|
-
|
|
5
|
-
const DEFAULT_TIMEOUT = 360000;
|
|
6
|
-
const MAX_RETRIES = 3;
|
|
7
|
-
const RETRY_STATUS_CODES = [429, 500, 503];
|
|
8
|
-
|
|
9
|
-
class LLMClient {
|
|
10
|
-
/**
|
|
11
|
-
* @param {object} config
|
|
12
|
-
* @param {object} config.llm
|
|
13
|
-
* @param {string} config.llm.provider - lmstudio | openai | azure | anthropic | ollama | openrouter
|
|
14
|
-
* @param {string} [config.llm.endpoint] - Custom endpoint URL
|
|
15
|
-
* @param {string} config.llm.model - Model name or deployment name
|
|
16
|
-
* @param {string} [config.llm.apiKey] - API key (required for openai, azure, anthropic, openrouter)
|
|
17
|
-
* @param {number} [config.llm.timeout] - Request timeout in ms
|
|
18
|
-
* @param {number} [config.llm.maxRetries] - Max retry attempts for transient errors
|
|
19
|
-
*/
|
|
20
|
-
constructor(config) {
|
|
21
|
-
if (!config || !config.llm) {
|
|
22
|
-
throw new Error('LLMClient requires a config object with an "llm" property');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const { provider, endpoint, model, apiKey, timeout, maxRetries } = config.llm;
|
|
26
|
-
|
|
27
|
-
if (!provider) {
|
|
28
|
-
throw new Error('LLMClient requires "provider" in config.llm');
|
|
29
|
-
}
|
|
30
|
-
if (!model) {
|
|
31
|
-
throw new Error('LLMClient requires "model" in config.llm');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
this.provider = provider.toLowerCase();
|
|
35
|
-
this.endpoint = endpoint;
|
|
36
|
-
this.model = model;
|
|
37
|
-
this.apiKey = apiKey;
|
|
38
|
-
this.timeout = timeout || DEFAULT_TIMEOUT;
|
|
39
|
-
this.maxRetries = maxRetries != null ? maxRetries : MAX_RETRIES;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Resolve the full URL for the chat completions endpoint based on provider.
|
|
44
|
-
* @returns {string}
|
|
45
|
-
*/
|
|
46
|
-
_resolveUrl() {
|
|
47
|
-
switch (this.provider) {
|
|
48
|
-
case 'lmstudio':
|
|
49
|
-
return `${this.endpoint || 'http://localhost:1234/v1'}/chat/completions`;
|
|
50
|
-
|
|
51
|
-
case 'openai':
|
|
52
|
-
return this.endpoint
|
|
53
|
-
? `${this.endpoint}/chat/completions`
|
|
54
|
-
: 'https://api.openai.com/v1/chat/completions';
|
|
55
|
-
|
|
56
|
-
case 'azure':
|
|
57
|
-
if (!this.endpoint) {
|
|
58
|
-
throw new Error('Azure OpenAI requires an "endpoint" in config.llm');
|
|
59
|
-
}
|
|
60
|
-
return `${this.endpoint}/openai/deployments/${this.model}/chat/completions?api-version=2024-02-01`;
|
|
61
|
-
|
|
62
|
-
case 'anthropic':
|
|
63
|
-
return this.endpoint
|
|
64
|
-
? `${this.endpoint}/v1/messages`
|
|
65
|
-
: 'https://api.anthropic.com/v1/messages';
|
|
66
|
-
|
|
67
|
-
case 'ollama':
|
|
68
|
-
return this.endpoint
|
|
69
|
-
? `${this.endpoint}/api/chat`
|
|
70
|
-
: 'http://localhost:11434/api/chat';
|
|
71
|
-
|
|
72
|
-
case 'openrouter':
|
|
73
|
-
return this.endpoint
|
|
74
|
-
? `${this.endpoint}/chat/completions`
|
|
75
|
-
: 'https://openrouter.ai/api/v1/chat/completions';
|
|
76
|
-
|
|
77
|
-
default:
|
|
78
|
-
throw new Error(`Unsupported LLM provider: "${this.provider}"`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Build request headers based on provider.
|
|
84
|
-
* @returns {object}
|
|
85
|
-
*/
|
|
86
|
-
_buildHeaders() {
|
|
87
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
88
|
-
|
|
89
|
-
switch (this.provider) {
|
|
90
|
-
case 'anthropic':
|
|
91
|
-
if (this.apiKey) {
|
|
92
|
-
headers['x-api-key'] = this.apiKey;
|
|
93
|
-
}
|
|
94
|
-
headers['anthropic-version'] = '2023-06-01';
|
|
95
|
-
break;
|
|
96
|
-
|
|
97
|
-
case 'azure':
|
|
98
|
-
if (this.apiKey) {
|
|
99
|
-
headers['api-key'] = this.apiKey;
|
|
100
|
-
}
|
|
101
|
-
break;
|
|
102
|
-
|
|
103
|
-
default:
|
|
104
|
-
if (this.apiKey) {
|
|
105
|
-
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
106
|
-
}
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return headers;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Convert OpenAI-format messages to Anthropic format.
|
|
115
|
-
* @param {Array<{role: string, content: string}>} messages
|
|
116
|
-
* @returns {{ system: string|undefined, messages: Array<{role: string, content: string}> }}
|
|
117
|
-
*/
|
|
118
|
-
_toAnthropicFormat(messages) {
|
|
119
|
-
let system;
|
|
120
|
-
const filtered = [];
|
|
121
|
-
|
|
122
|
-
for (const msg of messages) {
|
|
123
|
-
if (msg.role === 'system') {
|
|
124
|
-
system = system ? `${system}\n${msg.content}` : msg.content;
|
|
125
|
-
} else {
|
|
126
|
-
filtered.push({
|
|
127
|
-
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
|
128
|
-
content: msg.content,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { system, messages: filtered };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Build the request body based on provider.
|
|
138
|
-
* @param {Array<{role: string, content: string}>} messages
|
|
139
|
-
* @param {object} options
|
|
140
|
-
* @returns {object}
|
|
141
|
-
*/
|
|
142
|
-
_buildBody(messages, options = {}) {
|
|
143
|
-
const { temperature, maxTokens, topP, stop, stream } = options;
|
|
144
|
-
|
|
145
|
-
if (this.provider === 'anthropic') {
|
|
146
|
-
const { system, messages: anthropicMessages } = this._toAnthropicFormat(messages);
|
|
147
|
-
const body = {
|
|
148
|
-
model: this.model,
|
|
149
|
-
messages: anthropicMessages,
|
|
150
|
-
max_tokens: maxTokens || 4096,
|
|
151
|
-
};
|
|
152
|
-
if (system) body.system = system;
|
|
153
|
-
if (temperature != null) body.temperature = temperature;
|
|
154
|
-
if (topP != null) body.top_p = topP;
|
|
155
|
-
if (stop) body.stop_sequences = Array.isArray(stop) ? stop : [stop];
|
|
156
|
-
if (stream) body.stream = true;
|
|
157
|
-
return body;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (this.provider === 'ollama') {
|
|
161
|
-
const body = {
|
|
162
|
-
model: this.model,
|
|
163
|
-
messages,
|
|
164
|
-
stream: !!stream,
|
|
165
|
-
};
|
|
166
|
-
if (temperature != null) body.options = { ...body.options, temperature };
|
|
167
|
-
if (topP != null) body.options = { ...body.options, top_p: topP };
|
|
168
|
-
return body;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// OpenAI-compatible (openai, lmstudio, azure, openrouter)
|
|
172
|
-
const body = {
|
|
173
|
-
model: this.model,
|
|
174
|
-
messages,
|
|
175
|
-
};
|
|
176
|
-
if (temperature != null) body.temperature = temperature;
|
|
177
|
-
if (maxTokens != null) body.max_tokens = maxTokens;
|
|
178
|
-
if (topP != null) body.top_p = topP;
|
|
179
|
-
if (stop) body.stop = stop;
|
|
180
|
-
if (stream) body.stream = true;
|
|
181
|
-
return body;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Extract assistant response text from a provider's response.
|
|
186
|
-
* @param {object} data - Response body
|
|
187
|
-
* @returns {string}
|
|
188
|
-
*/
|
|
189
|
-
_extractResponse(data) {
|
|
190
|
-
if (this.provider === 'anthropic') {
|
|
191
|
-
if (data.content && data.content.length > 0) {
|
|
192
|
-
return data.content
|
|
193
|
-
.filter((block) => block.type === 'text')
|
|
194
|
-
.map((block) => block.text)
|
|
195
|
-
.join('');
|
|
196
|
-
}
|
|
197
|
-
return '';
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (this.provider === 'ollama') {
|
|
201
|
-
return data.message ? data.message.content : '';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// OpenAI-compatible
|
|
205
|
-
if (data.choices && data.choices.length > 0) {
|
|
206
|
-
return data.choices[0].message.content || '';
|
|
207
|
-
}
|
|
208
|
-
return '';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Sleep for the given number of milliseconds.
|
|
213
|
-
* @param {number} ms
|
|
214
|
-
* @returns {Promise<void>}
|
|
215
|
-
*/
|
|
216
|
-
_sleep(ms) {
|
|
217
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Execute an HTTP request with retry logic for transient errors.
|
|
222
|
-
* @param {object} axiosConfig
|
|
223
|
-
* @returns {Promise<object>}
|
|
224
|
-
*/
|
|
225
|
-
async _requestWithRetry(axiosConfig) {
|
|
226
|
-
let lastError;
|
|
227
|
-
|
|
228
|
-
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
229
|
-
try {
|
|
230
|
-
const response = await axios(axiosConfig);
|
|
231
|
-
return response;
|
|
232
|
-
} catch (err) {
|
|
233
|
-
lastError = err;
|
|
234
|
-
const status = err.response ? err.response.status : null;
|
|
235
|
-
|
|
236
|
-
if (status && RETRY_STATUS_CODES.includes(status) && attempt < this.maxRetries) {
|
|
237
|
-
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
238
|
-
await this._sleep(delay);
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
throw this._wrapError(err);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
throw this._wrapError(lastError);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Wrap an axios error into a more descriptive error.
|
|
251
|
-
* @param {Error} err
|
|
252
|
-
* @returns {Error}
|
|
253
|
-
*/
|
|
254
|
-
_wrapError(err) {
|
|
255
|
-
if (err.response) {
|
|
256
|
-
const status = err.response.status;
|
|
257
|
-
const data = err.response.data;
|
|
258
|
-
const detail =
|
|
259
|
-
typeof data === 'string'
|
|
260
|
-
? data
|
|
261
|
-
: data && data.error
|
|
262
|
-
? typeof data.error === 'string'
|
|
263
|
-
? data.error
|
|
264
|
-
: data.error.message || JSON.stringify(data.error)
|
|
265
|
-
: JSON.stringify(data);
|
|
266
|
-
return new Error(`LLM request failed (${status}): ${detail}`);
|
|
267
|
-
}
|
|
268
|
-
if (err.code === 'ECONNABORTED') {
|
|
269
|
-
return new Error(`LLM request timed out after ${this.timeout}ms`);
|
|
270
|
-
}
|
|
271
|
-
return new Error(`LLM request failed: ${err.message}`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Send messages to the LLM and return the assistant's response text.
|
|
276
|
-
* @param {Array<{role: string, content: string}>} messages
|
|
277
|
-
* @param {object} [options]
|
|
278
|
-
* @param {number} [options.temperature]
|
|
279
|
-
* @param {number} [options.maxTokens]
|
|
280
|
-
* @param {number} [options.topP]
|
|
281
|
-
* @param {string|string[]} [options.stop]
|
|
282
|
-
* @returns {Promise<string>}
|
|
283
|
-
*/
|
|
284
|
-
async chat(messages, options = {}) {
|
|
285
|
-
const url = this._resolveUrl();
|
|
286
|
-
const headers = this._buildHeaders();
|
|
287
|
-
const body = this._buildBody(messages, { ...options, stream: false });
|
|
288
|
-
|
|
289
|
-
const response = await this._requestWithRetry({
|
|
290
|
-
method: 'post',
|
|
291
|
-
url,
|
|
292
|
-
headers,
|
|
293
|
-
data: body,
|
|
294
|
-
timeout: this.timeout,
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
return this._extractResponse(response.data);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Send messages to the LLM and return an async generator yielding text chunks.
|
|
302
|
-
* @param {Array<{role: string, content: string}>} messages
|
|
303
|
-
* @param {object} [options]
|
|
304
|
-
* @param {number} [options.temperature]
|
|
305
|
-
* @param {number} [options.maxTokens]
|
|
306
|
-
* @param {number} [options.topP]
|
|
307
|
-
* @param {string|string[]} [options.stop]
|
|
308
|
-
* @returns {AsyncGenerator<string>}
|
|
309
|
-
*/
|
|
310
|
-
async *streamChat(messages, options = {}) {
|
|
311
|
-
const url = this._resolveUrl();
|
|
312
|
-
const headers = this._buildHeaders();
|
|
313
|
-
const body = this._buildBody(messages, { ...options, stream: true });
|
|
314
|
-
|
|
315
|
-
let response;
|
|
316
|
-
try {
|
|
317
|
-
response = await axios({
|
|
318
|
-
method: 'post',
|
|
319
|
-
url,
|
|
320
|
-
headers,
|
|
321
|
-
data: body,
|
|
322
|
-
timeout: this.timeout,
|
|
323
|
-
responseType: 'stream',
|
|
324
|
-
});
|
|
325
|
-
} catch (err) {
|
|
326
|
-
throw this._wrapError(err);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const stream = response.data;
|
|
330
|
-
let buffer = '';
|
|
331
|
-
|
|
332
|
-
for await (const chunk of stream) {
|
|
333
|
-
buffer += chunk.toString();
|
|
334
|
-
const lines = buffer.split('\n');
|
|
335
|
-
buffer = lines.pop() || '';
|
|
336
|
-
|
|
337
|
-
for (const line of lines) {
|
|
338
|
-
const trimmed = line.trim();
|
|
339
|
-
if (!trimmed) continue;
|
|
340
|
-
|
|
341
|
-
if (this.provider === 'ollama') {
|
|
342
|
-
// Ollama streams JSON objects, one per line
|
|
343
|
-
try {
|
|
344
|
-
const parsed = JSON.parse(trimmed);
|
|
345
|
-
if (parsed.message && parsed.message.content) {
|
|
346
|
-
yield parsed.message.content;
|
|
347
|
-
}
|
|
348
|
-
if (parsed.done) return;
|
|
349
|
-
} catch {
|
|
350
|
-
// Incomplete JSON, skip
|
|
351
|
-
}
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// SSE format: data: {...}
|
|
356
|
-
if (!trimmed.startsWith('data:')) continue;
|
|
357
|
-
const payload = trimmed.slice(5).trim();
|
|
358
|
-
if (payload === '[DONE]') return;
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
const parsed = JSON.parse(payload);
|
|
362
|
-
|
|
363
|
-
if (this.provider === 'anthropic') {
|
|
364
|
-
// Anthropic streaming events
|
|
365
|
-
if (parsed.type === 'content_block_delta' && parsed.delta && parsed.delta.text) {
|
|
366
|
-
yield parsed.delta.text;
|
|
367
|
-
}
|
|
368
|
-
} else {
|
|
369
|
-
// OpenAI-compatible streaming
|
|
370
|
-
if (parsed.choices && parsed.choices.length > 0) {
|
|
371
|
-
const delta = parsed.choices[0].delta;
|
|
372
|
-
if (delta && delta.content) {
|
|
373
|
-
yield delta.content;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
} catch {
|
|
378
|
-
// Malformed JSON line, skip
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Process remaining buffer
|
|
384
|
-
if (buffer.trim()) {
|
|
385
|
-
const trimmed = buffer.trim();
|
|
386
|
-
if (this.provider === 'ollama') {
|
|
387
|
-
try {
|
|
388
|
-
const parsed = JSON.parse(trimmed);
|
|
389
|
-
if (parsed.message && parsed.message.content) {
|
|
390
|
-
yield parsed.message.content;
|
|
391
|
-
}
|
|
392
|
-
} catch {
|
|
393
|
-
// Incomplete JSON
|
|
394
|
-
}
|
|
395
|
-
} else if (trimmed.startsWith('data:')) {
|
|
396
|
-
const payload = trimmed.slice(5).trim();
|
|
397
|
-
if (payload !== '[DONE]') {
|
|
398
|
-
try {
|
|
399
|
-
const parsed = JSON.parse(payload);
|
|
400
|
-
if (this.provider === 'anthropic') {
|
|
401
|
-
if (parsed.type === 'content_block_delta' && parsed.delta && parsed.delta.text) {
|
|
402
|
-
yield parsed.delta.text;
|
|
403
|
-
}
|
|
404
|
-
} else if (parsed.choices && parsed.choices.length > 0) {
|
|
405
|
-
const delta = parsed.choices[0].delta;
|
|
406
|
-
if (delta && delta.content) {
|
|
407
|
-
yield delta.content;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
} catch {
|
|
411
|
-
// Malformed JSON
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
module.exports = LLMClient;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@sonar-autofix/llm-client",
|
|
3
|
-
"version": "0.0.2",
|
|
4
|
-
"description": "LLM client for the Sonar Autofix tool",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
-
},
|
|
9
|
-
"keywords": [
|
|
10
|
-
"sonar",
|
|
11
|
-
"autofix",
|
|
12
|
-
"github",
|
|
13
|
-
"llm"
|
|
14
|
-
],
|
|
15
|
-
"author": "Sonar Autofix Team",
|
|
16
|
-
"license": "MIT"
|
|
17
|
-
}
|