@llm-translate/cli 1.0.0-next.1

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 (157) hide show
  1. package/.dockerignore +51 -0
  2. package/.env.example +33 -0
  3. package/.github/workflows/docs-pages.yml +57 -0
  4. package/.github/workflows/release.yml +49 -0
  5. package/.translaterc.json +44 -0
  6. package/CLAUDE.md +243 -0
  7. package/Dockerfile +55 -0
  8. package/README.md +371 -0
  9. package/RFC.md +1595 -0
  10. package/dist/cli/index.d.ts +2 -0
  11. package/dist/cli/index.js +4494 -0
  12. package/dist/cli/index.js.map +1 -0
  13. package/dist/index.d.ts +1152 -0
  14. package/dist/index.js +3841 -0
  15. package/dist/index.js.map +1 -0
  16. package/docker-compose.yml +56 -0
  17. package/docs/.vitepress/config.ts +161 -0
  18. package/docs/api/agent.md +262 -0
  19. package/docs/api/engine.md +274 -0
  20. package/docs/api/index.md +171 -0
  21. package/docs/api/providers.md +304 -0
  22. package/docs/changelog.md +64 -0
  23. package/docs/cli/dir.md +243 -0
  24. package/docs/cli/file.md +213 -0
  25. package/docs/cli/glossary.md +273 -0
  26. package/docs/cli/index.md +129 -0
  27. package/docs/cli/init.md +158 -0
  28. package/docs/cli/serve.md +211 -0
  29. package/docs/glossary.json +235 -0
  30. package/docs/guide/chunking.md +272 -0
  31. package/docs/guide/configuration.md +139 -0
  32. package/docs/guide/cost-optimization.md +237 -0
  33. package/docs/guide/docker.md +371 -0
  34. package/docs/guide/getting-started.md +150 -0
  35. package/docs/guide/glossary.md +241 -0
  36. package/docs/guide/index.md +86 -0
  37. package/docs/guide/ollama.md +515 -0
  38. package/docs/guide/prompt-caching.md +221 -0
  39. package/docs/guide/providers.md +232 -0
  40. package/docs/guide/quality-control.md +206 -0
  41. package/docs/guide/vitepress-integration.md +265 -0
  42. package/docs/index.md +63 -0
  43. package/docs/ja/api/agent.md +262 -0
  44. package/docs/ja/api/engine.md +274 -0
  45. package/docs/ja/api/index.md +171 -0
  46. package/docs/ja/api/providers.md +304 -0
  47. package/docs/ja/changelog.md +64 -0
  48. package/docs/ja/cli/dir.md +243 -0
  49. package/docs/ja/cli/file.md +213 -0
  50. package/docs/ja/cli/glossary.md +273 -0
  51. package/docs/ja/cli/index.md +111 -0
  52. package/docs/ja/cli/init.md +158 -0
  53. package/docs/ja/guide/chunking.md +271 -0
  54. package/docs/ja/guide/configuration.md +139 -0
  55. package/docs/ja/guide/cost-optimization.md +30 -0
  56. package/docs/ja/guide/getting-started.md +150 -0
  57. package/docs/ja/guide/glossary.md +214 -0
  58. package/docs/ja/guide/index.md +32 -0
  59. package/docs/ja/guide/ollama.md +410 -0
  60. package/docs/ja/guide/prompt-caching.md +221 -0
  61. package/docs/ja/guide/providers.md +232 -0
  62. package/docs/ja/guide/quality-control.md +137 -0
  63. package/docs/ja/guide/vitepress-integration.md +265 -0
  64. package/docs/ja/index.md +58 -0
  65. package/docs/ko/api/agent.md +262 -0
  66. package/docs/ko/api/engine.md +274 -0
  67. package/docs/ko/api/index.md +171 -0
  68. package/docs/ko/api/providers.md +304 -0
  69. package/docs/ko/changelog.md +64 -0
  70. package/docs/ko/cli/dir.md +243 -0
  71. package/docs/ko/cli/file.md +213 -0
  72. package/docs/ko/cli/glossary.md +273 -0
  73. package/docs/ko/cli/index.md +111 -0
  74. package/docs/ko/cli/init.md +158 -0
  75. package/docs/ko/guide/chunking.md +271 -0
  76. package/docs/ko/guide/configuration.md +139 -0
  77. package/docs/ko/guide/cost-optimization.md +30 -0
  78. package/docs/ko/guide/getting-started.md +150 -0
  79. package/docs/ko/guide/glossary.md +214 -0
  80. package/docs/ko/guide/index.md +32 -0
  81. package/docs/ko/guide/ollama.md +410 -0
  82. package/docs/ko/guide/prompt-caching.md +221 -0
  83. package/docs/ko/guide/providers.md +232 -0
  84. package/docs/ko/guide/quality-control.md +137 -0
  85. package/docs/ko/guide/vitepress-integration.md +265 -0
  86. package/docs/ko/index.md +58 -0
  87. package/docs/zh/api/agent.md +262 -0
  88. package/docs/zh/api/engine.md +274 -0
  89. package/docs/zh/api/index.md +171 -0
  90. package/docs/zh/api/providers.md +304 -0
  91. package/docs/zh/changelog.md +64 -0
  92. package/docs/zh/cli/dir.md +243 -0
  93. package/docs/zh/cli/file.md +213 -0
  94. package/docs/zh/cli/glossary.md +273 -0
  95. package/docs/zh/cli/index.md +111 -0
  96. package/docs/zh/cli/init.md +158 -0
  97. package/docs/zh/guide/chunking.md +271 -0
  98. package/docs/zh/guide/configuration.md +139 -0
  99. package/docs/zh/guide/cost-optimization.md +30 -0
  100. package/docs/zh/guide/getting-started.md +150 -0
  101. package/docs/zh/guide/glossary.md +214 -0
  102. package/docs/zh/guide/index.md +32 -0
  103. package/docs/zh/guide/ollama.md +410 -0
  104. package/docs/zh/guide/prompt-caching.md +221 -0
  105. package/docs/zh/guide/providers.md +232 -0
  106. package/docs/zh/guide/quality-control.md +137 -0
  107. package/docs/zh/guide/vitepress-integration.md +265 -0
  108. package/docs/zh/index.md +58 -0
  109. package/package.json +91 -0
  110. package/release.config.mjs +15 -0
  111. package/schemas/glossary.schema.json +110 -0
  112. package/src/cli/commands/dir.ts +469 -0
  113. package/src/cli/commands/file.ts +291 -0
  114. package/src/cli/commands/glossary.ts +221 -0
  115. package/src/cli/commands/init.ts +68 -0
  116. package/src/cli/commands/serve.ts +60 -0
  117. package/src/cli/index.ts +64 -0
  118. package/src/cli/options.ts +59 -0
  119. package/src/core/agent.ts +1119 -0
  120. package/src/core/chunker.ts +391 -0
  121. package/src/core/engine.ts +634 -0
  122. package/src/errors.ts +188 -0
  123. package/src/index.ts +147 -0
  124. package/src/integrations/vitepress.ts +549 -0
  125. package/src/parsers/markdown.ts +383 -0
  126. package/src/providers/claude.ts +259 -0
  127. package/src/providers/interface.ts +109 -0
  128. package/src/providers/ollama.ts +379 -0
  129. package/src/providers/openai.ts +308 -0
  130. package/src/providers/registry.ts +153 -0
  131. package/src/server/index.ts +152 -0
  132. package/src/server/middleware/auth.ts +93 -0
  133. package/src/server/middleware/logger.ts +90 -0
  134. package/src/server/routes/health.ts +84 -0
  135. package/src/server/routes/translate.ts +210 -0
  136. package/src/server/types.ts +138 -0
  137. package/src/services/cache.ts +899 -0
  138. package/src/services/config.ts +217 -0
  139. package/src/services/glossary.ts +247 -0
  140. package/src/types/analysis.ts +164 -0
  141. package/src/types/index.ts +265 -0
  142. package/src/types/modes.ts +121 -0
  143. package/src/types/mqm.ts +157 -0
  144. package/src/utils/logger.ts +141 -0
  145. package/src/utils/tokens.ts +116 -0
  146. package/tests/fixtures/glossaries/ml-glossary.json +53 -0
  147. package/tests/fixtures/input/lynq-installation.ko.md +350 -0
  148. package/tests/fixtures/input/lynq-installation.md +350 -0
  149. package/tests/fixtures/input/simple.ko.md +27 -0
  150. package/tests/fixtures/input/simple.md +27 -0
  151. package/tests/unit/chunker.test.ts +229 -0
  152. package/tests/unit/glossary.test.ts +146 -0
  153. package/tests/unit/markdown.test.ts +205 -0
  154. package/tests/unit/tokens.test.ts +81 -0
  155. package/tsconfig.json +28 -0
  156. package/tsup.config.ts +34 -0
  157. package/vitest.config.ts +16 -0
