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.
Files changed (126) hide show
  1. package/.agent/skills/openspec-apply-change/SKILL.md +156 -0
  2. package/.agent/skills/openspec-archive-change/SKILL.md +114 -0
  3. package/.agent/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  4. package/.agent/skills/openspec-continue-change/SKILL.md +118 -0
  5. package/.agent/skills/openspec-explore/SKILL.md +290 -0
  6. package/.agent/skills/openspec-ff-change/SKILL.md +101 -0
  7. package/.agent/skills/openspec-new-change/SKILL.md +74 -0
  8. package/.agent/skills/openspec-onboard/SKILL.md +529 -0
  9. package/.agent/skills/openspec-sync-specs/SKILL.md +138 -0
  10. package/.agent/skills/openspec-verify-change/SKILL.md +168 -0
  11. package/.agent/workflows/opsx-apply.md +149 -0
  12. package/.agent/workflows/opsx-archive.md +154 -0
  13. package/.agent/workflows/opsx-bulk-archive.md +239 -0
  14. package/.agent/workflows/opsx-continue.md +111 -0
  15. package/.agent/workflows/opsx-explore.md +171 -0
  16. package/.agent/workflows/opsx-ff.md +91 -0
  17. package/.agent/workflows/opsx-new.md +66 -0
  18. package/.agent/workflows/opsx-onboard.md +522 -0
  19. package/.agent/workflows/opsx-sync.md +131 -0
  20. package/.agent/workflows/opsx-verify.md +161 -0
  21. package/.github/prompts/opsx-apply.prompt.md +149 -0
  22. package/.github/prompts/opsx-archive.prompt.md +154 -0
  23. package/.github/prompts/opsx-bulk-archive.prompt.md +239 -0
  24. package/.github/prompts/opsx-continue.prompt.md +111 -0
  25. package/.github/prompts/opsx-explore.prompt.md +171 -0
  26. package/.github/prompts/opsx-ff.prompt.md +91 -0
  27. package/.github/prompts/opsx-new.prompt.md +66 -0
  28. package/.github/prompts/opsx-onboard.prompt.md +522 -0
  29. package/.github/prompts/opsx-sync.prompt.md +131 -0
  30. package/.github/prompts/opsx-verify.prompt.md +161 -0
  31. package/.github/skills/openspec-apply-change/SKILL.md +156 -0
  32. package/.github/skills/openspec-archive-change/SKILL.md +114 -0
  33. package/.github/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  34. package/.github/skills/openspec-continue-change/SKILL.md +118 -0
  35. package/.github/skills/openspec-explore/SKILL.md +290 -0
  36. package/.github/skills/openspec-ff-change/SKILL.md +101 -0
  37. package/.github/skills/openspec-new-change/SKILL.md +74 -0
  38. package/.github/skills/openspec-onboard/SKILL.md +529 -0
  39. package/.github/skills/openspec-sync-specs/SKILL.md +138 -0
  40. package/.github/skills/openspec-verify-change/SKILL.md +168 -0
  41. package/.legacyverignore.example +43 -0
  42. package/.legacyverrc +7 -0
  43. package/.opencode/command/opsx-apply.md +149 -0
  44. package/.opencode/command/opsx-archive.md +154 -0
  45. package/.opencode/command/opsx-bulk-archive.md +239 -0
  46. package/.opencode/command/opsx-continue.md +111 -0
  47. package/.opencode/command/opsx-explore.md +171 -0
  48. package/.opencode/command/opsx-ff.md +91 -0
  49. package/.opencode/command/opsx-new.md +66 -0
  50. package/.opencode/command/opsx-onboard.md +522 -0
  51. package/.opencode/command/opsx-sync.md +131 -0
  52. package/.opencode/command/opsx-verify.md +161 -0
  53. package/.opencode/skills/openspec-apply-change/SKILL.md +156 -0
  54. package/.opencode/skills/openspec-archive-change/SKILL.md +114 -0
  55. package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  56. package/.opencode/skills/openspec-continue-change/SKILL.md +118 -0
  57. package/.opencode/skills/openspec-explore/SKILL.md +290 -0
  58. package/.opencode/skills/openspec-ff-change/SKILL.md +101 -0
  59. package/.opencode/skills/openspec-new-change/SKILL.md +74 -0
  60. package/.opencode/skills/openspec-onboard/SKILL.md +529 -0
  61. package/.opencode/skills/openspec-sync-specs/SKILL.md +138 -0
  62. package/.opencode/skills/openspec-verify-change/SKILL.md +168 -0
  63. package/LICENSE +1 -1
  64. package/README.md +128 -83
  65. package/bin/legacyver.js +48 -25
  66. package/legacyver-docs/SUMMARY.md +3 -0
  67. package/legacyver-docs/components.md +57 -0
  68. package/legacyver-docs/index.md +15 -0
  69. package/nul +2 -0
  70. package/package.json +23 -25
  71. package/src/cache/hash.js +9 -10
  72. package/src/cache/index.js +43 -65
  73. package/src/cli/commands/analyze.js +212 -190
  74. package/src/cli/commands/cache.js +15 -35
  75. package/src/cli/commands/init.js +63 -107
  76. package/src/cli/commands/providers.js +56 -81
  77. package/src/cli/commands/version.js +7 -10
  78. package/src/cli/ui.js +58 -77
  79. package/src/crawler/filters.js +41 -40
  80. package/src/crawler/index.js +52 -36
  81. package/src/crawler/manifest.js +31 -43
  82. package/src/crawler/walk.js +32 -38
  83. package/src/llm/chunker.js +34 -56
  84. package/src/llm/cost-estimator.js +68 -51
  85. package/src/llm/free-model.js +67 -0
  86. package/src/llm/index.js +22 -43
  87. package/src/llm/prompts.js +45 -33
  88. package/src/llm/providers/gemini.js +94 -0
  89. package/src/llm/providers/groq.js +55 -40
  90. package/src/llm/providers/ollama.js +38 -65
  91. package/src/llm/providers/openrouter.js +67 -0
  92. package/src/llm/queue.js +59 -88
  93. package/src/llm/re-prompter.js +41 -0
  94. package/src/llm/validator.js +72 -0
  95. package/src/parser/ast/generic.js +45 -222
  96. package/src/parser/ast/go.js +86 -205
  97. package/src/parser/ast/java.js +76 -146
  98. package/src/parser/ast/javascript.js +173 -241
  99. package/src/parser/ast/laravel/blade.js +56 -0
  100. package/src/parser/ast/laravel/classifier.js +30 -0
  101. package/src/parser/ast/laravel/controller.js +35 -0
  102. package/src/parser/ast/laravel/index.js +54 -0
  103. package/src/parser/ast/laravel/model.js +41 -0
  104. package/src/parser/ast/laravel/provider.js +28 -0
  105. package/src/parser/ast/laravel/routes.js +45 -0
  106. package/src/parser/ast/php.js +129 -0
  107. package/src/parser/ast/python.js +76 -199
  108. package/src/parser/ast/typescript.js +10 -244
  109. package/src/parser/body-extractor.js +40 -0
  110. package/src/parser/call-graph.js +50 -67
  111. package/src/parser/complexity-scorer.js +59 -0
  112. package/src/parser/index.js +61 -86
  113. package/src/parser/pattern-detector.js +71 -0
  114. package/src/parser/pkg-builder.js +36 -83
  115. package/src/renderer/html.js +63 -135
  116. package/src/renderer/index.js +23 -35
  117. package/src/renderer/json.js +17 -35
  118. package/src/renderer/markdown.js +83 -117
  119. package/src/utils/config.js +52 -53
  120. package/src/utils/errors.js +26 -41
  121. package/src/utils/logger.js +32 -53
  122. package/src/cli/flags.js +0 -87
  123. package/src/llm/providers/anthropic.js +0 -57
  124. package/src/llm/providers/google.js +0 -65
  125. package/src/llm/providers/openai.js +0 -52
  126. 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 PROVIDER_MAP = {
18
- anthropic: AnthropicProvider,
19
- openai: OpenAIProvider,
20
- google: GoogleProvider,
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
- * Create an LLM engine instance for the given config.
27
- * @param {object} config - App configuration with provider, model, etc.
28
- * @returns {object} Provider instance with complete() method
9
+ * Provider factory returns the appropriate LLM adapter.
10
+ * @param {Object} config
11
+ * @returns {OpenRouterProvider|OllamaProvider|GroqProvider|GeminiProvider}
29
12
  */
30
- export function createLLMEngine(config) {
31
- const providerName = config.provider || 'anthropic';
32
- const ProviderClass = PROVIDER_MAP[providerName];
33
-
34
- if (!ProviderClass) {
35
- throw new LegacyverError(
36
- `Unknown provider "${providerName}". Supported: ${Object.keys(PROVIDER_MAP).join(', ')}`,
37
- { exitCode: 1, suggestion: 'Run "legacyver providers" to see available providers' }
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 };
@@ -1,52 +1,64 @@
1
- /**
2
- * LLM Prompt definitions — SINGLE SOURCE OF TRUTH.
3
- * Do NOT modify prompts at call sites.
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
- export const SYSTEM_PROMPT = `Kamu adalah penulis dokumentasi teknis profesional. Kamu akan diberikan:
7
- 1. Fakta struktural yang diekstrak dari sebuah file sumber kode (format JSON)
8
- 2. Kode sumber mentah dari file tersebut
9
-
10
- Tugasmu adalah menulis dokumentasi Markdown yang jelas, akurat, dan DETAIL berdasarkan HANYA
11
- apa yang ada di dalam kode. Aturan:
12
- - JANGAN menyimpulkan perilaku yang tidak ditunjukkan secara eksplisit dalam kode
13
- - JANGAN menyebut sistem eksternal kecuali muncul di bagian import
14
- - JANGAN mengarang deskripsi fungsi
15
- - Jika tujuan suatu fungsi tidak jelas dari isinya, katakan dengan jujur
16
- - Tulis semua penjelasan, deskripsi, dan isi dokumentasi dalam Bahasa Indonesia
17
- - Istilah teknis umum (seperti middleware, router, endpoint, request, response, callback) boleh tetap dalam Bahasa Inggris
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-4 kalimat yang menjelaskan apa yang dilakukan file ini, konteksnya dalam aplikasi, dan tanggung jawab utamanya]
22
+ [1-2 sentences about what this file does]
23
23
 
24
24
  ## Functions
25
- [Satu subseksi ### per fungsi/method, berisi:
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
- [Daftar bullet setiap import beserta penjelasan satu baris mengapa modul tersebut digunakan dalam file ini]
28
+ [Bullet list of imports with one-line description of each]
32
29
 
33
30
  ## Usage Example
34
- [Hanya sertakan jika pola penggunaan yang jelas terlihat dalam kode itu sendiri]`;
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 LLM request.
38
- * @param {object} fileFacts - FileFacts JSON
39
- * @param {string} rawSource - Raw file content
40
- * @returns {string} Formatted user message
34
+ * Build the user message for a single file.
35
+ * @param {Object} fileFacts
36
+ * @param {string} rawSource
37
+ * @returns {string}
41
38
  */
42
- export function buildUserMessage(fileFacts, rawSource) {
43
- const factsJson = JSON.stringify(fileFacts, null, 2);
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
- ${factsJson}
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
- import Groq from 'groq-sdk';
8
- import { NoApiKeyError, RateLimitError } from '../../utils/errors.js';
3
+ const { NoApiKeyError } = require('../../utils/errors');
9
4
 
10
- export class GroqProvider {
11
- constructor(config = {}) {
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
- this.client = new Groq({ apiKey });
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(request) {
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
- const response = await this.client.chat.completions.create({
26
- model: this.model,
27
- max_tokens: 4096,
28
- messages: [
29
- { role: 'system', content: request.systemPrompt },
30
- { role: 'user', content: request.userMessage },
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
- return {
35
- content: response.choices[0].message.content,
36
- tokensUsed: {
37
- input: response.usage?.prompt_tokens || 0,
38
- output: response.usage?.completion_tokens || 0,
39
- },
40
- };
41
- } catch (err) {
42
- if (err.status === 429) {
43
- throw new RateLimitError('groq');
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
- estimateCost(inputTokens, outputTokens) {
50
- return (inputTokens * 0.00059 + outputTokens * 0.00079) / 1000;
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
- import { LegacyverError } from '../../utils/errors.js';
3
+ const logger = require('../../utils/logger');
9
4
 
10
- const DEFAULT_BASE_URL = 'http://localhost:11434';
5
+ const OLLAMA_BASE = 'http://localhost:11434';
6
+ const DEFAULT_MODEL = 'llama3.2';
11
7
 
12
- export class OllamaProvider {
13
- constructor(config = {}) {
8
+ class OllamaProvider {
9
+ constructor(config) {
10
+ this.model = config.model || DEFAULT_MODEL;
14
11
  this.name = 'ollama';
15
- this.model = config.model || 'llama3.2';
16
- this.baseUrl = config.baseUrl || process.env.OLLAMA_HOST || DEFAULT_BASE_URL;
12
+ this.isFreeModel = true;
17
13
  }
18
14
 
19
- async complete(request) {
20
- const url = `${this.baseUrl}/api/chat`;
21
- const timeoutMs = 300_000; // 5 minutes — local inference can be slow
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
- const controller = new AbortController();
26
- const timer = setTimeout(() => controller.abort(), timeoutMs);
27
- try {
28
- response = await fetch(url, {
29
- method: 'POST',
30
- headers: { 'Content-Type': 'application/json' },
31
- signal: controller.signal,
32
- body: JSON.stringify({
33
- model: this.model,
34
- messages: [
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 body = await response.text();
65
- throw new LegacyverError(
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
- return {
74
- content: data.message?.content || '',
75
- tokensUsed: {
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
- estimateCost() {
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 };