@game_ryo/lsji 1.0.0 → 1.1.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 -185
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/cli.js +3 -3
- package/src/core/qlearning.js +5 -1
- package/src/execution/budget/circuit-breaker.js +4 -1
- package/src/execution/budget/cost-tracker.js +40 -5
- package/src/execution/engine.js +4 -2
- package/src/execution/idempotency.js +49 -28
- package/src/index.js +1 -0
- package/src/llm/index.js +2 -1
- package/src/llm/providers/base.js +4 -0
- package/src/llm/providers/gemini.js +323 -0
- package/src/llm/tools/registry.js +5 -2
- package/src/server/ui/dist/assets/{main-DDUT9zbA.js → main-Dfki4A3Q.js} +8 -8
- package/src/server/ui/dist/index.html +1 -1
- package/src/server/ui/src/components/NewRunModal.jsx +59 -9
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements LLMProvider interface for Google's Gemini API.
|
|
5
|
+
* Supports function calling, streaming, and token counting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { LLMProvider } from './base.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gemini Provider
|
|
14
|
+
*/
|
|
15
|
+
export class GeminiProvider extends LLMProvider {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
super(config);
|
|
18
|
+
this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
|
|
19
|
+
this.apiVersion = config.apiVersion || 'v1beta';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate completion from Gemini
|
|
24
|
+
*/
|
|
25
|
+
async generate(messages, options = {}) {
|
|
26
|
+
const { temperature = 0.7, maxTokens = 4096, tools, toolChoice = 'auto' } = options;
|
|
27
|
+
|
|
28
|
+
// Convert messages to Gemini format
|
|
29
|
+
const contents = this.convertMessages(messages);
|
|
30
|
+
const systemInstruction = this.extractSystemInstruction(messages);
|
|
31
|
+
|
|
32
|
+
const requestBody = {
|
|
33
|
+
contents,
|
|
34
|
+
generationConfig: {
|
|
35
|
+
temperature,
|
|
36
|
+
maxOutputTokens: maxTokens,
|
|
37
|
+
topP: 0.95,
|
|
38
|
+
topK: 40,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Add system instruction if present
|
|
43
|
+
if (systemInstruction) {
|
|
44
|
+
requestBody.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add tools if provided
|
|
48
|
+
if (tools && tools.length > 0) {
|
|
49
|
+
requestBody.tools = [{ functionDeclarations: this.convertTools(tools) }];
|
|
50
|
+
requestBody.toolConfig = { functionCallingConfig: { mode: this.convertToolChoice(toolChoice) } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const url = `${this.baseUrl}/models/${this.model}:generateContent?key=${this.apiKey}`;
|
|
54
|
+
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify(requestBody),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const error = await response.json().catch(() => ({}));
|
|
63
|
+
throw new Error(`Gemini API error: ${response.status} - ${error.error?.message || response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
return this.parseResponse(data);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate streaming completion
|
|
72
|
+
*/
|
|
73
|
+
async *generateStream(messages, options = {}) {
|
|
74
|
+
const { temperature = 0.7, maxTokens = 4096, tools, toolChoice = 'auto' } = options;
|
|
75
|
+
|
|
76
|
+
const contents = this.convertMessages(messages);
|
|
77
|
+
const systemInstruction = this.extractSystemInstruction(messages);
|
|
78
|
+
|
|
79
|
+
const requestBody = {
|
|
80
|
+
contents,
|
|
81
|
+
generationConfig: {
|
|
82
|
+
temperature,
|
|
83
|
+
maxOutputTokens: maxTokens,
|
|
84
|
+
topP: 0.95,
|
|
85
|
+
topK: 40,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (systemInstruction) {
|
|
90
|
+
requestBody.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (tools && tools.length > 0) {
|
|
94
|
+
requestBody.tools = [{ functionDeclarations: this.convertTools(tools) }];
|
|
95
|
+
requestBody.toolConfig = { functionCallingConfig: { mode: this.convertToolChoice(toolChoice) } };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const url = `${this.baseUrl}/models/${this.model}:streamGenerateContent?key=${this.apiKey}`;
|
|
99
|
+
|
|
100
|
+
const response = await fetch(url, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
103
|
+
body: JSON.stringify(requestBody),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const error = await response.json().catch(() => ({}));
|
|
108
|
+
throw new Error(`Gemini API error: ${response.status} - ${error.error?.message || response.statusText}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const reader = response.body.getReader();
|
|
112
|
+
const decoder = new TextDecoder();
|
|
113
|
+
let buffer = '';
|
|
114
|
+
|
|
115
|
+
while (true) {
|
|
116
|
+
const { done, value } = await reader.read();
|
|
117
|
+
if (done) break;
|
|
118
|
+
|
|
119
|
+
buffer += decoder.decode(value, { stream: true });
|
|
120
|
+
const lines = buffer.split('\n');
|
|
121
|
+
buffer = lines.pop() || '';
|
|
122
|
+
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
if (line.startsWith('data: ')) {
|
|
125
|
+
const jsonStr = line.slice(6);
|
|
126
|
+
if (jsonStr === '[DONE]') return;
|
|
127
|
+
try {
|
|
128
|
+
const chunk = JSON.parse(jsonStr);
|
|
129
|
+
const parsed = this.parseStreamChunk(chunk);
|
|
130
|
+
if (parsed) yield parsed;
|
|
131
|
+
} catch {
|
|
132
|
+
// Ignore parse errors for partial chunks
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Convert messages to Gemini format
|
|
141
|
+
*/
|
|
142
|
+
convertMessages(messages) {
|
|
143
|
+
return messages
|
|
144
|
+
.filter(m => m.role !== 'system')
|
|
145
|
+
.map(m => ({
|
|
146
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
147
|
+
parts: [{ text: m.content || '' }],
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract system instruction from messages
|
|
153
|
+
*/
|
|
154
|
+
extractSystemInstruction(messages) {
|
|
155
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
156
|
+
return systemMsg?.content || null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Convert OpenAI-style tools to Gemini function declarations
|
|
161
|
+
*/
|
|
162
|
+
convertTools(tools) {
|
|
163
|
+
return tools
|
|
164
|
+
.filter(t => t.type === 'function')
|
|
165
|
+
.map(t => ({
|
|
166
|
+
name: t.function.name,
|
|
167
|
+
description: t.function.description,
|
|
168
|
+
parameters: t.function.parameters,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Convert tool choice to Gemini mode
|
|
174
|
+
*/
|
|
175
|
+
convertToolChoice(choice) {
|
|
176
|
+
switch (choice) {
|
|
177
|
+
case 'none': return 'NONE';
|
|
178
|
+
case 'required': return 'ANY';
|
|
179
|
+
case 'auto':
|
|
180
|
+
default: return 'AUTO';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Parse full response
|
|
186
|
+
*/
|
|
187
|
+
parseResponse(data) {
|
|
188
|
+
const candidate = data.candidates?.[0];
|
|
189
|
+
if (!candidate) {
|
|
190
|
+
throw new Error('No candidate in Gemini response');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const content = candidate.content?.parts?.[0]?.text || '';
|
|
194
|
+
const toolCalls = this.extractToolCalls(candidate);
|
|
195
|
+
const usage = data.usageMetadata ? {
|
|
196
|
+
promptTokens: data.usageMetadata.promptTokenCount || 0,
|
|
197
|
+
completionTokens: data.usageMetadata.candidatesTokenCount || 0,
|
|
198
|
+
totalTokens: data.usageMetadata.totalTokenCount || 0,
|
|
199
|
+
} : null;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
content,
|
|
203
|
+
toolCalls,
|
|
204
|
+
usage,
|
|
205
|
+
model: this.model,
|
|
206
|
+
provider: 'gemini',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Parse streaming chunk
|
|
212
|
+
*/
|
|
213
|
+
parseStreamChunk(data) {
|
|
214
|
+
const candidate = data.candidates?.[0];
|
|
215
|
+
if (!candidate) return null;
|
|
216
|
+
|
|
217
|
+
const delta = candidate.content?.parts?.[0]?.text || '';
|
|
218
|
+
const toolCalls = this.extractToolCalls(candidate);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
content: delta,
|
|
222
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
223
|
+
usage: data.usageMetadata ? {
|
|
224
|
+
promptTokens: data.usageMetadata.promptTokenCount || 0,
|
|
225
|
+
completionTokens: data.usageMetadata.candidatesTokenCount || 0,
|
|
226
|
+
totalTokens: data.usageMetadata.totalTokenCount || 0,
|
|
227
|
+
} : undefined,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Extract tool calls from candidate
|
|
233
|
+
*/
|
|
234
|
+
extractToolCalls(candidate) {
|
|
235
|
+
const functionCalls = candidate.content?.parts
|
|
236
|
+
?.filter(p => p.functionCall)
|
|
237
|
+
.map(p => p.functionCall) || [];
|
|
238
|
+
|
|
239
|
+
return functionCalls.map((fc, i) => ({
|
|
240
|
+
id: `call_${Date.now()}_${i}`,
|
|
241
|
+
type: 'function',
|
|
242
|
+
function: {
|
|
243
|
+
name: fc.name,
|
|
244
|
+
arguments: JSON.stringify(fc.args || {}),
|
|
245
|
+
},
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Calculate estimated cost
|
|
251
|
+
*/
|
|
252
|
+
calculateCost(usage) {
|
|
253
|
+
if (!usage) return 0;
|
|
254
|
+
|
|
255
|
+
// Approximate pricing (as of 2024) - update as needed
|
|
256
|
+
const pricing = {
|
|
257
|
+
'gemini-1.5-flash': { input: 0.075, output: 0.30 }, // per 1M tokens
|
|
258
|
+
'gemini-1.5-pro': { input: 3.50, output: 10.50 },
|
|
259
|
+
'gemini-2.0-flash': { input: 0.075, output: 0.30 },
|
|
260
|
+
'gemini-2.0-flash-lite': { input: 0.0375, output: 0.15 },
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const modelPricing = pricing[this.model] || pricing['gemini-1.5-flash'];
|
|
264
|
+
const inputCost = (usage.promptTokens / 1_000_000) * modelPricing.input;
|
|
265
|
+
const outputCost = (usage.completionTokens / 1_000_000) * modelPricing.output;
|
|
266
|
+
|
|
267
|
+
return inputCost + outputCost;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Validate configuration
|
|
272
|
+
*/
|
|
273
|
+
async validate() {
|
|
274
|
+
if (!this.apiKey) {
|
|
275
|
+
return { valid: false, error: 'Gemini API key is required' };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Quick validation call
|
|
279
|
+
try {
|
|
280
|
+
const url = `${this.baseUrl}/models/${this.model}:generateContent?key=${this.apiKey}`;
|
|
281
|
+
const response = await fetch(url, {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: { 'Content-Type': 'application/json' },
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
contents: [{ role: 'user', parts: [{ text: 'test' }] }],
|
|
286
|
+
generationConfig: { maxOutputTokens: 1 },
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
const error = await response.json().catch(() => ({}));
|
|
292
|
+
return { valid: false, error: `Gemini validation failed: ${error.error?.message || response.statusText}` };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { valid: true };
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return { valid: false, error: `Gemini validation failed: ${error.message}` };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if provider supports function calling
|
|
303
|
+
*/
|
|
304
|
+
supportsTools() {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Check if provider supports streaming
|
|
310
|
+
*/
|
|
311
|
+
supportsStreaming() {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* More accurate token estimation for Gemini
|
|
317
|
+
*/
|
|
318
|
+
estimateTokens(messages) {
|
|
319
|
+
const text = messages.map(m => m.content || '').join(' ');
|
|
320
|
+
// Gemini uses similar tokenization to other models
|
|
321
|
+
return Math.ceil(text.length / 4);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -134,8 +134,11 @@ export class ToolRegistry {
|
|
|
134
134
|
|
|
135
135
|
// Execute with idempotency if enabled
|
|
136
136
|
if (tool.idempotent && this.idempotencyStore) {
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
// BUG FIX: Use full request object (including tool name) for idempotency key
|
|
138
|
+
const requestForIdem = { name, params };
|
|
139
|
+
const requestHash = this.idempotencyStore.hashRequest(requestForIdem);
|
|
140
|
+
const key = `${name}_${requestHash}`;
|
|
141
|
+
return this.idempotencyStore.execute(key, name, requestForIdem, () => tool.execute(params, context));
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
return tool.execute(params, context);
|