tlc-claude-code 1.7.0 → 1.8.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/package.json +1 -1
- package/server/lib/code-gate/first-commit-audit.js +138 -0
- package/server/lib/code-gate/first-commit-audit.test.js +203 -0
- package/server/lib/code-gate/multi-model-reviewer.js +172 -0
- package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
- package/server/lib/infra/infra-generator.js +331 -0
- package/server/lib/infra/infra-generator.test.js +146 -0
- package/server/lib/llm/adapters/api-adapter.js +95 -0
- package/server/lib/llm/adapters/api-adapter.test.js +81 -0
- package/server/lib/llm/adapters/codex-adapter.js +85 -0
- package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
- package/server/lib/llm/adapters/gemini-adapter.js +100 -0
- package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
- package/server/lib/llm/index.js +109 -0
- package/server/lib/llm/index.test.js +147 -0
- package/server/lib/llm/provider-executor.js +168 -0
- package/server/lib/llm/provider-executor.test.js +244 -0
- package/server/lib/llm/provider-registry.js +104 -0
- package/server/lib/llm/provider-registry.test.js +157 -0
- package/server/lib/llm/review-service.js +222 -0
- package/server/lib/llm/review-service.test.js +220 -0
- package/server/lib/shame/shame-registry.js +224 -0
- package/server/lib/shame/shame-registry.test.js +202 -0
- package/server/lib/standards/cleanup-dry-run.js +254 -0
- package/server/lib/standards/cleanup-dry-run.test.js +220 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Adapter
|
|
3
|
+
*
|
|
4
|
+
* HTTP adapter for OpenAI-compatible APIs (LiteLLM, direct).
|
|
5
|
+
*
|
|
6
|
+
* @module llm/adapters/api-adapter
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT = 60000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build HTTP request for OpenAI-compatible API
|
|
13
|
+
* @param {string} prompt - Prompt text
|
|
14
|
+
* @param {Object} options - Adapter options
|
|
15
|
+
* @returns {Object} Request specification
|
|
16
|
+
*/
|
|
17
|
+
function buildRequest(prompt, options = {}) {
|
|
18
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
19
|
+
|
|
20
|
+
if (options.apiKey) {
|
|
21
|
+
headers['Authorization'] = 'Bearer ' + options.apiKey;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
url: options.url,
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers,
|
|
28
|
+
body: {
|
|
29
|
+
model: options.model,
|
|
30
|
+
messages: [{ role: 'user', content: prompt }],
|
|
31
|
+
},
|
|
32
|
+
timeout: options.timeout || DEFAULT_TIMEOUT,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse OpenAI-format API response
|
|
38
|
+
* @param {Object|null} data - Response JSON body
|
|
39
|
+
* @param {Object} httpResponse - HTTP response metadata
|
|
40
|
+
* @returns {Object} Parsed response
|
|
41
|
+
*/
|
|
42
|
+
function parseApiResponse(data, httpResponse = {}) {
|
|
43
|
+
if (httpResponse.status === 429) {
|
|
44
|
+
return {
|
|
45
|
+
error: 'Rate limited (429)',
|
|
46
|
+
retryable: true,
|
|
47
|
+
response: '',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!data || !data.choices) {
|
|
52
|
+
return {
|
|
53
|
+
error: 'Invalid API response',
|
|
54
|
+
retryable: false,
|
|
55
|
+
response: '',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const content = data.choices[0]?.message?.content || '';
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
response: content,
|
|
63
|
+
model: data.model || 'unknown',
|
|
64
|
+
tokens: data.usage?.total_tokens || 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create an API adapter instance
|
|
70
|
+
* @param {Object} config - Adapter config
|
|
71
|
+
* @returns {Object} Adapter with execute interface
|
|
72
|
+
*/
|
|
73
|
+
function createAdapter(config = {}) {
|
|
74
|
+
return {
|
|
75
|
+
name: 'api',
|
|
76
|
+
execute: async (prompt, deps = {}) => {
|
|
77
|
+
const req = buildRequest(prompt, config);
|
|
78
|
+
const { executeApiProvider } = deps;
|
|
79
|
+
if (!executeApiProvider) {
|
|
80
|
+
throw new Error('executeApiProvider dependency required');
|
|
81
|
+
}
|
|
82
|
+
const result = await executeApiProvider(prompt, {
|
|
83
|
+
...config,
|
|
84
|
+
fetch: deps.fetch,
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
buildRequest,
|
|
93
|
+
parseApiResponse,
|
|
94
|
+
createAdapter,
|
|
95
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Adapter Tests
|
|
3
|
+
*
|
|
4
|
+
* HTTP adapter for OpenAI-compatible APIs (LiteLLM, direct).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
buildRequest,
|
|
10
|
+
parseApiResponse,
|
|
11
|
+
createAdapter,
|
|
12
|
+
} = require('./api-adapter.js');
|
|
13
|
+
|
|
14
|
+
describe('API Adapter', () => {
|
|
15
|
+
describe('buildRequest', () => {
|
|
16
|
+
it('sends correct HTTP request body', () => {
|
|
17
|
+
const req = buildRequest('Review code', {
|
|
18
|
+
url: 'http://localhost:4000/v1/chat/completions',
|
|
19
|
+
model: 'gpt-4o',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(req.url).toBe('http://localhost:4000/v1/chat/completions');
|
|
23
|
+
expect(req.body.model).toBe('gpt-4o');
|
|
24
|
+
expect(req.body.messages).toBeDefined();
|
|
25
|
+
expect(req.body.messages[0].content).toContain('Review code');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('includes auth header from config', () => {
|
|
29
|
+
const req = buildRequest('prompt', {
|
|
30
|
+
url: 'http://api.example.com/v1/chat/completions',
|
|
31
|
+
model: 'gpt-4o',
|
|
32
|
+
apiKey: 'sk-test-key',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(req.headers['Authorization']).toBe('Bearer sk-test-key');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('respects timeout', () => {
|
|
39
|
+
const req = buildRequest('prompt', {
|
|
40
|
+
url: 'http://localhost:4000/v1/chat/completions',
|
|
41
|
+
model: 'gpt-4o',
|
|
42
|
+
timeout: 30000,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(req.timeout).toBe(30000);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('parseApiResponse', () => {
|
|
50
|
+
it('parses OpenAI-format response', () => {
|
|
51
|
+
const apiResp = {
|
|
52
|
+
choices: [{ message: { content: '{"findings": [], "summary": "Clean"}' } }],
|
|
53
|
+
model: 'gpt-4o',
|
|
54
|
+
usage: { total_tokens: 500 },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const result = parseApiResponse(apiResp);
|
|
58
|
+
expect(result.response).toContain('Clean');
|
|
59
|
+
expect(result.model).toBe('gpt-4o');
|
|
60
|
+
expect(result.tokens).toBe(500);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles rate limiting (429)', () => {
|
|
64
|
+
const result = parseApiResponse(null, { status: 429 });
|
|
65
|
+
expect(result.error).toBeDefined();
|
|
66
|
+
expect(result.retryable).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('createAdapter', () => {
|
|
71
|
+
it('implements execute interface', () => {
|
|
72
|
+
const adapter = createAdapter({
|
|
73
|
+
url: 'http://localhost:4000/v1/chat/completions',
|
|
74
|
+
model: 'gpt-4o',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(adapter.name).toBe('api');
|
|
78
|
+
expect(adapter.execute).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Format prompts for Codex CLI and parse its output.
|
|
5
|
+
*
|
|
6
|
+
* @module llm/adapters/codex-adapter
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT = 60000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build Codex CLI command
|
|
13
|
+
* @param {string} prompt - Prompt text
|
|
14
|
+
* @param {Object} options - Adapter options
|
|
15
|
+
* @returns {Object} Command specification
|
|
16
|
+
*/
|
|
17
|
+
function buildCommand(prompt, options = {}) {
|
|
18
|
+
const args = ['--quiet'];
|
|
19
|
+
|
|
20
|
+
if (options.model) {
|
|
21
|
+
args.push('--model', options.model);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
command: 'codex',
|
|
26
|
+
args,
|
|
27
|
+
prompt,
|
|
28
|
+
timeout: options.timeout || DEFAULT_TIMEOUT,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse Codex response
|
|
34
|
+
* @param {string} output - Raw CLI output
|
|
35
|
+
* @returns {Object} Parsed response
|
|
36
|
+
*/
|
|
37
|
+
function parseResponse(output) {
|
|
38
|
+
const trimmed = output.trim();
|
|
39
|
+
|
|
40
|
+
// Try JSON parse first
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(trimmed);
|
|
43
|
+
return {
|
|
44
|
+
findings: parsed.findings || [],
|
|
45
|
+
summary: parsed.summary || '',
|
|
46
|
+
raw: trimmed,
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
// Not JSON — return raw
|
|
50
|
+
return {
|
|
51
|
+
raw: trimmed,
|
|
52
|
+
findings: [],
|
|
53
|
+
summary: trimmed,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a Codex adapter instance
|
|
60
|
+
* @param {Object} config - Adapter config
|
|
61
|
+
* @returns {Object} Adapter with execute interface
|
|
62
|
+
*/
|
|
63
|
+
function createAdapter(config = {}) {
|
|
64
|
+
return {
|
|
65
|
+
name: 'codex',
|
|
66
|
+
execute: async (prompt, deps = {}) => {
|
|
67
|
+
const cmd = buildCommand(prompt, config);
|
|
68
|
+
const { executeCliProvider } = deps;
|
|
69
|
+
if (!executeCliProvider) {
|
|
70
|
+
throw new Error('executeCliProvider dependency required');
|
|
71
|
+
}
|
|
72
|
+
const result = await executeCliProvider(prompt, {
|
|
73
|
+
...cmd,
|
|
74
|
+
spawn: deps.spawn,
|
|
75
|
+
});
|
|
76
|
+
return parseResponse(result.response);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
buildCommand,
|
|
83
|
+
parseResponse,
|
|
84
|
+
createAdapter,
|
|
85
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI Adapter Tests
|
|
3
|
+
*
|
|
4
|
+
* Format prompts for Codex CLI, parse its output.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
buildCommand,
|
|
10
|
+
parseResponse,
|
|
11
|
+
createAdapter,
|
|
12
|
+
} = require('./codex-adapter.js');
|
|
13
|
+
|
|
14
|
+
describe('Codex Adapter', () => {
|
|
15
|
+
describe('buildCommand', () => {
|
|
16
|
+
it('builds correct CLI command', () => {
|
|
17
|
+
const cmd = buildCommand('Review this code', { model: 'gpt-4o' });
|
|
18
|
+
expect(cmd.command).toBe('codex');
|
|
19
|
+
expect(cmd.args).toContain('--quiet');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('passes model flag from config', () => {
|
|
23
|
+
const cmd = buildCommand('prompt', { model: 'o3-mini' });
|
|
24
|
+
expect(cmd.args).toContain('--model');
|
|
25
|
+
expect(cmd.args).toContain('o3-mini');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('respects timeout', () => {
|
|
29
|
+
const cmd = buildCommand('prompt', { timeout: 30000 });
|
|
30
|
+
expect(cmd.timeout).toBe(30000);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('parseResponse', () => {
|
|
35
|
+
it('parses JSON response', () => {
|
|
36
|
+
const json = JSON.stringify({ findings: [{ severity: 'high', message: 'XSS' }], summary: 'Issues found' });
|
|
37
|
+
const result = parseResponse(json);
|
|
38
|
+
expect(result.findings).toHaveLength(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('handles non-JSON output gracefully', () => {
|
|
42
|
+
const result = parseResponse('This is a plain text review response with some issues noted.');
|
|
43
|
+
expect(result.raw).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('createAdapter', () => {
|
|
48
|
+
it('implements execute interface', () => {
|
|
49
|
+
const adapter = createAdapter({ model: 'gpt-4o' });
|
|
50
|
+
expect(adapter.name).toBe('codex');
|
|
51
|
+
expect(adapter.execute).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Format prompts for Gemini CLI and parse its output.
|
|
5
|
+
*
|
|
6
|
+
* @module llm/adapters/gemini-adapter
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT = 60000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build Gemini CLI command
|
|
13
|
+
* @param {string} prompt - Prompt text
|
|
14
|
+
* @param {Object} options - Adapter options
|
|
15
|
+
* @returns {Object} Command specification
|
|
16
|
+
*/
|
|
17
|
+
function buildCommand(prompt, options = {}) {
|
|
18
|
+
const args = [];
|
|
19
|
+
|
|
20
|
+
if (options.model) {
|
|
21
|
+
args.push('--model', options.model);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
command: 'gemini',
|
|
26
|
+
args,
|
|
27
|
+
prompt,
|
|
28
|
+
timeout: options.timeout || DEFAULT_TIMEOUT,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse Gemini response (may be markdown or JSON in code block)
|
|
34
|
+
* @param {string} output - Raw CLI output
|
|
35
|
+
* @returns {Object} Parsed response
|
|
36
|
+
*/
|
|
37
|
+
function parseResponse(output) {
|
|
38
|
+
const trimmed = output.trim();
|
|
39
|
+
|
|
40
|
+
// Try to extract JSON from markdown code block
|
|
41
|
+
const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
42
|
+
if (codeBlockMatch) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(codeBlockMatch[1].trim());
|
|
45
|
+
return {
|
|
46
|
+
findings: parsed.findings || [],
|
|
47
|
+
summary: parsed.summary || '',
|
|
48
|
+
raw: trimmed,
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
// JSON in code block but invalid — fall through
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Try direct JSON parse
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(trimmed);
|
|
58
|
+
return {
|
|
59
|
+
findings: parsed.findings || [],
|
|
60
|
+
summary: parsed.summary || '',
|
|
61
|
+
raw: trimmed,
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
// Markdown/text response
|
|
65
|
+
return {
|
|
66
|
+
raw: trimmed,
|
|
67
|
+
findings: [],
|
|
68
|
+
summary: trimmed,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a Gemini adapter instance
|
|
75
|
+
* @param {Object} config - Adapter config
|
|
76
|
+
* @returns {Object} Adapter with execute interface
|
|
77
|
+
*/
|
|
78
|
+
function createAdapter(config = {}) {
|
|
79
|
+
return {
|
|
80
|
+
name: 'gemini',
|
|
81
|
+
execute: async (prompt, deps = {}) => {
|
|
82
|
+
const cmd = buildCommand(prompt, config);
|
|
83
|
+
const { executeCliProvider } = deps;
|
|
84
|
+
if (!executeCliProvider) {
|
|
85
|
+
throw new Error('executeCliProvider dependency required');
|
|
86
|
+
}
|
|
87
|
+
const result = await executeCliProvider(prompt, {
|
|
88
|
+
...cmd,
|
|
89
|
+
spawn: deps.spawn,
|
|
90
|
+
});
|
|
91
|
+
return parseResponse(result.response);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
buildCommand,
|
|
98
|
+
parseResponse,
|
|
99
|
+
createAdapter,
|
|
100
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Adapter Tests
|
|
3
|
+
*
|
|
4
|
+
* Format prompts for Gemini CLI, parse its output.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
buildCommand,
|
|
10
|
+
parseResponse,
|
|
11
|
+
createAdapter,
|
|
12
|
+
} = require('./gemini-adapter.js');
|
|
13
|
+
|
|
14
|
+
describe('Gemini Adapter', () => {
|
|
15
|
+
describe('buildCommand', () => {
|
|
16
|
+
it('builds correct CLI command', () => {
|
|
17
|
+
const cmd = buildCommand('Review this code', {});
|
|
18
|
+
expect(cmd.command).toBe('gemini');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('passes model flag from config', () => {
|
|
22
|
+
const cmd = buildCommand('prompt', { model: 'gemini-2.5-pro' });
|
|
23
|
+
expect(cmd.args).toContain('--model');
|
|
24
|
+
expect(cmd.args).toContain('gemini-2.5-pro');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('respects timeout', () => {
|
|
28
|
+
const cmd = buildCommand('prompt', { timeout: 45000 });
|
|
29
|
+
expect(cmd.timeout).toBe(45000);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('parseResponse', () => {
|
|
34
|
+
it('parses markdown response', () => {
|
|
35
|
+
const markdown = '## Issues\n- Line 5: XSS vulnerability\n- Line 12: Missing validation';
|
|
36
|
+
const result = parseResponse(markdown);
|
|
37
|
+
expect(result.raw).toContain('XSS');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('handles structured output mode', () => {
|
|
41
|
+
const json = '```json\n{"findings": [{"severity": "high", "message": "SQL injection"}]}\n```';
|
|
42
|
+
const result = parseResponse(json);
|
|
43
|
+
expect(result.findings).toHaveLength(1);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('createAdapter', () => {
|
|
48
|
+
it('implements execute interface', () => {
|
|
49
|
+
const adapter = createAdapter({ model: 'gemini-2.5-pro' });
|
|
50
|
+
expect(adapter.name).toBe('gemini');
|
|
51
|
+
expect(adapter.execute).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Service - Unified API
|
|
3
|
+
*
|
|
4
|
+
* createLLMService(config) → { review(diff), execute(prompt), health() }
|
|
5
|
+
*
|
|
6
|
+
* @module llm
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { createExecutor } = require('./provider-executor.js');
|
|
10
|
+
const { createRegistry } = require('./provider-registry.js');
|
|
11
|
+
const { createReviewService } = require('./review-service.js');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create the unified LLM service
|
|
15
|
+
* @param {Object} config - Service configuration
|
|
16
|
+
* @param {Object} config.providers - Provider configs { name: { type, command, capabilities } }
|
|
17
|
+
* @param {boolean} config.multiModel - Enable multi-model review
|
|
18
|
+
* @param {number} config.timeout - Default timeout
|
|
19
|
+
* @param {Object} deps - Injectable dependencies
|
|
20
|
+
* @param {Function} deps.healthCheck - Health check function
|
|
21
|
+
* @param {Function} deps.spawn - Process spawn function
|
|
22
|
+
* @param {Function} deps.fetch - HTTP fetch function
|
|
23
|
+
* @returns {Object} Service with review, execute, health methods
|
|
24
|
+
*/
|
|
25
|
+
function createLLMService(config = {}, deps = {}) {
|
|
26
|
+
const { providers = {}, multiModel = false, timeout } = config;
|
|
27
|
+
|
|
28
|
+
// Build health check from deps
|
|
29
|
+
const healthCheck = deps.healthCheck || (() => Promise.resolve({ available: false }));
|
|
30
|
+
|
|
31
|
+
// Create registry and load providers
|
|
32
|
+
const registry = createRegistry({ healthCheck, cacheTTL: config.cacheTTL });
|
|
33
|
+
registry.loadFromConfig({ providers });
|
|
34
|
+
|
|
35
|
+
// Create executor with real spawn/fetch
|
|
36
|
+
const executor = createExecutor({
|
|
37
|
+
spawn: deps.spawn,
|
|
38
|
+
fetch: deps.fetch,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Create review service
|
|
42
|
+
const reviewService = createReviewService({
|
|
43
|
+
registry,
|
|
44
|
+
executor,
|
|
45
|
+
multiModel,
|
|
46
|
+
timeout,
|
|
47
|
+
standards: config.standards || '',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
/**
|
|
52
|
+
* Review a diff using configured providers
|
|
53
|
+
* @param {string} diff - Git diff content
|
|
54
|
+
* @param {Object} options - Review options
|
|
55
|
+
* @returns {Promise<Object>} { findings, summary, provider, latency }
|
|
56
|
+
*/
|
|
57
|
+
review: (diff, options) => reviewService.review(diff, options),
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Execute a generic prompt against best available provider
|
|
61
|
+
* @param {string} prompt - Prompt text
|
|
62
|
+
* @param {Object} options - Execution options
|
|
63
|
+
* @returns {Promise<Object>} { response, model, latency }
|
|
64
|
+
*/
|
|
65
|
+
execute: async (prompt, options = {}) => {
|
|
66
|
+
const capability = options.capability || 'code-gen';
|
|
67
|
+
const provider = await registry.getBestProvider(capability);
|
|
68
|
+
|
|
69
|
+
if (!provider) {
|
|
70
|
+
// Fall back to first available provider
|
|
71
|
+
const allProviders = registry.list();
|
|
72
|
+
if (allProviders.length === 0) {
|
|
73
|
+
return { response: '', error: 'No providers configured' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
return await executor.execute(prompt, { ...allProviders[0], timeout });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return { response: '', error: err.message };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
return await executor.execute(prompt, { ...provider, timeout });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return { response: '', error: err.message };
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get health status of all providers
|
|
92
|
+
* @returns {Promise<Object>} { providers: { name: status } }
|
|
93
|
+
*/
|
|
94
|
+
health: async () => {
|
|
95
|
+
const allProviders = registry.list();
|
|
96
|
+
const statuses = {};
|
|
97
|
+
|
|
98
|
+
for (const provider of allProviders) {
|
|
99
|
+
statuses[provider.name] = await registry.checkHealth(provider.name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { providers: statuses };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
createLLMService,
|
|
109
|
+
};
|