@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.
- package/package.json +12 -7
- package/src/cli.js +395 -62
- package/src/execution/budget/circuit-breaker.js +245 -0
- package/src/execution/budget/cost-tracker.js +387 -0
- package/src/execution/budget/index.js +63 -0
- package/src/execution/budget/token-counter.js +159 -0
- package/src/execution/engine.js +428 -0
- package/src/execution/hitl/approval-gate.js +210 -0
- package/src/execution/hitl/index.js +12 -0
- package/src/execution/hitl/notifier.js +151 -0
- package/src/execution/hitl/store.js +311 -0
- package/src/execution/idempotency.js +312 -0
- package/src/execution/index.js +14 -0
- package/src/index.js +80 -4
- package/src/llm/index.js +21 -0
- package/src/llm/llm-agent.js +357 -0
- package/src/llm/memory/conversation.js +271 -0
- package/src/llm/memory/episodic.js +312 -0
- package/src/llm/memory/index.js +12 -0
- package/src/llm/memory/semantic.js +324 -0
- package/src/llm/plugins/index.js +202 -0
- package/src/llm/prompt-manager.js +332 -0
- package/src/llm/providers/anthropic.js +250 -0
- package/src/llm/providers/base.js +116 -0
- package/src/llm/providers/local.js +163 -0
- package/src/llm/providers/openai.js +212 -0
- package/src/llm/tools/registry.js +342 -0
- package/src/server/index.js +416 -0
- package/src/server/ui/index.html +16 -0
- package/src/server/ui/package.json +19 -0
- package/src/server/ui/src/main.jsx +10 -0
- package/src/server/ui/src/styles.css +260 -0
- package/src/server/ui/vite.config.js +27 -0
|
@@ -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
|
+
}
|