@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentgrowth/llm-service",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Unified LLM Service for Content Growth",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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 || '4096'),
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
- const operation = await this.client.models.generateVideos({
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: 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
- // Store operation for later polling
465
- this._pendingOperations.set(operation.name, operation);
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
- return { operationName: operation.name };
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
@@ -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
  /**