@contentgrowth/llm-service 0.7.1 → 0.7.3
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
package/src/index.js
CHANGED
|
@@ -5,3 +5,5 @@ export { MODEL_CONFIGS } from './llm/config-manager.js';
|
|
|
5
5
|
export { OpenAIProvider } from './llm/providers/openai-provider.js';
|
|
6
6
|
export { GeminiProvider } from './llm/providers/gemini-provider.js';
|
|
7
7
|
export { extractJsonFromResponse } from './llm/json-utils.js';
|
|
8
|
+
export { FINISH_REASONS } from './llm/providers/base-provider.js';
|
|
9
|
+
|
|
@@ -94,7 +94,7 @@ export class DefaultConfigProvider extends BaseConfigProvider {
|
|
|
94
94
|
apiKey: tenantConfig.api_key,
|
|
95
95
|
models: MODEL_CONFIGS[tenantConfig.provider],
|
|
96
96
|
temperature: parseFloat(env.DEFAULT_TEMPERATURE || '0.7'),
|
|
97
|
-
maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || '
|
|
97
|
+
maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || '16384'),
|
|
98
98
|
capabilities: tenantConfig.capabilities || { chat: true, image: false, video: false },
|
|
99
99
|
isTenantOwned: true
|
|
100
100
|
};
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized finish reasons across all LLM providers.
|
|
3
|
+
* Providers map their native values to these standard constants.
|
|
4
|
+
*/
|
|
5
|
+
export const FINISH_REASONS = {
|
|
6
|
+
COMPLETED: 'completed', // Normal completion (OpenAI: stop, Gemini: STOP, Anthropic: end_turn)
|
|
7
|
+
TRUNCATED: 'truncated', // Hit max tokens (OpenAI: length, Gemini: MAX_TOKENS, Anthropic: max_tokens)
|
|
8
|
+
CONTENT_FILTER: 'content_filter', // Content was filtered
|
|
9
|
+
TOOL_CALL: 'tool_call', // Stopped for tool call
|
|
10
|
+
UNKNOWN: 'unknown', // Unknown/unmapped reason
|
|
11
|
+
};
|
|
12
|
+
|
|
1
13
|
/**
|
|
2
14
|
* Abstract base class for LLM Providers.
|
|
3
15
|
* Defines the standard interface that all providers must implement.
|
|
@@ -7,6 +19,39 @@ export class BaseLLMProvider {
|
|
|
7
19
|
this.config = config;
|
|
8
20
|
}
|
|
9
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Normalize provider-specific finish reason to standard value.
|
|
24
|
+
* Override in subclass if provider uses different values.
|
|
25
|
+
* @param {string} providerReason - The provider's native finish reason
|
|
26
|
+
* @returns {string} Standardized finish reason from FINISH_REASONS
|
|
27
|
+
*/
|
|
28
|
+
normalizeFinishReason(providerReason) {
|
|
29
|
+
// Default mappings - providers can override
|
|
30
|
+
const upperReason = (providerReason || '').toUpperCase();
|
|
31
|
+
|
|
32
|
+
// Completed mappings
|
|
33
|
+
if (['STOP', 'END_TURN'].includes(upperReason)) {
|
|
34
|
+
return FINISH_REASONS.COMPLETED;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Truncated mappings
|
|
38
|
+
if (['LENGTH', 'MAX_TOKENS'].includes(upperReason)) {
|
|
39
|
+
return FINISH_REASONS.TRUNCATED;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Content filter mappings
|
|
43
|
+
if (['CONTENT_FILTER', 'SAFETY'].includes(upperReason)) {
|
|
44
|
+
return FINISH_REASONS.CONTENT_FILTER;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Tool call mappings
|
|
48
|
+
if (['TOOL_CALLS', 'TOOL_USE', 'FUNCTION_CALL'].includes(upperReason)) {
|
|
49
|
+
return FINISH_REASONS.TOOL_CALL;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return FINISH_REASONS.UNKNOWN;
|
|
53
|
+
}
|
|
54
|
+
|
|
10
55
|
/**
|
|
11
56
|
* Simple chat interface for single-turn conversations
|
|
12
57
|
* @param {string} userMessage
|
|
@@ -180,7 +180,7 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
console.log('[GeminiProvider] generateContent request:', JSON.stringify(requestOptions, null, 2));
|
|
183
|
+
// console.log('[GeminiProvider] generateContent request:', JSON.stringify(requestOptions, null, 2));
|
|
184
184
|
|
|
185
185
|
let response;
|
|
186
186
|
try {
|
|
@@ -228,11 +228,16 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
228
228
|
);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
console.log('Gemini returns:', textContent);
|
|
231
|
+
// console.log('Gemini returns:', textContent);
|
|
232
232
|
// Return with parsed JSON if applicable
|
|
233
|
+
// Normalize the finish reason to standard value for consistent handling
|
|
234
|
+
const normalizedFinishReason = this.normalizeFinishReason(candidate.finishReason);
|
|
235
|
+
|
|
233
236
|
return {
|
|
234
237
|
content: textContent,
|
|
235
238
|
tool_calls: toolCalls ? (Array.isArray(toolCalls) ? toolCalls : [toolCalls]).map(fc => ({ type: 'function', function: fc })) : null,
|
|
239
|
+
finishReason: normalizedFinishReason, // Standardized: 'completed', 'truncated', etc.
|
|
240
|
+
_rawFinishReason: candidate.finishReason, // Keep original for debugging
|
|
236
241
|
_responseFormat: options.responseFormat,
|
|
237
242
|
...(options.responseFormat && this._shouldAutoParse(options) ? {
|
|
238
243
|
parsedContent: this._safeJsonParse(textContent)
|
|
@@ -265,7 +270,7 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
265
270
|
// Use responseSchema for strict structured output
|
|
266
271
|
// Must convert to Gemini Schema format (Uppercase types)
|
|
267
272
|
config.responseSchema = this._convertToGeminiSchema(schema);
|
|
268
|
-
console.log('[GeminiProvider] Using Strict JSON mode with schema (responseSchema)');
|
|
273
|
+
// console.log('[GeminiProvider] Using Strict JSON mode with schema (responseSchema)');
|
|
269
274
|
} else {
|
|
270
275
|
console.warn('[GeminiProvider] Using legacy JSON mode without schema - may produce markdown wrappers');
|
|
271
276
|
}
|
|
@@ -336,9 +341,7 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
336
341
|
// - Brace extraction as fallback
|
|
337
342
|
const parsed = extractJsonFromResponse(content);
|
|
338
343
|
|
|
339
|
-
if (parsed) {
|
|
340
|
-
console.log('[GeminiProvider] Successfully parsed JSON from response');
|
|
341
|
-
} else {
|
|
344
|
+
if (!parsed) {
|
|
342
345
|
console.error('[GeminiProvider] Failed to extract valid JSON from response');
|
|
343
346
|
console.error('[GeminiProvider] Content preview:', content.substring(0, 200));
|
|
344
347
|
}
|
|
@@ -354,7 +357,7 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
354
357
|
const tool_call_id = `gemini-tool-call-${index}`;
|
|
355
358
|
toolCall.id = tool_call_id;
|
|
356
359
|
|
|
357
|
-
console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.args);
|
|
360
|
+
// console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.args);
|
|
358
361
|
|
|
359
362
|
if (!tool) {
|
|
360
363
|
console.error(`[Tool Error] Tool '${toolName}' not found`);
|
|
@@ -362,7 +365,7 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
362
365
|
}
|
|
363
366
|
try {
|
|
364
367
|
const output = await tool(toolCall.function.args, { env, tenantId });
|
|
365
|
-
console.log(`[Tool Result] ${toolName} returned:`, output.substring(0, 200) + (output.length > 200 ? '...' : ''));
|
|
368
|
+
// console.log(`[Tool Result] ${toolName} returned:`, output.substring(0, 200) + (output.length > 200 ? '...' : ''));
|
|
366
369
|
return { tool_call_id, output };
|
|
367
370
|
} catch (error) {
|
|
368
371
|
console.error(`[Tool Error] ${toolName} failed:`, error.message);
|
|
@@ -411,7 +414,7 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
411
414
|
requestOptions.config.systemInstruction = { parts: [{ text: systemPrompt }] };
|
|
412
415
|
}
|
|
413
416
|
|
|
414
|
-
console.log('[GeminiProvider] imageGeneration request:', JSON.stringify(requestOptions, null, 2));
|
|
417
|
+
// console.log('[GeminiProvider] imageGeneration request:', JSON.stringify(requestOptions, null, 2));
|
|
415
418
|
|
|
416
419
|
const response = await this.client.models.generateContent(requestOptions);
|
|
417
420
|
|
|
@@ -449,9 +452,12 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
449
452
|
|
|
450
453
|
async startVideoGeneration(prompt, images, modelName, systemPrompt, options = {}) {
|
|
451
454
|
// Use unified client for video generation
|
|
452
|
-
|
|
455
|
+
// Prepend system prompt to user prompt if provided, as video models often expect instructions in the prompt
|
|
456
|
+
const effectivePrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
|
|
457
|
+
|
|
458
|
+
const requestConfig = {
|
|
453
459
|
model: modelName,
|
|
454
|
-
prompt:
|
|
460
|
+
prompt: effectivePrompt,
|
|
455
461
|
config: {
|
|
456
462
|
durationSeconds: options.durationSeconds || 6,
|
|
457
463
|
aspectRatio: options.aspectRatio || '16:9',
|
|
@@ -459,15 +465,35 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
459
465
|
// Pass reference images if provided
|
|
460
466
|
...(images && images.length > 0 ? { referenceImages: images } : {}),
|
|
461
467
|
}
|
|
462
|
-
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// Create a loggable copy of the config
|
|
471
|
+
const logConfig = JSON.parse(JSON.stringify(requestConfig));
|
|
472
|
+
if (logConfig.config && logConfig.config.referenceImages) {
|
|
473
|
+
logConfig.config.referenceImages = logConfig.config.referenceImages.map(img => ({
|
|
474
|
+
...img,
|
|
475
|
+
data: `... (${img.data ? img.data.length : 0} bytes)` // Summarize data
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
console.log('[GeminiProvider] startVideoGeneration request:', JSON.stringify(logConfig, null, 2));
|
|
463
480
|
|
|
464
|
-
|
|
465
|
-
|
|
481
|
+
try {
|
|
482
|
+
const operation = await this.client.models.generateVideos(requestConfig);
|
|
483
|
+
|
|
484
|
+
// Store operation for later polling
|
|
485
|
+
this._pendingOperations.set(operation.name, operation);
|
|
466
486
|
|
|
467
|
-
|
|
487
|
+
return { operationName: operation.name };
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error('[GeminiProvider] startVideoGeneration failed:', error);
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
468
492
|
}
|
|
469
493
|
|
|
470
494
|
async getVideoGenerationStatus(operationName) {
|
|
495
|
+
console.log(`[GeminiProvider] Checking status for operation: ${operationName}`);
|
|
496
|
+
|
|
471
497
|
// Get the operation from cache or fetch it
|
|
472
498
|
let operation = this._pendingOperations.get(operationName);
|
|
473
499
|
|
|
@@ -488,6 +514,8 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
488
514
|
state: operation.metadata?.state || (operation.done ? 'COMPLETED' : 'PROCESSING'),
|
|
489
515
|
};
|
|
490
516
|
|
|
517
|
+
console.log(`[GeminiProvider] Operation status: ${result.state}, Progress: ${result.progress}%`);
|
|
518
|
+
|
|
491
519
|
if (operation.done) {
|
|
492
520
|
// Clean up from cache
|
|
493
521
|
this._pendingOperations.delete(operationName);
|
|
@@ -68,10 +68,16 @@ export class OpenAIProvider extends BaseLLMProvider {
|
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Normalize the finish reason to standard value for consistent handling
|
|
72
|
+
const rawFinishReason = response.choices[0].finish_reason;
|
|
73
|
+
const normalizedFinishReason = this.normalizeFinishReason(rawFinishReason);
|
|
74
|
+
|
|
71
75
|
// Return with parsed JSON if applicable
|
|
72
76
|
return {
|
|
73
77
|
content: message.content,
|
|
74
78
|
tool_calls: message.tool_calls,
|
|
79
|
+
finishReason: normalizedFinishReason, // Standardized: 'completed', 'truncated', etc.
|
|
80
|
+
_rawFinishReason: rawFinishReason, // Keep original for debugging
|
|
75
81
|
// Add metadata about response format
|
|
76
82
|
_responseFormat: options.responseFormat,
|
|
77
83
|
// Auto-parse JSON if requested
|
package/src/llm-service.js
CHANGED
|
@@ -174,12 +174,12 @@ export class LLMService {
|
|
|
174
174
|
options
|
|
175
175
|
);
|
|
176
176
|
|
|
177
|
-
let { content, tool_calls, parsedContent } = initialResponse;
|
|
177
|
+
let { content, tool_calls, parsedContent, finishReason } = initialResponse;
|
|
178
178
|
|
|
179
179
|
// Tool execution loop with safety limit
|
|
180
180
|
while (tool_calls && iteration < MAX_ITERATIONS) {
|
|
181
181
|
iteration++;
|
|
182
|
-
console.log(`[Tool Call] Iteration ${iteration}/${MAX_ITERATIONS}: Assistant wants to use tools:`, tool_calls);
|
|
182
|
+
console.log(`[Tool Call] Iteration ${iteration}/${MAX_ITERATIONS} with finish reason ${finishReason}: Assistant wants to use tools:`, tool_calls);
|
|
183
183
|
currentMessages.push({ role: 'assistant', content: content || '', tool_calls });
|
|
184
184
|
|
|
185
185
|
// Execute tools using the provider's helper (which formats results for that provider)
|
|
@@ -196,6 +196,7 @@ export class LLMService {
|
|
|
196
196
|
content = nextResponse.content;
|
|
197
197
|
tool_calls = nextResponse.tool_calls;
|
|
198
198
|
parsedContent = nextResponse.parsedContent; // Preserve parsedContent from final response
|
|
199
|
+
finishReason = nextResponse.finishReason; // Preserve finishReason from final response
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
if (iteration >= MAX_ITERATIONS) {
|
|
@@ -203,7 +204,7 @@ export class LLMService {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
// Return both content and parsedContent (if available)
|
|
206
|
-
return { content, parsedContent, toolCalls: tool_calls };
|
|
207
|
+
return { content, parsedContent, toolCalls: tool_calls, finishReason };
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
/**
|