@contentgrowth/llm-service 0.7.2 → 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.2",
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
@@ -230,9 +230,14 @@ export class GeminiProvider extends BaseLLMProvider {
230
230
 
231
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)
@@ -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
  /**