@game_ryo/lsji 0.1.1 → 0.3.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.
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Anthropic Provider Implementation
3
+ *
4
+ * Supports Anthropic API (Claude 3.5 Sonnet, Haiku, Opus, etc.)
5
+ */
6
+
7
+ import { LLMProvider } from './base.js';
8
+
9
+ // Anthropic pricing (USD per 1M tokens) - as of 2026
10
+ const PRICING = {
11
+ 'claude-3-5-sonnet-20241022': { input: 3.00, output: 15.00 },
12
+ 'claude-3-5-haiku-20241022': { input: 0.25, output: 1.25 },
13
+ 'claude-3-opus-20240229': { input: 15.00, output: 75.00 },
14
+ 'claude-3-sonnet-20240229': { input: 3.00, output: 15.00 },
15
+ 'claude-3-haiku-20240307': { input: 0.25, output: 1.25 },
16
+ };
17
+
18
+ export class AnthropicProvider extends LLMProvider {
19
+ constructor(config = {}) {
20
+ super({
21
+ model: config.model || 'claude-3-5-haiku-20241022',
22
+ apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,
23
+ baseUrl: config.baseUrl || 'https://api.anthropic.com',
24
+ defaultParams: config.defaultParams || {},
25
+ });
26
+
27
+ this.pricing = PRICING[this.model] || PRICING['claude-3-5-haiku-20241022'];
28
+ this.client = null;
29
+ }
30
+
31
+ /**
32
+ * Lazy-load Anthropic client
33
+ */
34
+ async getClient() {
35
+ if (!this.client) {
36
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
37
+ this.client = new Anthropic({
38
+ apiKey: this.apiKey,
39
+ baseURL: this.baseUrl,
40
+ });
41
+ }
42
+ return this.client;
43
+ }
44
+
45
+ /**
46
+ * Generate completion from Anthropic
47
+ */
48
+ async generate(messages, options = {}) {
49
+ const client = await this.getClient();
50
+
51
+ // Separate system message from conversation
52
+ const systemMessage = messages.find(m => m.role === 'system');
53
+ const conversationMessages = messages.filter(m => m.role !== 'system');
54
+
55
+ const params = {
56
+ model: this.model,
57
+ messages: this.formatMessages(conversationMessages),
58
+ system: systemMessage?.content,
59
+ temperature: options.temperature ?? this.defaultParams.temperature ?? 0.7,
60
+ max_tokens: options.maxTokens ?? this.defaultParams.maxTokens ?? 4096,
61
+ ...this.defaultParams,
62
+ };
63
+
64
+ if (options.tools && options.tools.length > 0) {
65
+ params.tools = this.formatTools(options.tools);
66
+ params.tool_choice = options.toolChoice || 'auto';
67
+ }
68
+
69
+ const response = await client.messages.create(params);
70
+
71
+ return this.parseResponse(response);
72
+ }
73
+
74
+ /**
75
+ * Generate streaming completion
76
+ */
77
+ async *generateStream(messages, options = {}) {
78
+ const client = await this.getClient();
79
+
80
+ const systemMessage = messages.find(m => m.role === 'system');
81
+ const conversationMessages = messages.filter(m => m.role !== 'system');
82
+
83
+ const params = {
84
+ model: this.model,
85
+ messages: this.formatMessages(conversationMessages),
86
+ system: systemMessage?.content,
87
+ temperature: options.temperature ?? this.defaultParams.temperature ?? 0.7,
88
+ max_tokens: options.maxTokens ?? this.defaultParams.maxTokens ?? 4096,
89
+ stream: true,
90
+ ...this.defaultParams,
91
+ };
92
+
93
+ if (options.tools && options.tools.length > 0) {
94
+ params.tools = this.formatTools(options.tools);
95
+ params.tool_choice = options.toolChoice || 'auto';
96
+ }
97
+
98
+ const stream = await client.messages.stream(params);
99
+
100
+ for await (const chunk of stream) {
101
+ yield this.parseStreamChunk(chunk);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Format messages for Anthropic API
107
+ */
108
+ formatMessages(messages) {
109
+ return messages.map(msg => ({
110
+ role: msg.role === 'assistant' ? 'assistant' : 'user',
111
+ content: msg.content,
112
+ }));
113
+ }
114
+
115
+ /**
116
+ * Format tools for Anthropic API
117
+ */
118
+ formatTools(tools) {
119
+ return tools.map(tool => ({
120
+ name: tool.name,
121
+ description: tool.description,
122
+ input_schema: tool.parameters,
123
+ }));
124
+ }
125
+
126
+ /**
127
+ * Parse Anthropic response
128
+ */
129
+ parseResponse(response) {
130
+ const content = response.content.find(c => c.type === 'text');
131
+ const toolCalls = response.content
132
+ .filter(c => c.type === 'tool_use')
133
+ .map(c => ({
134
+ id: c.id,
135
+ type: 'function',
136
+ function: {
137
+ name: c.name,
138
+ arguments: JSON.stringify(c.input),
139
+ },
140
+ }));
141
+
142
+ return {
143
+ content: content?.text || '',
144
+ toolCalls,
145
+ usage: {
146
+ inputTokens: response.usage?.input_tokens || 0,
147
+ outputTokens: response.usage?.output_tokens || 0,
148
+ totalTokens: (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0),
149
+ },
150
+ finishReason: response.stop_reason,
151
+ model: response.model,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Parse streaming chunk
157
+ */
158
+ parseStreamChunk(chunk) {
159
+ if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
160
+ return {
161
+ content: chunk.delta.text,
162
+ toolCalls: [],
163
+ usage: null,
164
+ finishReason: null,
165
+ };
166
+ }
167
+
168
+ if (chunk.type === 'content_block_start' && chunk.content_block.type === 'tool_use') {
169
+ return {
170
+ content: '',
171
+ toolCalls: [{
172
+ id: chunk.content_block.id,
173
+ type: 'function',
174
+ function: {
175
+ name: chunk.content_block.name,
176
+ arguments: JSON.stringify(chunk.content_block.input),
177
+ },
178
+ }],
179
+ usage: null,
180
+ finishReason: null,
181
+ };
182
+ }
183
+
184
+ if (chunk.type === 'message_delta') {
185
+ return {
186
+ content: '',
187
+ toolCalls: [],
188
+ usage: chunk.usage ? {
189
+ inputTokens: chunk.usage.input_tokens || 0,
190
+ outputTokens: chunk.usage.output_tokens || 0,
191
+ totalTokens: (chunk.usage.input_tokens || 0) + (chunk.usage.output_tokens || 0),
192
+ } : null,
193
+ finishReason: chunk.delta?.stop_reason,
194
+ };
195
+ }
196
+
197
+ return { content: '', toolCalls: [], usage: null, finishReason: null };
198
+ }
199
+
200
+ /**
201
+ * Calculate cost for usage
202
+ */
203
+ calculateCost(usage) {
204
+ const inputCost = (usage.inputTokens / 1_000_000) * this.pricing.input;
205
+ const outputCost = (usage.outputTokens / 1_000_000) * this.pricing.output;
206
+ return inputCost + outputCost;
207
+ }
208
+
209
+ /**
210
+ * Get pricing info for current model
211
+ */
212
+ getPricing() {
213
+ return { ...this.pricing };
214
+ }
215
+
216
+ /**
217
+ * Check if provider supports function calling
218
+ */
219
+ supportsTools() {
220
+ return true;
221
+ }
222
+
223
+ /**
224
+ * Check if provider supports streaming
225
+ */
226
+ supportsStreaming() {
227
+ return true;
228
+ }
229
+
230
+ /**
231
+ * Validate configuration
232
+ */
233
+ async validate() {
234
+ if (!this.apiKey) {
235
+ return { valid: false, error: 'Anthropic API key is required' };
236
+ }
237
+
238
+ try {
239
+ const client = await this.getClient();
240
+ await client.messages.create({
241
+ model: this.model,
242
+ max_tokens: 1,
243
+ messages: [{ role: 'user', content: 'test' }],
244
+ });
245
+ return { valid: true };
246
+ } catch (e) {
247
+ return { valid: false, error: e.message };
248
+ }
249
+ }
250
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * LLM Provider Base Interface
3
+ *
4
+ * All LLM providers must implement this interface.
5
+ * Provides a unified API for different LLM backends.
6
+ */
7
+
8
+ export class LLMProvider {
9
+ /**
10
+ * @param {Object} config
11
+ * @param {string} config.model - Model name
12
+ * @param {string} [config.apiKey] - API key (for cloud providers)
13
+ * @param {string} [config.baseUrl] - Base URL (for local/custom endpoints)
14
+ * @param {Object} [config.defaultParams] - Default generation parameters
15
+ */
16
+ constructor(config = {}) {
17
+ this.model = config.model;
18
+ this.apiKey = config.apiKey;
19
+ this.baseUrl = config.baseUrl;
20
+ this.defaultParams = config.defaultParams || {};
21
+ }
22
+
23
+ /**
24
+ * Generate a completion from the LLM
25
+ * @param {Array<Object>} messages - Chat messages [{role, content}]
26
+ * @param {Object} [options] - Generation options
27
+ * @param {number} [options.temperature] - Sampling temperature
28
+ * @param {number} [options.maxTokens] - Maximum tokens to generate
29
+ * @param {Array<Object>} [options.tools] - Tool definitions
30
+ * @param {string} [options.toolChoice] - Tool choice strategy
31
+ * @returns {Promise<Object>} Response with content, toolCalls, usage
32
+ */
33
+ async generate(messages, options = {}) {
34
+ throw new Error('generate() must be implemented by subclass');
35
+ }
36
+
37
+ /**
38
+ * Generate a completion with streaming
39
+ * @param {Array<Object>} messages - Chat messages
40
+ * @param {Object} [options] - Generation options
41
+ * @returns {AsyncGenerator<Object>} Stream of response chunks
42
+ */
43
+ async *generateStream(messages, options = {}) {
44
+ throw new Error('generateStream() must be implemented by subclass');
45
+ }
46
+
47
+ /**
48
+ * Get the model name
49
+ * @returns {string}
50
+ */
51
+ getModel() {
52
+ return this.model;
53
+ }
54
+
55
+ /**
56
+ * Estimate token count for messages (approximate)
57
+ * @param {Array<Object>} messages
58
+ * @returns {number} Estimated token count
59
+ */
60
+ estimateTokens(messages) {
61
+ // Rough approximation: ~4 chars per token for English
62
+ const text = messages.map(m => m.content || '').join(' ');
63
+ return Math.ceil(text.length / 4);
64
+ }
65
+
66
+ /**
67
+ * Check if provider supports function calling
68
+ * @returns {boolean}
69
+ */
70
+ supportsTools() {
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * Check if provider supports streaming
76
+ * @returns {boolean}
77
+ */
78
+ supportsStreaming() {
79
+ return false;
80
+ }
81
+
82
+ /**
83
+ * Validate configuration
84
+ * @returns {Promise<{valid: boolean, error?: string}>}
85
+ */
86
+ async validate() {
87
+ return { valid: true };
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Create provider instance from config
93
+ * @param {Object} config - Provider configuration
94
+ * @returns {Promise<LLMProvider>}
95
+ */
96
+ export async function createProvider(config) {
97
+ const { provider, ...options } = config;
98
+
99
+ switch (provider) {
100
+ case 'openai': {
101
+ const { OpenAIProvider } = await import('./openai.js');
102
+ return new OpenAIProvider(options);
103
+ }
104
+ case 'anthropic': {
105
+ const { AnthropicProvider } = await import('./anthropic.js');
106
+ return new AnthropicProvider(options);
107
+ }
108
+ case 'local':
109
+ case 'ollama': {
110
+ const { LocalProvider } = await import('./local.js');
111
+ return new LocalProvider(options);
112
+ }
113
+ default:
114
+ throw new Error(`Unknown provider: ${provider}`);
115
+ }
116
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Local LLM Provider (Ollama, LM Studio, etc.)
3
+ *
4
+ * Supports OpenAI-compatible local endpoints
5
+ */
6
+
7
+ import { LLMProvider } from './base.js';
8
+
9
+ export class LocalProvider extends LLMProvider {
10
+ constructor(config = {}) {
11
+ super({
12
+ model: config.model || 'llama3.1',
13
+ apiKey: config.apiKey || 'ollama', // dummy key for Ollama
14
+ baseUrl: config.baseUrl || 'http://localhost:11434/v1',
15
+ defaultParams: config.defaultParams || {},
16
+ });
17
+
18
+ this.client = null;
19
+ // Local models are free (no API cost)
20
+ this.pricing = { input: 0, output: 0 };
21
+ }
22
+
23
+ /**
24
+ * Lazy-load OpenAI-compatible client
25
+ */
26
+ async getClient() {
27
+ if (!this.client) {
28
+ const { default: OpenAI } = await import('openai');
29
+ this.client = new OpenAI({
30
+ apiKey: this.apiKey,
31
+ baseURL: this.baseUrl,
32
+ });
33
+ }
34
+ return this.client;
35
+ }
36
+
37
+ /**
38
+ * Generate completion from local LLM
39
+ */
40
+ async generate(messages, options = {}) {
41
+ const client = await this.getClient();
42
+
43
+ const params = {
44
+ model: this.model,
45
+ messages: messages.map(m => ({ role: m.role, content: m.content })),
46
+ temperature: options.temperature ?? this.defaultParams.temperature ?? 0.7,
47
+ max_tokens: options.maxTokens ?? this.defaultParams.maxTokens ?? 4096,
48
+ ...this.defaultParams,
49
+ };
50
+
51
+ if (options.tools && options.tools.length > 0) {
52
+ params.tools = options.tools.map(tool => ({
53
+ type: 'function',
54
+ function: {
55
+ name: tool.name,
56
+ description: tool.description,
57
+ parameters: tool.parameters,
58
+ },
59
+ }));
60
+ params.tool_choice = options.toolChoice || 'auto';
61
+ }
62
+
63
+ const response = await client.chat.completions.create(params);
64
+
65
+ return {
66
+ content: response.choices[0]?.message?.content || '',
67
+ toolCalls: response.choices[0]?.message?.tool_calls || [],
68
+ usage: {
69
+ inputTokens: response.usage?.prompt_tokens || 0,
70
+ outputTokens: response.usage?.completion_tokens || 0,
71
+ totalTokens: response.usage?.total_tokens || 0,
72
+ },
73
+ finishReason: response.choices[0]?.finish_reason,
74
+ model: response.model,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Generate streaming completion
80
+ */
81
+ async *generateStream(messages, options = {}) {
82
+ const client = await this.getClient();
83
+
84
+ const params = {
85
+ model: this.model,
86
+ messages: messages.map(m => ({ role: m.role, content: m.content })),
87
+ temperature: options.temperature ?? this.defaultParams.temperature ?? 0.7,
88
+ max_tokens: options.maxTokens ?? this.defaultParams.maxTokens ?? 4096,
89
+ stream: true,
90
+ ...this.defaultParams,
91
+ };
92
+
93
+ if (options.tools && options.tools.length > 0) {
94
+ params.tools = options.tools.map(tool => ({
95
+ type: 'function',
96
+ function: {
97
+ name: tool.name,
98
+ description: tool.description,
99
+ parameters: tool.parameters,
100
+ },
101
+ }));
102
+ params.tool_choice = options.toolChoice || 'auto';
103
+ }
104
+
105
+ const stream = await client.chat.completions.create(params);
106
+
107
+ for await (const chunk of stream) {
108
+ const delta = chunk.choices[0]?.delta;
109
+ yield {
110
+ content: delta?.content || '',
111
+ toolCalls: delta?.tool_calls || [],
112
+ usage: chunk.usage ? {
113
+ inputTokens: chunk.usage.prompt_tokens || 0,
114
+ outputTokens: chunk.usage.completion_tokens || 0,
115
+ totalTokens: chunk.usage.total_tokens || 0,
116
+ } : null,
117
+ finishReason: chunk.choices[0]?.finish_reason,
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Calculate cost (free for local)
124
+ */
125
+ calculateCost(usage) {
126
+ return 0;
127
+ }
128
+
129
+ /**
130
+ * Get pricing info
131
+ */
132
+ getPricing() {
133
+ return { input: 0, output: 0 };
134
+ }
135
+
136
+ /**
137
+ * Check if provider supports function calling
138
+ */
139
+ supportsTools() {
140
+ // Depends on the model, assume yes for modern models
141
+ return true;
142
+ }
143
+
144
+ /**
145
+ * Check if provider supports streaming
146
+ */
147
+ supportsStreaming() {
148
+ return true;
149
+ }
150
+
151
+ /**
152
+ * Validate configuration
153
+ */
154
+ async validate() {
155
+ try {
156
+ const client = await this.getClient();
157
+ await client.models.list();
158
+ return { valid: true };
159
+ } catch (e) {
160
+ return { valid: false, error: `Cannot connect to local LLM at ${this.baseUrl}: ${e.message}` };
161
+ }
162
+ }
163
+ }