@contentgrowth/llm-service 0.6.8 → 0.6.9
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentgrowth/llm-service",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
4
4
|
"description": "Unified LLM Service for Content Growth",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"author": "Content Growth",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@google/
|
|
17
|
+
"@google/genai": "^1.31.0",
|
|
18
18
|
"openai": "^6.9.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GoogleGenAI } from '@google/genai';
|
|
2
2
|
import { BaseLLMProvider } from './base-provider.js';
|
|
3
3
|
import { LLMServiceException } from '../../llm-service.js';
|
|
4
4
|
import { extractJsonFromResponse } from '../json-utils.js';
|
|
@@ -6,11 +6,21 @@ import { extractJsonFromResponse } from '../json-utils.js';
|
|
|
6
6
|
export class GeminiProvider extends BaseLLMProvider {
|
|
7
7
|
constructor(config) {
|
|
8
8
|
super(config);
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
// Unified client for all operations (text, image, video)
|
|
11
|
+
// Uses apiKey for Gemini, and automatically handles Vertex AI env vars for Veo
|
|
12
|
+
this.client = new GoogleGenAI({
|
|
13
|
+
apiKey: config.apiKey,
|
|
14
|
+
});
|
|
15
|
+
|
|
10
16
|
this.models = config.models;
|
|
11
17
|
this.defaultModel = config.models.default;
|
|
18
|
+
|
|
19
|
+
// Store pending operations for polling
|
|
20
|
+
this._pendingOperations = new Map();
|
|
12
21
|
}
|
|
13
22
|
|
|
23
|
+
|
|
14
24
|
async chat(userMessage, systemPrompt = '', options = {}) {
|
|
15
25
|
const messages = [{ role: 'user', content: userMessage }];
|
|
16
26
|
const tier = options.tier || 'default';
|
|
@@ -42,25 +52,18 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
// Build generation config
|
|
56
|
+
const generationConfig = {
|
|
57
|
+
temperature: options.temperature ?? temperature,
|
|
58
|
+
maxOutputTokens: options.maxTokens ?? maxTokens,
|
|
49
59
|
};
|
|
50
60
|
|
|
51
|
-
// Add JSON mode support
|
|
61
|
+
// Add JSON mode support
|
|
52
62
|
if (options.responseFormat) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// Apply temperature/maxTokens overrides even without JSON mode
|
|
56
|
-
modelConfig.generationConfig = {
|
|
57
|
-
temperature: options.temperature ?? temperature,
|
|
58
|
-
maxOutputTokens: options.maxTokens ?? maxTokens,
|
|
59
|
-
};
|
|
63
|
+
const formatConfig = this._buildGenerationConfig(options, maxTokens, temperature);
|
|
64
|
+
Object.assign(generationConfig, formatConfig);
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
const model = this.client.getGenerativeModel(modelConfig);
|
|
63
|
-
|
|
64
67
|
// Pre-process messages to handle the 'system' role for Gemini
|
|
65
68
|
const geminiMessages = [];
|
|
66
69
|
let systemContentBuffer = [];
|
|
@@ -79,7 +82,7 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
const
|
|
85
|
+
const contents = geminiMessages.map((msg, index) => {
|
|
83
86
|
let role = '';
|
|
84
87
|
let parts;
|
|
85
88
|
|
|
@@ -115,32 +118,38 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
115
118
|
return { role, parts };
|
|
116
119
|
}).filter(Boolean);
|
|
117
120
|
|
|
118
|
-
while (
|
|
119
|
-
|
|
121
|
+
while (contents.length > 0 && contents[0].role !== 'user') {
|
|
122
|
+
contents.shift();
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
if (
|
|
125
|
+
if (contents.length === 0) {
|
|
123
126
|
throw new LLMServiceException('Cannot process a conversation with no user messages.', 400);
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
|
|
127
|
-
const
|
|
129
|
+
// Use the new @google/genai API
|
|
130
|
+
const result = await this.client.models.generateContent({
|
|
131
|
+
model: modelName,
|
|
132
|
+
contents: contents,
|
|
133
|
+
systemInstruction: systemPrompt,
|
|
134
|
+
generationConfig: generationConfig,
|
|
135
|
+
tools: tools ? [{ functionDeclarations: tools.map(t => t.function) }] : undefined,
|
|
136
|
+
});
|
|
128
137
|
|
|
129
|
-
const result = await chat.sendMessage(lastMessage.parts);
|
|
130
138
|
const response = result.response;
|
|
131
|
-
const toolCalls = response.functionCalls();
|
|
139
|
+
const toolCalls = response.functionCalls?.() || response.functionCalls || null;
|
|
132
140
|
|
|
133
141
|
let textContent = '';
|
|
134
142
|
try {
|
|
135
|
-
textContent = response.text();
|
|
143
|
+
textContent = typeof response.text === 'function' ? response.text() : (response.text || '');
|
|
136
144
|
} catch (e) {
|
|
137
145
|
// response.text() throws if there is no text content (e.g. only tool calls)
|
|
138
146
|
// This is expected behavior for tool-only responses
|
|
139
147
|
}
|
|
148
|
+
|
|
140
149
|
// Validate that we have EITHER content OR tool calls
|
|
141
150
|
if (!textContent && (!toolCalls || toolCalls.length === 0)) {
|
|
142
151
|
console.error('[GeminiProvider] Model returned empty response (no text, no tool calls)');
|
|
143
|
-
console.error('[GeminiProvider]
|
|
152
|
+
console.error('[GeminiProvider] Contents:', JSON.stringify(contents, null, 2));
|
|
144
153
|
throw new LLMServiceException(
|
|
145
154
|
'Model returned empty response. This usually means the prompt or schema is confusing the model.',
|
|
146
155
|
500
|
|
@@ -150,7 +159,7 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
150
159
|
// Return with parsed JSON if applicable
|
|
151
160
|
return {
|
|
152
161
|
content: textContent,
|
|
153
|
-
tool_calls: toolCalls ? toolCalls.map(fc => ({ type: 'function', function: fc })) : null,
|
|
162
|
+
tool_calls: toolCalls ? (Array.isArray(toolCalls) ? toolCalls : [toolCalls]).map(fc => ({ type: 'function', function: fc })) : null,
|
|
154
163
|
_responseFormat: options.responseFormat,
|
|
155
164
|
...(options.responseFormat && this._shouldAutoParse(options) ? {
|
|
156
165
|
parsedContent: this._safeJsonParse(textContent)
|
|
@@ -158,6 +167,7 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
158
167
|
};
|
|
159
168
|
}
|
|
160
169
|
|
|
170
|
+
|
|
161
171
|
_buildGenerationConfig(options, maxTokens, temperature) {
|
|
162
172
|
const config = {
|
|
163
173
|
temperature: options.temperature ?? temperature,
|
|
@@ -299,11 +309,6 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
299
309
|
}
|
|
300
310
|
|
|
301
311
|
async imageGeneration(prompt, modelName, systemPrompt, options = {}) {
|
|
302
|
-
const model = this.client.getGenerativeModel({
|
|
303
|
-
model: modelName,
|
|
304
|
-
systemInstruction: systemPrompt,
|
|
305
|
-
});
|
|
306
|
-
|
|
307
312
|
const generationConfig = {
|
|
308
313
|
responseModalities: ["IMAGE"],
|
|
309
314
|
};
|
|
@@ -327,11 +332,14 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
327
332
|
});
|
|
328
333
|
}
|
|
329
334
|
|
|
330
|
-
|
|
335
|
+
// Use the new @google/genai API
|
|
336
|
+
const result = await this.client.models.generateContent({
|
|
337
|
+
model: modelName,
|
|
331
338
|
contents: [{
|
|
332
339
|
role: "user",
|
|
333
340
|
parts: parts
|
|
334
341
|
}],
|
|
342
|
+
systemInstruction: systemPrompt,
|
|
335
343
|
generationConfig
|
|
336
344
|
});
|
|
337
345
|
|
|
@@ -355,40 +363,58 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
355
363
|
}
|
|
356
364
|
|
|
357
365
|
async startVideoGeneration(prompt, images, modelName, systemPrompt, options = {}) {
|
|
358
|
-
//
|
|
366
|
+
// Use unified client for video generation
|
|
359
367
|
const operation = await this.client.models.generateVideos({
|
|
360
368
|
model: modelName,
|
|
361
369
|
prompt: prompt,
|
|
362
370
|
config: {
|
|
363
|
-
|
|
371
|
+
durationSeconds: options.durationSeconds || 6,
|
|
372
|
+
aspectRatio: options.aspectRatio || '16:9',
|
|
373
|
+
numberOfVideos: 1,
|
|
374
|
+
// Pass reference images if provided
|
|
375
|
+
...(images && images.length > 0 ? { referenceImages: images } : {}),
|
|
364
376
|
}
|
|
365
377
|
});
|
|
366
378
|
|
|
379
|
+
// Store operation for later polling
|
|
380
|
+
this._pendingOperations.set(operation.name, operation);
|
|
381
|
+
|
|
367
382
|
return { operationName: operation.name };
|
|
368
383
|
}
|
|
369
384
|
|
|
370
385
|
async getVideoGenerationStatus(operationName) {
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
386
|
+
// Get the operation from cache or fetch it
|
|
387
|
+
let operation = this._pendingOperations.get(operationName);
|
|
388
|
+
|
|
389
|
+
if (!operation) {
|
|
390
|
+
// If not in cache, we need to fetch it by name
|
|
391
|
+
operation = await this.client.models.getOperation(operationName);
|
|
392
|
+
}
|
|
375
393
|
|
|
376
394
|
// Refresh status
|
|
377
|
-
await operation.get();
|
|
395
|
+
operation = await operation.get();
|
|
396
|
+
|
|
397
|
+
// Update cache
|
|
398
|
+
this._pendingOperations.set(operationName, operation);
|
|
378
399
|
|
|
379
400
|
const result = {
|
|
380
401
|
done: operation.done,
|
|
381
|
-
// Extract progress if available in metadata
|
|
382
402
|
progress: operation.metadata?.progressPercent || 0,
|
|
383
403
|
state: operation.metadata?.state || (operation.done ? 'COMPLETED' : 'PROCESSING'),
|
|
384
404
|
};
|
|
385
405
|
|
|
386
406
|
if (operation.done) {
|
|
407
|
+
// Clean up from cache
|
|
408
|
+
this._pendingOperations.delete(operationName);
|
|
409
|
+
|
|
387
410
|
if (operation.error) {
|
|
388
411
|
result.error = operation.error;
|
|
389
412
|
} else {
|
|
390
413
|
const videoResult = operation.response;
|
|
391
|
-
|
|
414
|
+
// Extract video URI from response
|
|
415
|
+
result.videoUri = videoResult.videos?.[0]?.gcsUri ||
|
|
416
|
+
videoResult.uri ||
|
|
417
|
+
(videoResult.generatedAssets?.[0]?.uri);
|
|
392
418
|
result.content = "Video generation completed.";
|
|
393
419
|
}
|
|
394
420
|
}
|