@@ -0,0 +1,109 @@
1
+ import type { ProviderName } from '../types/index.js';
2
+
3
+ // ============================================================================
4
+ // Chat Message Types
5
+ // ============================================================================
6
+
7
+ export type ChatRole = 'system' | 'user' | 'assistant';
8
+
9
+ /**
10
+ * Content part with optional cache control for prompt caching
11
+ */
12
+ export interface CacheableTextPart {
13
+ type: 'text';
14
+ text: string;
15
+ /** Enable prompt caching for this content part (Claude only) */
16
+ cacheControl?: { type: 'ephemeral' };
17
+ }
18
+
19
+ export interface ChatMessage {
20
+ role: ChatRole;
21
+ /** Content can be a string or array of cacheable parts */
22
+ content: string | CacheableTextPart[];
23
+ }
24
+
25
+ // ============================================================================
26
+ // Request/Response Types
27
+ // ============================================================================
28
+
29
+ export interface ChatRequest {
30
+ messages: ChatMessage[];
31
+ model?: string;
32
+ temperature?: number;
33
+ maxTokens?: number;
34
+ }
35
+
36
+ export interface ChatResponse {
37
+ content: string;
38
+ usage: {
39
+ inputTokens: number;
40
+ outputTokens: number;
41
+ /** Tokens read from cache (90% cost reduction) */
42
+ cacheReadTokens?: number;
43
+ /** Tokens written to cache (25% cost increase for first write) */
44
+ cacheWriteTokens?: number;
45
+ };
46
+ model: string;
47
+ finishReason: 'stop' | 'length' | 'error';
48
+ }
49
+
50
+ // ============================================================================
51
+ // Model Information
52
+ // ============================================================================
53
+
54
+ export interface ModelInfo {
55
+ maxContextTokens: number;
56
+ supportsStreaming: boolean;
57
+ costPer1kInput?: number;
58
+ costPer1kOutput?: number;
59
+ }
60
+
61
+ // ============================================================================
62
+ // Provider Configuration
63
+ // ============================================================================
64
+
65
+ export interface ProviderConfig {
66
+ apiKey?: string;
67
+ baseUrl?: string;
68
+ defaultModel?: string;
69
+ timeout?: number;
70
+ }
71
+
72
+ // ============================================================================
73
+ // LLM Provider Interface
74
+ // ============================================================================
75
+
76
+ export interface LLMProvider {
77
+ readonly name: ProviderName;
78
+
79
+ /**
80
+ * The default model used by this provider
81
+ */
82
+ readonly defaultModel: string;
83
+
84
+ /**
85
+ * Send a chat request and receive a complete response
86
+ */
87
+ chat(request: ChatRequest): Promise<ChatResponse>;
88
+
89
+ /**
90
+ * Send a chat request and receive a streaming response
91
+ */
92
+ stream(request: ChatRequest): AsyncIterable<string>;
93
+
94
+ /**
95
+ * Count the number of tokens in a text string
96
+ */
97
+ countTokens(text: string): number;
98
+
99
+ /**
100
+ * Get information about a specific model
101
+ */
102
+ getModelInfo(model?: string): ModelInfo;
103
+ }
104
+
105
+ // ============================================================================
106
+ // Provider Factory Type
107
+ // ============================================================================
108
+
109
+ export type ProviderFactory = (config: ProviderConfig) => LLMProvider;
@@ -0,0 +1,379 @@
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
+ import { generateText, streamText } from 'ai';
3
+ import type { ProviderName } from '../types/index.js';
4
+ import type {
5
+ LLMProvider,
6
+ ProviderConfig,
7
+ ChatRequest,
8
+ ChatResponse,
9
+ ModelInfo,
10
+ } from './interface.js';
11
+ import { TranslationError, ErrorCode } from '../errors.js';
12
+ import { estimateTokens } from '../utils/tokens.js';
13
+
14
+ // ============================================================================
15
+ // Model Information
16
+ // ============================================================================
17
+
18
+ // Common Ollama models with their context sizes
19
+ // Note: These are estimates - actual limits depend on model variant and system memory
20
+ const MODEL_INFO: Record<string, ModelInfo> = {
21
+ // Llama 3.x models
22
+ 'llama3.3': {
23
+ maxContextTokens: 128000,
24
+ supportsStreaming: true,
25
+ },
26
+ 'llama3.2': {
27
+ maxContextTokens: 128000,
28
+ supportsStreaming: true,
29
+ },
30
+ 'llama3.1': {
31
+ maxContextTokens: 128000,
32
+ supportsStreaming: true,
33
+ },
34
+ 'llama3': {
35
+ maxContextTokens: 8192,
36
+ supportsStreaming: true,
37
+ },
38
+ // Llama 2 models
39
+ llama2: {
40
+ maxContextTokens: 4096,
41
+ supportsStreaming: true,
42
+ },
43
+ 'llama2:13b': {
44
+ maxContextTokens: 4096,
45
+ supportsStreaming: true,
46
+ },
47
+ 'llama2:70b': {
48
+ maxContextTokens: 4096,
49
+ supportsStreaming: true,
50
+ },
51
+ // Mistral models
52
+ mistral: {
53
+ maxContextTokens: 32768,
54
+ supportsStreaming: true,
55
+ },
56
+ 'mistral-nemo': {
57
+ maxContextTokens: 128000,
58
+ supportsStreaming: true,
59
+ },
60
+ mixtral: {
61
+ maxContextTokens: 32768,
62
+ supportsStreaming: true,
63
+ },
64
+ // Qwen models
65
+ qwen2: {
66
+ maxContextTokens: 32768,
67
+ supportsStreaming: true,
68
+ },
69
+ 'qwen2.5': {
70
+ maxContextTokens: 128000,
71
+ supportsStreaming: true,
72
+ },
73
+ 'qwen2.5-coder': {
74
+ maxContextTokens: 128000,
75
+ supportsStreaming: true,
76
+ },
77
+ // Gemma models
78
+ gemma2: {
79
+ maxContextTokens: 8192,
80
+ supportsStreaming: true,
81
+ },
82
+ gemma: {
83
+ maxContextTokens: 8192,
84
+ supportsStreaming: true,
85
+ },
86
+ // Phi models
87
+ phi3: {
88
+ maxContextTokens: 128000,
89
+ supportsStreaming: true,
90
+ },
91
+ 'phi3:mini': {
92
+ maxContextTokens: 128000,
93
+ supportsStreaming: true,
94
+ },
95
+ // Code models
96
+ codellama: {
97
+ maxContextTokens: 16384,
98
+ supportsStreaming: true,
99
+ },
100
+ 'deepseek-coder': {
101
+ maxContextTokens: 16384,
102
+ supportsStreaming: true,
103
+ },
104
+ // Other popular models
105
+ 'neural-chat': {
106
+ maxContextTokens: 8192,
107
+ supportsStreaming: true,
108
+ },
109
+ vicuna: {
110
+ maxContextTokens: 2048,
111
+ supportsStreaming: true,
112
+ },
113
+ };
114
+
115
+ // Default to llama3.2 for better multilingual support
116
+ const DEFAULT_MODEL = 'llama3.2';
117
+ const DEFAULT_BASE_URL = 'http://localhost:11434';
118
+
119
+ // ============================================================================
120
+ // Ollama Provider Implementation
121
+ // ============================================================================
122
+
123
+ export class OllamaProvider implements LLMProvider {
124
+ readonly name: ProviderName = 'ollama';
125
+ readonly defaultModel: string;
126
+ private readonly client: ReturnType<typeof createOpenAI>;
127
+ private readonly baseUrl: string;
128
+
129
+ constructor(config: ProviderConfig = {}) {
130
+ this.baseUrl =
131
+ config.baseUrl ??
132
+ process.env['OLLAMA_BASE_URL'] ??
133
+ DEFAULT_BASE_URL;
134
+
135
+ // Ollama uses OpenAI-compatible API at /v1 endpoint
136
+ this.client = createOpenAI({
137
+ apiKey: 'ollama', // Ollama doesn't require an API key
138
+ baseURL: `${this.baseUrl}/v1`,
139
+ });
140
+
141
+ this.defaultModel = config.defaultModel ?? DEFAULT_MODEL;
142
+ }
143
+
144
+ async chat(request: ChatRequest): Promise<ChatResponse> {
145
+ const model = request.model ?? this.defaultModel;
146
+
147
+ try {
148
+ await this.ensureModelAvailable(model);
149
+
150
+ const messages = this.convertMessages(request.messages);
151
+
152
+ const result = await generateText({
153
+ model: this.client(model),
154
+ messages,
155
+ temperature: request.temperature ?? 0,
156
+ maxTokens: request.maxTokens ?? 4096,
157
+ });
158
+
159
+ return {
160
+ content: result.text,
161
+ usage: {
162
+ inputTokens: result.usage?.promptTokens ?? 0,
163
+ outputTokens: result.usage?.completionTokens ?? 0,
164
+ },
165
+ model,
166
+ finishReason: mapFinishReason(result.finishReason),
167
+ };
168
+ } catch (error) {
169
+ throw this.handleError(error, model);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Convert messages to Vercel AI SDK format
175
+ * Ollama doesn't support cache control, so we simplify content
176
+ */
177
+ private convertMessages(
178
+ messages: Array<{
179
+ role: 'system' | 'user' | 'assistant';
180
+ content: string | Array<{ type: 'text'; text: string }>;
181
+ }>
182
+ ) {
183
+ return messages.map((msg) => {
184
+ // If content is an array of parts, concatenate text
185
+ if (Array.isArray(msg.content)) {
186
+ return {
187
+ role: msg.role,
188
+ content: msg.content.map((part) => part.text).join(''),
189
+ };
190
+ }
191
+ return { role: msg.role, content: msg.content };
192
+ });
193
+ }
194
+
195
+ async *stream(request: ChatRequest): AsyncIterable<string> {
196
+ const model = request.model ?? this.defaultModel;
197
+
198
+ try {
199
+ await this.ensureModelAvailable(model);
200
+
201
+ const messages = this.convertMessages(request.messages);
202
+
203
+ const result = streamText({
204
+ model: this.client(model),
205
+ messages,
206
+ temperature: request.temperature ?? 0,
207
+ maxTokens: request.maxTokens ?? 4096,
208
+ });
209
+
210
+ for await (const chunk of result.textStream) {
211
+ yield chunk;
212
+ }
213
+ } catch (error) {
214
+ throw this.handleError(error, model);
215
+ }
216
+ }
217
+
218
+ countTokens(text: string): number {
219
+ // Use estimation for Ollama models
220
+ return estimateTokens(text);
221
+ }
222
+
223
+ getModelInfo(model?: string): ModelInfo {
224
+ const modelName = model ?? this.defaultModel;
225
+
226
+ // Try exact match first
227
+ if (MODEL_INFO[modelName]) {
228
+ return MODEL_INFO[modelName];
229
+ }
230
+
231
+ // Try base model name (e.g., "llama3.2:7b" -> "llama3.2")
232
+ const baseModel = modelName.split(':')[0] ?? modelName;
233
+ if (baseModel && MODEL_INFO[baseModel]) {
234
+ return MODEL_INFO[baseModel];
235
+ }
236
+
237
+ // Default fallback
238
+ return {
239
+ maxContextTokens: 4096,
240
+ supportsStreaming: true,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Check if the Ollama server is running and the model is available
246
+ */
247
+ private async ensureModelAvailable(model: string): Promise<void> {
248
+ try {
249
+ const response = await fetch(`${this.baseUrl}/api/tags`);
250
+
251
+ if (!response.ok) {
252
+ throw new TranslationError(ErrorCode.PROVIDER_ERROR, {
253
+ provider: 'ollama',
254
+ message: `Ollama server not responding at ${this.baseUrl}`,
255
+ });
256
+ }
257
+
258
+ const data = (await response.json()) as {
259
+ models?: Array<{ name: string }>;
260
+ };
261
+ const models = data.models ?? [];
262
+ const modelNames = models.map((m) => m.name);
263
+
264
+ // Check for exact match or base model match
265
+ const modelExists = modelNames.some(
266
+ (name) => name === model || name.startsWith(`${model}:`)
267
+ );
268
+
269
+ if (!modelExists) {
270
+ throw new TranslationError(ErrorCode.PROVIDER_ERROR, {
271
+ provider: 'ollama',
272
+ model,
273
+ availableModels: modelNames.slice(0, 10), // Show first 10
274
+ message: `Model "${model}" not found. Pull it with: ollama pull ${model}`,
275
+ });
276
+ }
277
+ } catch (error) {
278
+ if (error instanceof TranslationError) {
279
+ throw error;
280
+ }
281
+
282
+ // Connection refused or other network error
283
+ throw new TranslationError(ErrorCode.PROVIDER_ERROR, {
284
+ provider: 'ollama',
285
+ baseUrl: this.baseUrl,
286
+ message: `Cannot connect to Ollama server at ${this.baseUrl}. Is Ollama running?`,
287
+ });
288
+ }
289
+ }
290
+
291
+ private handleError(error: unknown, model?: string): TranslationError {
292
+ if (error instanceof TranslationError) {
293
+ return error;
294
+ }
295
+
296
+ const errorMessage =
297
+ error instanceof Error ? error.message : String(error);
298
+
299
+ // Check for connection errors
300
+ if (
301
+ errorMessage.includes('ECONNREFUSED') ||
302
+ errorMessage.includes('fetch failed') ||
303
+ errorMessage.includes('network')
304
+ ) {
305
+ return new TranslationError(ErrorCode.PROVIDER_ERROR, {
306
+ provider: 'ollama',
307
+ baseUrl: this.baseUrl,
308
+ message: `Cannot connect to Ollama server at ${this.baseUrl}. Is Ollama running?`,
309
+ });
310
+ }
311
+
312
+ // Check for model not found
313
+ if (
314
+ errorMessage.includes('model') &&
315
+ errorMessage.includes('not found')
316
+ ) {
317
+ return new TranslationError(ErrorCode.PROVIDER_ERROR, {
318
+ provider: 'ollama',
319
+ model,
320
+ message: `Model "${model}" not found. Pull it with: ollama pull ${model}`,
321
+ });
322
+ }
323
+
324
+ // Check for context length errors
325
+ if (
326
+ errorMessage.includes('context') ||
327
+ errorMessage.includes('too long')
328
+ ) {
329
+ return new TranslationError(ErrorCode.CHUNK_TOO_LARGE, {
330
+ provider: 'ollama',
331
+ model,
332
+ message: errorMessage,
333
+ });
334
+ }
335
+
336
+ // Check for out of memory
337
+ if (
338
+ errorMessage.includes('out of memory') ||
339
+ errorMessage.includes('OOM')
340
+ ) {
341
+ return new TranslationError(ErrorCode.PROVIDER_ERROR, {
342
+ provider: 'ollama',
343
+ model,
344
+ message: 'Out of memory. Try a smaller model or reduce chunk size.',
345
+ });
346
+ }
347
+
348
+ return new TranslationError(ErrorCode.PROVIDER_ERROR, {
349
+ provider: 'ollama',
350
+ message: errorMessage,
351
+ });
352
+ }
353
+ }
354
+
355
+ // ============================================================================
356
+ // Helper Functions
357
+ // ============================================================================
358
+
359
+ function mapFinishReason(
360
+ reason: string | null | undefined
361
+ ): 'stop' | 'length' | 'error' {
362
+ switch (reason) {
363
+ case 'stop':
364
+ return 'stop';
365
+ case 'length':
366
+ case 'max_tokens':
367
+ return 'length';
368
+ default:
369
+ return 'error';
370
+ }
371
+ }
372
+
373
+ // ============================================================================
374
+ // Factory Function
375
+ // ============================================================================
376
+
377
+ export function createOllamaProvider(config: ProviderConfig = {}): LLMProvider {
378
+ return new OllamaProvider(config);
379
+ }