@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.
- package/.dockerignore +51 -0
- package/.env.example +33 -0
- package/.github/workflows/docs-pages.yml +57 -0
- package/.github/workflows/release.yml +49 -0
- package/.translaterc.json +44 -0
- package/CLAUDE.md +243 -0
- package/Dockerfile +55 -0
- package/README.md +371 -0
- package/RFC.md +1595 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4494 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1152 -0
- package/dist/index.js +3841 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.yml +56 -0
- package/docs/.vitepress/config.ts +161 -0
- package/docs/api/agent.md +262 -0
- package/docs/api/engine.md +274 -0
- package/docs/api/index.md +171 -0
- package/docs/api/providers.md +304 -0
- package/docs/changelog.md +64 -0
- package/docs/cli/dir.md +243 -0
- package/docs/cli/file.md +213 -0
- package/docs/cli/glossary.md +273 -0
- package/docs/cli/index.md +129 -0
- package/docs/cli/init.md +158 -0
- package/docs/cli/serve.md +211 -0
- package/docs/glossary.json +235 -0
- package/docs/guide/chunking.md +272 -0
- package/docs/guide/configuration.md +139 -0
- package/docs/guide/cost-optimization.md +237 -0
- package/docs/guide/docker.md +371 -0
- package/docs/guide/getting-started.md +150 -0
- package/docs/guide/glossary.md +241 -0
- package/docs/guide/index.md +86 -0
- package/docs/guide/ollama.md +515 -0
- package/docs/guide/prompt-caching.md +221 -0
- package/docs/guide/providers.md +232 -0
- package/docs/guide/quality-control.md +206 -0
- package/docs/guide/vitepress-integration.md +265 -0
- package/docs/index.md +63 -0
- package/docs/ja/api/agent.md +262 -0
- package/docs/ja/api/engine.md +274 -0
- package/docs/ja/api/index.md +171 -0
- package/docs/ja/api/providers.md +304 -0
- package/docs/ja/changelog.md +64 -0
- package/docs/ja/cli/dir.md +243 -0
- package/docs/ja/cli/file.md +213 -0
- package/docs/ja/cli/glossary.md +273 -0
- package/docs/ja/cli/index.md +111 -0
- package/docs/ja/cli/init.md +158 -0
- package/docs/ja/guide/chunking.md +271 -0
- package/docs/ja/guide/configuration.md +139 -0
- package/docs/ja/guide/cost-optimization.md +30 -0
- package/docs/ja/guide/getting-started.md +150 -0
- package/docs/ja/guide/glossary.md +214 -0
- package/docs/ja/guide/index.md +32 -0
- package/docs/ja/guide/ollama.md +410 -0
- package/docs/ja/guide/prompt-caching.md +221 -0
- package/docs/ja/guide/providers.md +232 -0
- package/docs/ja/guide/quality-control.md +137 -0
- package/docs/ja/guide/vitepress-integration.md +265 -0
- package/docs/ja/index.md +58 -0
- package/docs/ko/api/agent.md +262 -0
- package/docs/ko/api/engine.md +274 -0
- package/docs/ko/api/index.md +171 -0
- package/docs/ko/api/providers.md +304 -0
- package/docs/ko/changelog.md +64 -0
- package/docs/ko/cli/dir.md +243 -0
- package/docs/ko/cli/file.md +213 -0
- package/docs/ko/cli/glossary.md +273 -0
- package/docs/ko/cli/index.md +111 -0
- package/docs/ko/cli/init.md +158 -0
- package/docs/ko/guide/chunking.md +271 -0
- package/docs/ko/guide/configuration.md +139 -0
- package/docs/ko/guide/cost-optimization.md +30 -0
- package/docs/ko/guide/getting-started.md +150 -0
- package/docs/ko/guide/glossary.md +214 -0
- package/docs/ko/guide/index.md +32 -0
- package/docs/ko/guide/ollama.md +410 -0
- package/docs/ko/guide/prompt-caching.md +221 -0
- package/docs/ko/guide/providers.md +232 -0
- package/docs/ko/guide/quality-control.md +137 -0
- package/docs/ko/guide/vitepress-integration.md +265 -0
- package/docs/ko/index.md +58 -0
- package/docs/zh/api/agent.md +262 -0
- package/docs/zh/api/engine.md +274 -0
- package/docs/zh/api/index.md +171 -0
- package/docs/zh/api/providers.md +304 -0
- package/docs/zh/changelog.md +64 -0
- package/docs/zh/cli/dir.md +243 -0
- package/docs/zh/cli/file.md +213 -0
- package/docs/zh/cli/glossary.md +273 -0
- package/docs/zh/cli/index.md +111 -0
- package/docs/zh/cli/init.md +158 -0
- package/docs/zh/guide/chunking.md +271 -0
- package/docs/zh/guide/configuration.md +139 -0
- package/docs/zh/guide/cost-optimization.md +30 -0
- package/docs/zh/guide/getting-started.md +150 -0
- package/docs/zh/guide/glossary.md +214 -0
- package/docs/zh/guide/index.md +32 -0
- package/docs/zh/guide/ollama.md +410 -0
- package/docs/zh/guide/prompt-caching.md +221 -0
- package/docs/zh/guide/providers.md +232 -0
- package/docs/zh/guide/quality-control.md +137 -0
- package/docs/zh/guide/vitepress-integration.md +265 -0
- package/docs/zh/index.md +58 -0
- package/package.json +91 -0
- package/release.config.mjs +15 -0
- package/schemas/glossary.schema.json +110 -0
- package/src/cli/commands/dir.ts +469 -0
- package/src/cli/commands/file.ts +291 -0
- package/src/cli/commands/glossary.ts +221 -0
- package/src/cli/commands/init.ts +68 -0
- package/src/cli/commands/serve.ts +60 -0
- package/src/cli/index.ts +64 -0
- package/src/cli/options.ts +59 -0
- package/src/core/agent.ts +1119 -0
- package/src/core/chunker.ts +391 -0
- package/src/core/engine.ts +634 -0
- package/src/errors.ts +188 -0
- package/src/index.ts +147 -0
- package/src/integrations/vitepress.ts +549 -0
- package/src/parsers/markdown.ts +383 -0
- package/src/providers/claude.ts +259 -0
- package/src/providers/interface.ts +109 -0
- package/src/providers/ollama.ts +379 -0
- package/src/providers/openai.ts +308 -0
- package/src/providers/registry.ts +153 -0
- package/src/server/index.ts +152 -0
- package/src/server/middleware/auth.ts +93 -0
- package/src/server/middleware/logger.ts +90 -0
- package/src/server/routes/health.ts +84 -0
- package/src/server/routes/translate.ts +210 -0
- package/src/server/types.ts +138 -0
- package/src/services/cache.ts +899 -0
- package/src/services/config.ts +217 -0
- package/src/services/glossary.ts +247 -0
- package/src/types/analysis.ts +164 -0
- package/src/types/index.ts +265 -0
- package/src/types/modes.ts +121 -0
- package/src/types/mqm.ts +157 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/tokens.ts +116 -0
- package/tests/fixtures/glossaries/ml-glossary.json +53 -0
- package/tests/fixtures/input/lynq-installation.ko.md +350 -0
- package/tests/fixtures/input/lynq-installation.md +350 -0
- package/tests/fixtures/input/simple.ko.md +27 -0
- package/tests/fixtures/input/simple.md +27 -0
- package/tests/unit/chunker.test.ts +229 -0
- package/tests/unit/glossary.test.ts +146 -0
- package/tests/unit/markdown.test.ts +205 -0
- package/tests/unit/tokens.test.ts +81 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +34 -0
- 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
|
+
}
|