@nahisaho/katashiro-llm 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/LLMClient.d.ts +64 -0
- package/dist/LLMClient.d.ts.map +1 -0
- package/dist/LLMClient.js +139 -0
- package/dist/LLMClient.js.map +1 -0
- package/dist/PromptManager.d.ts +66 -0
- package/dist/PromptManager.d.ts.map +1 -0
- package/dist/PromptManager.js +121 -0
- package/dist/PromptManager.js.map +1 -0
- package/dist/TokenCounter.d.ts +43 -0
- package/dist/TokenCounter.d.ts.map +1 -0
- package/dist/TokenCounter.js +100 -0
- package/dist/TokenCounter.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/AzureOpenAILLMProvider.d.ts +82 -0
- package/dist/providers/AzureOpenAILLMProvider.d.ts.map +1 -0
- package/dist/providers/AzureOpenAILLMProvider.js +339 -0
- package/dist/providers/AzureOpenAILLMProvider.js.map +1 -0
- package/dist/providers/BaseLLMProvider.d.ts +51 -0
- package/dist/providers/BaseLLMProvider.d.ts.map +1 -0
- package/dist/providers/BaseLLMProvider.js +72 -0
- package/dist/providers/BaseLLMProvider.js.map +1 -0
- package/dist/providers/LLMFactory.d.ts +75 -0
- package/dist/providers/LLMFactory.d.ts.map +1 -0
- package/dist/providers/LLMFactory.js +149 -0
- package/dist/providers/LLMFactory.js.map +1 -0
- package/dist/providers/MockLLMProvider.d.ts +57 -0
- package/dist/providers/MockLLMProvider.d.ts.map +1 -0
- package/dist/providers/MockLLMProvider.js +120 -0
- package/dist/providers/MockLLMProvider.js.map +1 -0
- package/dist/providers/OllamaLLMProvider.d.ts +73 -0
- package/dist/providers/OllamaLLMProvider.d.ts.map +1 -0
- package/dist/providers/OllamaLLMProvider.js +242 -0
- package/dist/providers/OllamaLLMProvider.js.map +1 -0
- package/dist/providers/OpenAILLMProvider.d.ts +87 -0
- package/dist/providers/OpenAILLMProvider.d.ts.map +1 -0
- package/dist/providers/OpenAILLMProvider.js +349 -0
- package/dist/providers/OpenAILLMProvider.js.map +1 -0
- package/dist/providers/index.d.ts +17 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +19 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/types.d.ts +251 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +51 -0
- package/src/LLMClient.ts +171 -0
- package/src/PromptManager.ts +156 -0
- package/src/TokenCounter.ts +114 -0
- package/src/index.ts +35 -0
- package/src/providers/AzureOpenAILLMProvider.ts +494 -0
- package/src/providers/BaseLLMProvider.ts +110 -0
- package/src/providers/LLMFactory.ts +216 -0
- package/src/providers/MockLLMProvider.ts +173 -0
- package/src/providers/OllamaLLMProvider.ts +322 -0
- package/src/providers/OpenAILLMProvider.ts +500 -0
- package/src/providers/index.ts +35 -0
- package/src/types.ts +268 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama LLM Provider
|
|
3
|
+
*
|
|
4
|
+
* Local LLM provider using Ollama
|
|
5
|
+
*
|
|
6
|
+
* @requirement REQ-LLM-001
|
|
7
|
+
* @design DES-KATASHIRO-003-LLM
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { z, ZodType } from 'zod';
|
|
11
|
+
import type {
|
|
12
|
+
ProviderConfig,
|
|
13
|
+
GenerateRequest,
|
|
14
|
+
GenerateResponse,
|
|
15
|
+
StreamChunk,
|
|
16
|
+
Message,
|
|
17
|
+
TokenUsage,
|
|
18
|
+
} from '../types.js';
|
|
19
|
+
import { BaseLLMProvider } from './BaseLLMProvider.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ollama設定
|
|
23
|
+
*/
|
|
24
|
+
export interface OllamaProviderConfig extends ProviderConfig {
|
|
25
|
+
/** ベースURL(デフォルト: http://localhost:11434) */
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
/** モデル名(デフォルト: llama3.2) */
|
|
28
|
+
model?: string;
|
|
29
|
+
/** Keep Alive(モデルをメモリに保持する時間) */
|
|
30
|
+
keepAlive?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ollamaメッセージ形式
|
|
35
|
+
*/
|
|
36
|
+
interface OllamaMessage {
|
|
37
|
+
role: 'system' | 'user' | 'assistant';
|
|
38
|
+
content: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ollama Chat APIレスポンス
|
|
43
|
+
*/
|
|
44
|
+
interface OllamaChatResponse {
|
|
45
|
+
model: string;
|
|
46
|
+
created_at: string;
|
|
47
|
+
message: OllamaMessage;
|
|
48
|
+
done: boolean;
|
|
49
|
+
total_duration?: number;
|
|
50
|
+
load_duration?: number;
|
|
51
|
+
prompt_eval_count?: number;
|
|
52
|
+
prompt_eval_duration?: number;
|
|
53
|
+
eval_count?: number;
|
|
54
|
+
eval_duration?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Ollama LLMプロバイダー
|
|
59
|
+
*
|
|
60
|
+
* ローカルでOllamaを使用したテキスト生成
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const provider = new OllamaLLMProvider({
|
|
65
|
+
* baseUrl: 'http://192.168.224.1:11434',
|
|
66
|
+
* model: 'llama3.2',
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* const response = await provider.generate({
|
|
70
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export class OllamaLLMProvider extends BaseLLMProvider {
|
|
75
|
+
readonly name = 'ollama';
|
|
76
|
+
readonly supportedModels = [
|
|
77
|
+
'llama3.2',
|
|
78
|
+
'llama3.2:1b',
|
|
79
|
+
'llama3.2:3b',
|
|
80
|
+
'llama3.1',
|
|
81
|
+
'llama3.1:8b',
|
|
82
|
+
'llama3.1:70b',
|
|
83
|
+
'qwen2.5',
|
|
84
|
+
'qwen2.5:7b',
|
|
85
|
+
'qwen2.5:14b',
|
|
86
|
+
'qwen2.5:32b',
|
|
87
|
+
'qwen2.5-coder',
|
|
88
|
+
'mistral',
|
|
89
|
+
'mixtral',
|
|
90
|
+
'gemma2',
|
|
91
|
+
'phi3',
|
|
92
|
+
'codellama',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
private readonly baseUrl: string;
|
|
96
|
+
private readonly model: string;
|
|
97
|
+
private readonly keepAlive: string;
|
|
98
|
+
|
|
99
|
+
constructor(config: OllamaProviderConfig = {}) {
|
|
100
|
+
super(config);
|
|
101
|
+
|
|
102
|
+
this.baseUrl = config.baseUrl ?? 'http://localhost:11434';
|
|
103
|
+
this.model = config.model ?? config.defaultModel ?? 'llama3.2';
|
|
104
|
+
this.keepAlive = config.keepAlive ?? '5m';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected getDefaultModel(): string {
|
|
108
|
+
return 'llama3.2';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* メッセージ形式変換
|
|
113
|
+
*/
|
|
114
|
+
private convertMessages(messages: Message[]): OllamaMessage[] {
|
|
115
|
+
return messages.map((msg) => ({
|
|
116
|
+
role: msg.role === 'tool' ? 'assistant' : msg.role,
|
|
117
|
+
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* テキスト生成
|
|
123
|
+
*/
|
|
124
|
+
async generate(request: GenerateRequest): Promise<GenerateResponse> {
|
|
125
|
+
const url = `${this.baseUrl}/api/chat`;
|
|
126
|
+
const model = request.model ?? this.model;
|
|
127
|
+
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timeoutId = setTimeout(
|
|
130
|
+
() => controller.abort(),
|
|
131
|
+
this.config.timeout ?? 30000
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
model,
|
|
142
|
+
messages: this.convertMessages(request.messages),
|
|
143
|
+
stream: false,
|
|
144
|
+
keep_alive: this.keepAlive,
|
|
145
|
+
options: {
|
|
146
|
+
temperature: request.temperature ?? 0.7,
|
|
147
|
+
top_p: request.topP,
|
|
148
|
+
num_predict: request.maxTokens,
|
|
149
|
+
stop: request.stopSequences,
|
|
150
|
+
},
|
|
151
|
+
format: request.responseFormat?.type === 'json_object' ? 'json' : undefined,
|
|
152
|
+
}),
|
|
153
|
+
signal: controller.signal,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const errorText = await response.text();
|
|
158
|
+
throw new Error(`Ollama API error: ${response.status} - ${errorText}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const data = (await response.json()) as OllamaChatResponse;
|
|
162
|
+
|
|
163
|
+
// トークン数推定
|
|
164
|
+
const usage: TokenUsage = {
|
|
165
|
+
promptTokens: data.prompt_eval_count ?? 0,
|
|
166
|
+
completionTokens: data.eval_count ?? 0,
|
|
167
|
+
totalTokens: (data.prompt_eval_count ?? 0) + (data.eval_count ?? 0),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
id: `ollama-${Date.now()}`,
|
|
172
|
+
model: data.model,
|
|
173
|
+
content: data.message.content,
|
|
174
|
+
usage,
|
|
175
|
+
finishReason: 'stop',
|
|
176
|
+
metadata: {
|
|
177
|
+
totalDuration: data.total_duration,
|
|
178
|
+
loadDuration: data.load_duration,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
} finally {
|
|
182
|
+
clearTimeout(timeoutId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* ストリーミング生成
|
|
188
|
+
*/
|
|
189
|
+
async *generateStream(request: GenerateRequest): AsyncGenerator<StreamChunk> {
|
|
190
|
+
const url = `${this.baseUrl}/api/chat`;
|
|
191
|
+
const model = request.model ?? this.model;
|
|
192
|
+
|
|
193
|
+
const response = await fetch(url, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
model,
|
|
200
|
+
messages: this.convertMessages(request.messages),
|
|
201
|
+
stream: true,
|
|
202
|
+
keep_alive: this.keepAlive,
|
|
203
|
+
options: {
|
|
204
|
+
temperature: request.temperature ?? 0.7,
|
|
205
|
+
top_p: request.topP,
|
|
206
|
+
num_predict: request.maxTokens,
|
|
207
|
+
stop: request.stopSequences,
|
|
208
|
+
},
|
|
209
|
+
format: request.responseFormat?.type === 'json_object' ? 'json' : undefined,
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!response.ok || !response.body) {
|
|
214
|
+
const errorText = await response.text();
|
|
215
|
+
throw new Error(`Ollama API error: ${response.status} - ${errorText}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const reader = response.body.getReader();
|
|
219
|
+
const decoder = new TextDecoder();
|
|
220
|
+
let buffer = '';
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
while (true) {
|
|
224
|
+
const { done, value } = await reader.read();
|
|
225
|
+
if (done) break;
|
|
226
|
+
|
|
227
|
+
buffer += decoder.decode(value, { stream: true });
|
|
228
|
+
const lines = buffer.split('\n');
|
|
229
|
+
buffer = lines.pop() ?? '';
|
|
230
|
+
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
if (!line.trim()) continue;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const chunk = JSON.parse(line) as OllamaChatResponse;
|
|
236
|
+
|
|
237
|
+
if (chunk.message?.content) {
|
|
238
|
+
yield {
|
|
239
|
+
type: 'content',
|
|
240
|
+
content: chunk.message.content,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (chunk.done) {
|
|
245
|
+
yield {
|
|
246
|
+
type: 'usage',
|
|
247
|
+
usage: {
|
|
248
|
+
promptTokens: chunk.prompt_eval_count ?? 0,
|
|
249
|
+
completionTokens: chunk.eval_count ?? 0,
|
|
250
|
+
totalTokens:
|
|
251
|
+
(chunk.prompt_eval_count ?? 0) + (chunk.eval_count ?? 0),
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
yield { type: 'done' };
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// JSON parse error - skip
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} finally {
|
|
262
|
+
reader.releaseLock();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 構造化出力生成(Ollama JSON mode)
|
|
268
|
+
*/
|
|
269
|
+
override async generateStructured<T extends ZodType>(
|
|
270
|
+
request: GenerateRequest,
|
|
271
|
+
schema: T
|
|
272
|
+
): Promise<z.infer<T>> {
|
|
273
|
+
const jsonSchema = this.zodToJsonSchema(schema);
|
|
274
|
+
|
|
275
|
+
const enhancedRequest: GenerateRequest = {
|
|
276
|
+
...request,
|
|
277
|
+
responseFormat: { type: 'json_object' },
|
|
278
|
+
messages: [
|
|
279
|
+
...request.messages,
|
|
280
|
+
{
|
|
281
|
+
role: 'user',
|
|
282
|
+
content: `Respond with valid JSON matching this schema:\n${JSON.stringify(jsonSchema, null, 2)}`,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const response = await this.generate(enhancedRequest);
|
|
288
|
+
const parsed = JSON.parse(response.content);
|
|
289
|
+
return schema.parse(parsed);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 利用可能なモデル一覧取得
|
|
294
|
+
*/
|
|
295
|
+
async listModels(): Promise<string[]> {
|
|
296
|
+
const url = `${this.baseUrl}/api/tags`;
|
|
297
|
+
|
|
298
|
+
const response = await fetch(url);
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
throw new Error(`Failed to list models: ${response.status}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
interface OllamaTagsResponse {
|
|
304
|
+
models: Array<{ name: string; model: string }>;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const data = (await response.json()) as OllamaTagsResponse;
|
|
308
|
+
return data.models.map((m) => m.name);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* モデルの存在確認
|
|
313
|
+
*/
|
|
314
|
+
async isModelAvailable(modelName?: string): Promise<boolean> {
|
|
315
|
+
try {
|
|
316
|
+
const models = await this.listModels();
|
|
317
|
+
return models.some((m) => m.includes(modelName ?? this.model));
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|