legacyver 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/skills/openspec-apply-change/SKILL.md +156 -0
- package/.agent/skills/openspec-archive-change/SKILL.md +114 -0
- package/.agent/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.agent/skills/openspec-continue-change/SKILL.md +118 -0
- package/.agent/skills/openspec-explore/SKILL.md +290 -0
- package/.agent/skills/openspec-ff-change/SKILL.md +101 -0
- package/.agent/skills/openspec-new-change/SKILL.md +74 -0
- package/.agent/skills/openspec-onboard/SKILL.md +529 -0
- package/.agent/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.agent/skills/openspec-verify-change/SKILL.md +168 -0
- package/.agent/workflows/opsx-apply.md +149 -0
- package/.agent/workflows/opsx-archive.md +154 -0
- package/.agent/workflows/opsx-bulk-archive.md +239 -0
- package/.agent/workflows/opsx-continue.md +111 -0
- package/.agent/workflows/opsx-explore.md +171 -0
- package/.agent/workflows/opsx-ff.md +91 -0
- package/.agent/workflows/opsx-new.md +66 -0
- package/.agent/workflows/opsx-onboard.md +522 -0
- package/.agent/workflows/opsx-sync.md +131 -0
- package/.agent/workflows/opsx-verify.md +161 -0
- package/.github/prompts/opsx-apply.prompt.md +149 -0
- package/.github/prompts/opsx-archive.prompt.md +154 -0
- package/.github/prompts/opsx-bulk-archive.prompt.md +239 -0
- package/.github/prompts/opsx-continue.prompt.md +111 -0
- package/.github/prompts/opsx-explore.prompt.md +171 -0
- package/.github/prompts/opsx-ff.prompt.md +91 -0
- package/.github/prompts/opsx-new.prompt.md +66 -0
- package/.github/prompts/opsx-onboard.prompt.md +522 -0
- package/.github/prompts/opsx-sync.prompt.md +131 -0
- package/.github/prompts/opsx-verify.prompt.md +161 -0
- package/.github/skills/openspec-apply-change/SKILL.md +156 -0
- package/.github/skills/openspec-archive-change/SKILL.md +114 -0
- package/.github/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.github/skills/openspec-continue-change/SKILL.md +118 -0
- package/.github/skills/openspec-explore/SKILL.md +290 -0
- package/.github/skills/openspec-ff-change/SKILL.md +101 -0
- package/.github/skills/openspec-new-change/SKILL.md +74 -0
- package/.github/skills/openspec-onboard/SKILL.md +529 -0
- package/.github/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.github/skills/openspec-verify-change/SKILL.md +168 -0
- package/.legacyverignore.example +43 -0
- package/.legacyverrc +7 -0
- package/.opencode/command/opsx-apply.md +149 -0
- package/.opencode/command/opsx-archive.md +154 -0
- package/.opencode/command/opsx-bulk-archive.md +239 -0
- package/.opencode/command/opsx-continue.md +111 -0
- package/.opencode/command/opsx-explore.md +171 -0
- package/.opencode/command/opsx-ff.md +91 -0
- package/.opencode/command/opsx-new.md +66 -0
- package/.opencode/command/opsx-onboard.md +522 -0
- package/.opencode/command/opsx-sync.md +131 -0
- package/.opencode/command/opsx-verify.md +161 -0
- package/.opencode/skills/openspec-apply-change/SKILL.md +156 -0
- package/.opencode/skills/openspec-archive-change/SKILL.md +114 -0
- package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.opencode/skills/openspec-continue-change/SKILL.md +118 -0
- package/.opencode/skills/openspec-explore/SKILL.md +290 -0
- package/.opencode/skills/openspec-ff-change/SKILL.md +101 -0
- package/.opencode/skills/openspec-new-change/SKILL.md +74 -0
- package/.opencode/skills/openspec-onboard/SKILL.md +529 -0
- package/.opencode/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.opencode/skills/openspec-verify-change/SKILL.md +168 -0
- package/LICENSE +1 -1
- package/README.md +128 -83
- package/bin/legacyver.js +48 -25
- package/legacyver-docs/SUMMARY.md +3 -0
- package/legacyver-docs/components.md +57 -0
- package/legacyver-docs/index.md +15 -0
- package/nul +2 -0
- package/package.json +23 -25
- package/src/cache/hash.js +9 -10
- package/src/cache/index.js +43 -65
- package/src/cli/commands/analyze.js +212 -190
- package/src/cli/commands/cache.js +15 -35
- package/src/cli/commands/init.js +63 -107
- package/src/cli/commands/providers.js +56 -81
- package/src/cli/commands/version.js +7 -10
- package/src/cli/ui.js +58 -77
- package/src/crawler/filters.js +41 -40
- package/src/crawler/index.js +52 -36
- package/src/crawler/manifest.js +31 -43
- package/src/crawler/walk.js +32 -38
- package/src/llm/chunker.js +34 -56
- package/src/llm/cost-estimator.js +68 -51
- package/src/llm/free-model.js +67 -0
- package/src/llm/index.js +22 -43
- package/src/llm/prompts.js +45 -33
- package/src/llm/providers/gemini.js +94 -0
- package/src/llm/providers/groq.js +55 -40
- package/src/llm/providers/ollama.js +38 -65
- package/src/llm/providers/openrouter.js +67 -0
- package/src/llm/queue.js +59 -88
- package/src/llm/re-prompter.js +41 -0
- package/src/llm/validator.js +72 -0
- package/src/parser/ast/generic.js +45 -222
- package/src/parser/ast/go.js +86 -205
- package/src/parser/ast/java.js +76 -146
- package/src/parser/ast/javascript.js +173 -241
- package/src/parser/ast/laravel/blade.js +56 -0
- package/src/parser/ast/laravel/classifier.js +30 -0
- package/src/parser/ast/laravel/controller.js +35 -0
- package/src/parser/ast/laravel/index.js +54 -0
- package/src/parser/ast/laravel/model.js +41 -0
- package/src/parser/ast/laravel/provider.js +28 -0
- package/src/parser/ast/laravel/routes.js +45 -0
- package/src/parser/ast/php.js +129 -0
- package/src/parser/ast/python.js +76 -199
- package/src/parser/ast/typescript.js +10 -244
- package/src/parser/body-extractor.js +40 -0
- package/src/parser/call-graph.js +50 -67
- package/src/parser/complexity-scorer.js +59 -0
- package/src/parser/index.js +61 -86
- package/src/parser/pattern-detector.js +71 -0
- package/src/parser/pkg-builder.js +36 -83
- package/src/renderer/html.js +63 -135
- package/src/renderer/index.js +23 -35
- package/src/renderer/json.js +17 -35
- package/src/renderer/markdown.js +83 -117
- package/src/utils/config.js +52 -53
- package/src/utils/errors.js +26 -41
- package/src/utils/logger.js +32 -53
- package/src/cli/flags.js +0 -87
- package/src/llm/providers/anthropic.js +0 -57
- package/src/llm/providers/google.js +0 -65
- package/src/llm/providers/openai.js +0 -52
- package/src/parser/ast/tree-sitter-init.js +0 -80
package/src/llm/index.js
CHANGED
|
@@ -1,49 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
* LLM provider factory.
|
|
3
|
-
* Reads provider from config/flags, returns correct provider instance.
|
|
4
|
-
* Throws NoApiKeyError with per-provider instructions if key missing.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import Conf from 'conf';
|
|
8
|
-
import { AnthropicProvider } from './providers/anthropic.js';
|
|
9
|
-
import { OpenAIProvider } from './providers/openai.js';
|
|
10
|
-
import { GoogleProvider } from './providers/google.js';
|
|
11
|
-
import { GroqProvider } from './providers/groq.js';
|
|
12
|
-
import { OllamaProvider } from './providers/ollama.js';
|
|
13
|
-
import { LegacyverError } from '../utils/errors.js';
|
|
14
|
-
|
|
15
|
-
const userConfig = new Conf({ projectName: 'legacyver' });
|
|
1
|
+
'use strict';
|
|
16
2
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
groq: GroqProvider,
|
|
22
|
-
ollama: OllamaProvider,
|
|
23
|
-
};
|
|
3
|
+
const { OpenRouterProvider } = require('./providers/openrouter');
|
|
4
|
+
const { OllamaProvider } = require('./providers/ollama');
|
|
5
|
+
const { GroqProvider } = require('./providers/groq');
|
|
6
|
+
const { GeminiProvider } = require('./providers/gemini');
|
|
24
7
|
|
|
25
8
|
/**
|
|
26
|
-
*
|
|
27
|
-
* @param {
|
|
28
|
-
* @returns {
|
|
9
|
+
* Provider factory — returns the appropriate LLM adapter.
|
|
10
|
+
* @param {Object} config
|
|
11
|
+
* @returns {OpenRouterProvider|OllamaProvider|GroqProvider|GeminiProvider}
|
|
29
12
|
*/
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
13
|
+
function createProvider(config) {
|
|
14
|
+
const provider = (config.provider || 'openrouter').toLowerCase();
|
|
15
|
+
switch (provider) {
|
|
16
|
+
case 'ollama':
|
|
17
|
+
return new OllamaProvider(config);
|
|
18
|
+
case 'groq':
|
|
19
|
+
return new GroqProvider(config);
|
|
20
|
+
case 'gemini':
|
|
21
|
+
return new GeminiProvider(config);
|
|
22
|
+
case 'openrouter':
|
|
23
|
+
default:
|
|
24
|
+
return new OpenRouterProvider(config);
|
|
39
25
|
}
|
|
40
|
-
|
|
41
|
-
// Try to get API key from user config if not in env
|
|
42
|
-
const savedKey = userConfig.get(`apiKeys.${providerName}`);
|
|
43
|
-
const providerConfig = {
|
|
44
|
-
model: config.model,
|
|
45
|
-
apiKey: savedKey || undefined,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return new ProviderClass(providerConfig);
|
|
49
26
|
}
|
|
27
|
+
|
|
28
|
+
module.exports = { createProvider };
|
package/src/llm/prompts.js
CHANGED
|
@@ -1,52 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SYSTEM_PROMPT = `You are a technical documentation writer. You will be given:
|
|
4
|
+
1. Extracted structural facts about a source file (JSON)
|
|
5
|
+
2. The raw source code of that file
|
|
6
|
+
|
|
7
|
+
Your job is to write clear, accurate Markdown documentation based ONLY on what is present in the code.
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- Berikan penjelasan yang DETAIL dan ELABORATIF — jangan hanya 1 kalimat singkat, jelaskan konteks, alur kerja, dan tujuannya
|
|
19
|
-
|
|
20
|
-
Format output (Markdown ketat, ikuti persis):
|
|
9
|
+
Rules:
|
|
10
|
+
- Do NOT infer behavior not explicitly shown in the code
|
|
11
|
+
- Do NOT mention external systems unless they appear in imports
|
|
12
|
+
- Do NOT fabricate function descriptions
|
|
13
|
+
- If a function's purpose is unclear from its body, say so honestly
|
|
14
|
+
- Do not mention any function, class, parameter, or behavior that does not appear in the FileFacts JSON or bodySnippet above
|
|
15
|
+
- For each function with complexityClass "moderate" or "complex", explain the logic described in the bodySnippet in plain language
|
|
16
|
+
- For each function with detectedPatterns[] non-empty, explicitly describe what that pattern does in context
|
|
17
|
+
|
|
18
|
+
Temperature: use 0.1 — factual output only.
|
|
19
|
+
|
|
20
|
+
Output format (strict Markdown):
|
|
21
21
|
## Overview
|
|
22
|
-
[2
|
|
22
|
+
[1-2 sentences about what this file does]
|
|
23
23
|
|
|
24
24
|
## Functions
|
|
25
|
-
[
|
|
26
|
-
- Paragraf deskripsi detail: apa yang dilakukan fungsi, alur kerjanya step-by-step, dan kapan digunakan
|
|
27
|
-
- Tabel parameter: | Nama | Tipe | Deskripsi |
|
|
28
|
-
- Keterangan nilai kembalian (return value) beserta penjelasannya]
|
|
25
|
+
[One ### subsection per exported function with: description, params table, return value]
|
|
29
26
|
|
|
30
27
|
## Dependencies
|
|
31
|
-
[
|
|
28
|
+
[Bullet list of imports with one-line description of each]
|
|
32
29
|
|
|
33
30
|
## Usage Example
|
|
34
|
-
[
|
|
31
|
+
[Only include if a clear usage pattern is visible in the code itself]`;
|
|
35
32
|
|
|
36
33
|
/**
|
|
37
|
-
* Build the user message for a single file
|
|
38
|
-
* @param {
|
|
39
|
-
* @param {string} rawSource
|
|
40
|
-
* @returns {string}
|
|
34
|
+
* Build the user message for a single file.
|
|
35
|
+
* @param {Object} fileFacts
|
|
36
|
+
* @param {string} rawSource
|
|
37
|
+
* @returns {string}
|
|
41
38
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
function buildUserMessage(fileFacts, rawSource) {
|
|
40
|
+
// Include bodySnippets inline in the facts JSON for moderate/complex functions
|
|
41
|
+
const factsForPrompt = {
|
|
42
|
+
...fileFacts,
|
|
43
|
+
functions: (fileFacts.functions || []).map(fn => {
|
|
44
|
+
const entry = { ...fn };
|
|
45
|
+
if (fn.complexityClass === 'moderate' || fn.complexityClass === 'complex') {
|
|
46
|
+
entry._promptHint = `Explain this function's logic in plain language based on its bodySnippet.`;
|
|
47
|
+
}
|
|
48
|
+
if (fn.detectedPatterns && fn.detectedPatterns.length > 0) {
|
|
49
|
+
entry._patternHint = `This function uses patterns: ${fn.detectedPatterns.join(', ')}. Describe what each does in context.`;
|
|
50
|
+
}
|
|
51
|
+
return entry;
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
44
54
|
|
|
45
55
|
return `FILE FACTS (extracted by static analysis):
|
|
46
|
-
${
|
|
56
|
+
${JSON.stringify(factsForPrompt, null, 2)}
|
|
47
57
|
|
|
48
58
|
SOURCE CODE:
|
|
49
59
|
${rawSource}
|
|
50
60
|
|
|
51
61
|
Generate documentation for this file following the system instructions.`;
|
|
52
62
|
}
|
|
63
|
+
|
|
64
|
+
module.exports = { SYSTEM_PROMPT, buildUserMessage };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NoApiKeyError } = require('../../utils/errors');
|
|
4
|
+
|
|
5
|
+
const GEMINI_BASE = 'https://generativelanguage.googleapis.com/v1beta';
|
|
6
|
+
const DEFAULT_MODEL = 'gemini-2.0-flash';
|
|
7
|
+
|
|
8
|
+
class GeminiProvider {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.apiKey = config.geminiApiKey || process.env.GEMINI_API_KEY;
|
|
11
|
+
if (!this.apiKey) throw new NoApiKeyError('gemini');
|
|
12
|
+
this.model = config.model || DEFAULT_MODEL;
|
|
13
|
+
this.name = 'gemini';
|
|
14
|
+
this.isFreeModel = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async complete(chunk) {
|
|
18
|
+
const url = `${GEMINI_BASE}/models/${this.model}:generateContent?key=${this.apiKey}`;
|
|
19
|
+
|
|
20
|
+
const body = {
|
|
21
|
+
contents: [
|
|
22
|
+
{
|
|
23
|
+
parts: [
|
|
24
|
+
{ text: chunk.systemPrompt + '\n\n' + chunk.userMessage },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
generationConfig: {
|
|
29
|
+
temperature: 0.3,
|
|
30
|
+
maxOutputTokens: 4096,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = await fetch(url, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
});
|
|
41
|
+
} catch (e) {
|
|
42
|
+
throw new Error(`Could not connect to Gemini API: ${e.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (response.status === 429) {
|
|
46
|
+
const body = await response.text();
|
|
47
|
+
const { RateLimitError } = require('../../utils/errors');
|
|
48
|
+
const headerVal = parseInt(response.headers.get('retry-after') || '0');
|
|
49
|
+
const retryAfter = Math.max(headerVal * 1000, 15000);
|
|
50
|
+
const err = new RateLimitError('gemini', retryAfter);
|
|
51
|
+
// Include Gemini's actual error detail so user can see what's wrong
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(body);
|
|
54
|
+
const detail = parsed.error && parsed.error.message ? parsed.error.message : body.substring(0, 200);
|
|
55
|
+
err.message = `Rate limit exceeded for provider "gemini": ${detail}`;
|
|
56
|
+
} catch (_) {
|
|
57
|
+
err.message = `Rate limit exceeded for provider "gemini": ${body.substring(0, 200)}`;
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
throw new Error(`Gemini API error ${response.status}: ${text.substring(0, 500)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const text = await response.text();
|
|
69
|
+
throw new Error(`Gemini API error ${response.status}: ${text}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const data = await response.json();
|
|
73
|
+
|
|
74
|
+
// Extract content from Gemini response format
|
|
75
|
+
let content = '';
|
|
76
|
+
if (data.candidates && data.candidates[0] && data.candidates[0].content) {
|
|
77
|
+
const parts = data.candidates[0].content.parts || [];
|
|
78
|
+
content = parts.map((p) => p.text || '').join('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract usage metadata
|
|
82
|
+
const usage = data.usageMetadata || {};
|
|
83
|
+
const tokensUsed = {
|
|
84
|
+
input: usage.promptTokenCount || 0,
|
|
85
|
+
output: usage.candidatesTokenCount || 0,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return { content, tokensUsed };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
estimateCost() { return 0; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { GeminiProvider };
|
|
@@ -1,52 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
* Groq LLM provider adapter.
|
|
3
|
-
* Uses groq-sdk.
|
|
4
|
-
* Default model: llama-3.3-70b-versatile.
|
|
5
|
-
*/
|
|
1
|
+
'use strict';
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
import { NoApiKeyError, RateLimitError } from '../../utils/errors.js';
|
|
3
|
+
const { NoApiKeyError } = require('../../utils/errors');
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.name = 'groq';
|
|
13
|
-
this.model = config.model || 'llama-3.3-70b-versatile';
|
|
14
|
-
|
|
15
|
-
const apiKey = config.apiKey || process.env.GROQ_API_KEY;
|
|
16
|
-
if (!apiKey) {
|
|
17
|
-
throw new NoApiKeyError('groq');
|
|
18
|
-
}
|
|
5
|
+
const GROQ_BASE = 'https://api.groq.com/openai/v1';
|
|
6
|
+
const DEFAULT_MODEL = 'llama-3.3-70b-versatile';
|
|
19
7
|
|
|
20
|
-
|
|
8
|
+
class GroqProvider {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.apiKey = config.groqApiKey || process.env.GROQ_API_KEY;
|
|
11
|
+
if (!this.apiKey) throw new NoApiKeyError('groq');
|
|
12
|
+
this.model = config.model || DEFAULT_MODEL;
|
|
13
|
+
this.name = 'groq';
|
|
14
|
+
this.isFreeModel = true; // Groq free tier
|
|
21
15
|
}
|
|
22
16
|
|
|
23
|
-
async complete(
|
|
17
|
+
async complete(chunk) {
|
|
18
|
+
const body = {
|
|
19
|
+
model: this.model,
|
|
20
|
+
messages: [
|
|
21
|
+
{ role: 'system', content: chunk.systemPrompt },
|
|
22
|
+
{ role: 'user', content: chunk.userMessage },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let response;
|
|
24
27
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
response = await fetch(`${GROQ_BASE}/chat/completions`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(body),
|
|
32
35
|
});
|
|
36
|
+
} catch (e) {
|
|
37
|
+
throw new Error(`Could not connect to Groq API: ${e.message}`);
|
|
38
|
+
}
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
throw err;
|
|
40
|
+
if (response.status === 429) {
|
|
41
|
+
const { RateLimitError } = require('../../utils/errors');
|
|
42
|
+
const headerVal = parseInt(response.headers.get('retry-after') || '0');
|
|
43
|
+
// Minimum 15s wait — Groq free tier needs breathing room
|
|
44
|
+
const retryAfter = Math.max(headerVal * 1000, 15000);
|
|
45
|
+
throw new RateLimitError('groq', retryAfter);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
throw new Error(`Groq API error ${response.status}: ${text}`);
|
|
46
51
|
}
|
|
47
|
-
}
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const data = await response.json();
|
|
54
|
+
const content = (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) || '';
|
|
55
|
+
const usage = data.usage || {};
|
|
56
|
+
const tokensUsed = {
|
|
57
|
+
input: usage.prompt_tokens || 0,
|
|
58
|
+
output: usage.completion_tokens || 0,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return { content, tokensUsed };
|
|
51
62
|
}
|
|
63
|
+
|
|
64
|
+
estimateCost() { return 0; }
|
|
52
65
|
}
|
|
66
|
+
|
|
67
|
+
module.exports = { GroqProvider };
|
|
@@ -1,85 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
* Ollama LLM provider adapter.
|
|
3
|
-
* HTTP client to local Ollama API (http://localhost:11434).
|
|
4
|
-
* No API key required.
|
|
5
|
-
* Default model: llama3.2.
|
|
6
|
-
*/
|
|
1
|
+
'use strict';
|
|
7
2
|
|
|
8
|
-
|
|
3
|
+
const logger = require('../../utils/logger');
|
|
9
4
|
|
|
10
|
-
const
|
|
5
|
+
const OLLAMA_BASE = 'http://localhost:11434';
|
|
6
|
+
const DEFAULT_MODEL = 'llama3.2';
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
constructor(config
|
|
8
|
+
class OllamaProvider {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.model = config.model || DEFAULT_MODEL;
|
|
14
11
|
this.name = 'ollama';
|
|
15
|
-
this.
|
|
16
|
-
this.baseUrl = config.baseUrl || process.env.OLLAMA_HOST || DEFAULT_BASE_URL;
|
|
12
|
+
this.isFreeModel = true;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
async complete(
|
|
20
|
-
const
|
|
21
|
-
|
|
15
|
+
async complete(chunk) {
|
|
16
|
+
const body = {
|
|
17
|
+
model: this.model,
|
|
18
|
+
stream: false,
|
|
19
|
+
messages: [
|
|
20
|
+
{ role: 'system', content: chunk.systemPrompt },
|
|
21
|
+
{ role: 'user', content: chunk.userMessage },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
22
24
|
|
|
23
25
|
let response;
|
|
24
26
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{ role: 'system', content: request.systemPrompt },
|
|
36
|
-
{ role: 'user', content: request.userMessage },
|
|
37
|
-
],
|
|
38
|
-
stream: false,
|
|
39
|
-
options: {
|
|
40
|
-
num_predict: 2048,
|
|
41
|
-
},
|
|
42
|
-
}),
|
|
43
|
-
});
|
|
44
|
-
} finally {
|
|
45
|
-
clearTimeout(timer);
|
|
46
|
-
}
|
|
47
|
-
} catch (err) {
|
|
48
|
-
if (err.name === 'AbortError') {
|
|
49
|
-
throw new LegacyverError(
|
|
50
|
-
`Ollama request timed out after ${timeoutMs / 1000}s. Try a smaller file or faster model.`,
|
|
51
|
-
{ exitCode: 1, suggestion: 'Use --concurrency 1 to reduce load on Ollama' }
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
throw new LegacyverError(
|
|
55
|
-
`Cannot connect to Ollama at ${this.baseUrl}. Is Ollama running?`,
|
|
56
|
-
{
|
|
57
|
-
exitCode: 1,
|
|
58
|
-
suggestion: 'Start Ollama with "ollama serve" or install from https://ollama.ai',
|
|
59
|
-
}
|
|
27
|
+
response = await fetch(`${OLLAMA_BASE}/api/chat`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
} catch (e) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Could not connect to Ollama at ${OLLAMA_BASE}. ` +
|
|
35
|
+
`Make sure Ollama is running: run \`ollama serve\` in a terminal, ` +
|
|
36
|
+
`then \`ollama pull ${this.model}\` to download the model.`
|
|
60
37
|
);
|
|
61
38
|
}
|
|
62
39
|
|
|
63
40
|
if (!response.ok) {
|
|
64
|
-
const
|
|
65
|
-
throw new
|
|
66
|
-
`Ollama returned ${response.status}: ${body}`,
|
|
67
|
-
{ exitCode: 1 }
|
|
68
|
-
);
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
throw new Error(`Ollama API error ${response.status}: ${text}`);
|
|
69
43
|
}
|
|
70
44
|
|
|
71
45
|
const data = await response.json();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
input: data.prompt_eval_count || 0,
|
|
77
|
-
output: data.eval_count || 0,
|
|
78
|
-
},
|
|
46
|
+
const content = data.message && data.message.content || '';
|
|
47
|
+
const tokensUsed = {
|
|
48
|
+
input: data.prompt_eval_count || 0,
|
|
49
|
+
output: data.eval_count || 0,
|
|
79
50
|
};
|
|
80
|
-
}
|
|
81
51
|
|
|
82
|
-
|
|
83
|
-
return 0; // Local, free
|
|
52
|
+
return { content, tokensUsed };
|
|
84
53
|
}
|
|
54
|
+
|
|
55
|
+
estimateCost() { return 0; }
|
|
85
56
|
}
|
|
57
|
+
|
|
58
|
+
module.exports = { OllamaProvider };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NoApiKeyError, RateLimitError } = require('../../utils/errors');
|
|
4
|
+
const logger = require('../../utils/logger');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MODEL = 'meta-llama/llama-3.3-70b-instruct:free';
|
|
7
|
+
|
|
8
|
+
class OpenRouterProvider {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.apiKey = config.apiKey || process.env.OPENROUTER_API_KEY;
|
|
11
|
+
if (!this.apiKey) {
|
|
12
|
+
throw new NoApiKeyError('openrouter');
|
|
13
|
+
}
|
|
14
|
+
this.model = config.model || DEFAULT_MODEL;
|
|
15
|
+
this.isFreeModel = this.model.endsWith(':free');
|
|
16
|
+
this.name = 'openrouter';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async complete(chunk) {
|
|
20
|
+
const body = {
|
|
21
|
+
model: this.model,
|
|
22
|
+
temperature: 0.1,
|
|
23
|
+
messages: [
|
|
24
|
+
{ role: 'system', content: chunk.systemPrompt },
|
|
25
|
+
{ role: 'user', content: chunk.userMessage },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
34
|
+
'HTTP-Referer': 'https://github.com/user/legacyver',
|
|
35
|
+
'X-Title': 'Legacyver',
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(body),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (response.status === 429) {
|
|
41
|
+
const retryAfter = parseInt(response.headers.get('retry-after') || '1') * 1000;
|
|
42
|
+
throw new RateLimitError('openrouter', retryAfter);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const text = await response.text();
|
|
47
|
+
throw new Error(`OpenRouter API error ${response.status}: ${text}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
const content = data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content || '';
|
|
52
|
+
const tokensUsed = {
|
|
53
|
+
input: data.usage && data.usage.prompt_tokens || 0,
|
|
54
|
+
output: data.usage && data.usage.completion_tokens || 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return { content, tokensUsed };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
estimateCost(inputTokens, outputTokens) {
|
|
61
|
+
if (this.isFreeModel) return 0;
|
|
62
|
+
// Rough: use fallback rates
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { OpenRouterProvider };
|