@contentgrowth/llm-service 0.6.91 → 0.6.94

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.91",
3
+ "version": "0.6.94",
4
4
  "description": "Unified LLM Service for Content Growth",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -127,58 +127,60 @@ export class GeminiProvider extends BaseLLMProvider {
127
127
  }
128
128
 
129
129
  // Use the new @google/genai API
130
- const result = await this.client.models.generateContent({
130
+ // Use the new @google/genai API
131
+ const requestOptions = {
131
132
  model: modelName,
132
133
  contents: contents,
133
- systemInstruction: systemPrompt,
134
- generationConfig: generationConfig,
135
- tools: tools ? [{ functionDeclarations: tools.map(t => t.function) }] : undefined,
136
- });
134
+ config: generationConfig,
135
+ };
137
136
 
138
- // New SDK returns result directly, not result.response
139
- // Debug log to understand structure
140
- console.log('[GeminiProvider] Result structure:', JSON.stringify(Object.keys(result), null, 2));
137
+ if (systemPrompt) {
138
+ requestOptions.systemInstruction = systemPrompt;
139
+ }
141
140
 
142
- // Handle both old (result.response) and new (direct result) structures
143
- const response = result.response || result;
141
+ if (tools && tools.length > 0) {
142
+ requestOptions.tools = [{ functionDeclarations: tools.map(t => t.function) }];
143
+ }
144
144
 
145
- // Extract function calls - try multiple possible locations
146
- let toolCalls = null;
147
- if (typeof response.functionCalls === 'function') {
148
- toolCalls = response.functionCalls();
149
- } else if (response.functionCalls) {
150
- toolCalls = response.functionCalls;
151
- } else if (response.candidates?.[0]?.content?.parts) {
152
- // Check parts for function calls
153
- const functionCallParts = response.candidates[0].content.parts.filter(p => p.functionCall);
154
- if (functionCallParts.length > 0) {
155
- toolCalls = functionCallParts.map(p => p.functionCall);
156
- }
145
+ console.log('[GeminiProvider] generateContent request:', JSON.stringify(requestOptions, null, 2));
146
+
147
+ let response;
148
+ try {
149
+ response = await this.client.models.generateContent(requestOptions);
150
+ } catch (error) {
151
+ console.error('[GeminiProvider] generateContent failed:', error);
152
+ throw error;
153
+ }
154
+
155
+ // In @google/genai, the response is returned directly (no .response property)
156
+ // And helper methods like .text() or .functionCalls() might not exist on the raw object
157
+ // So we extract manually from candidates
158
+
159
+ const candidate = response.candidates?.[0];
160
+ if (!candidate) {
161
+ throw new LLMServiceException('No candidates returned from model', 500);
157
162
  }
158
163
 
159
- // Extract text content - try multiple possible locations
164
+ const parts = candidate.content?.parts || [];
165
+
166
+ // Extract text and function calls
160
167
  let textContent = '';
161
- try {
162
- if (typeof response.text === 'function') {
163
- textContent = response.text();
164
- } else if (typeof response.text === 'string') {
165
- textContent = response.text;
166
- } else if (response.candidates?.[0]?.content?.parts) {
167
- // Concatenate text from parts
168
- textContent = response.candidates[0].content.parts
169
- .filter(p => p.text)
170
- .map(p => p.text)
171
- .join('');
168
+ let toolCalls = null;
169
+
170
+ for (const part of parts) {
171
+ if (part.text) {
172
+ textContent += part.text;
173
+ }
174
+ if (part.functionCall) {
175
+ if (!toolCalls) toolCalls = [];
176
+ toolCalls.push(part.functionCall);
172
177
  }
173
- } catch (e) {
174
- // response.text() throws if there is no text content (e.g. only tool calls)
175
- // This is expected behavior for tool-only responses
176
178
  }
177
179
 
178
180
  // Validate that we have EITHER content OR tool calls
179
181
  if (!textContent && (!toolCalls || toolCalls.length === 0)) {
180
182
  console.error('[GeminiProvider] Model returned empty response (no text, no tool calls)');
181
- console.error('[GeminiProvider] Full result:', JSON.stringify(result, null, 2));
183
+ console.error('[GeminiProvider] Contents:', JSON.stringify(contents, null, 2));
182
184
  throw new LLMServiceException(
183
185
  'Model returned empty response. This usually means the prompt or schema is confusing the model.',
184
186
  500
@@ -211,15 +213,17 @@ export class GeminiProvider extends BaseLLMProvider {
211
213
 
212
214
  const schema = typeof options.responseFormat === 'object'
213
215
  ? options.responseFormat.schema
214
- : null;
216
+ : options.responseSchema || null;
215
217
 
216
218
  if (formatType === 'json' || formatType === 'json_schema') {
217
219
  config.responseMimeType = 'application/json';
218
220
 
219
221
  // CRITICAL: Must provide schema for "Strict Mode" to avoid markdown wrappers
220
222
  if (schema) {
223
+ // Use responseSchema for strict structured output
224
+ // Must convert to Gemini Schema format (Uppercase types)
221
225
  config.responseSchema = this._convertToGeminiSchema(schema);
222
- console.log('[GeminiProvider] Using Strict JSON mode with schema');
226
+ console.log('[GeminiProvider] Using Strict JSON mode with schema (responseSchema)');
223
227
  } else {
224
228
  console.warn('[GeminiProvider] Using legacy JSON mode without schema - may produce markdown wrappers');
225
229
  }
@@ -230,25 +234,15 @@ export class GeminiProvider extends BaseLLMProvider {
230
234
  }
231
235
 
232
236
  _convertToGeminiSchema(jsonSchema) {
233
- // SchemaType constants for Gemini schema conversion
234
- const SchemaType = {
235
- STRING: 'STRING',
236
- NUMBER: 'NUMBER',
237
- INTEGER: 'INTEGER',
238
- BOOLEAN: 'BOOLEAN',
239
- ARRAY: 'ARRAY',
240
- OBJECT: 'OBJECT'
241
- };
242
-
243
237
  const convertType = (type) => {
244
238
  switch (type) {
245
- case 'string': return SchemaType.STRING;
246
- case 'number': return SchemaType.NUMBER;
247
- case 'integer': return SchemaType.INTEGER;
248
- case 'boolean': return SchemaType.BOOLEAN;
249
- case 'array': return SchemaType.ARRAY;
250
- case 'object': return SchemaType.OBJECT;
251
- default: return SchemaType.STRING;
239
+ case 'string': return 'STRING';
240
+ case 'number': return 'NUMBER';
241
+ case 'integer': return 'INTEGER';
242
+ case 'boolean': return 'BOOLEAN';
243
+ case 'array': return 'ARRAY';
244
+ case 'object': return 'OBJECT';
245
+ default: return 'STRING';
252
246
  }
253
247
  };
254
248
 
@@ -362,22 +356,33 @@ export class GeminiProvider extends BaseLLMProvider {
362
356
  }
363
357
 
364
358
  // Use the new @google/genai API
365
- const result = await this.client.models.generateContent({
359
+ const requestOptions = {
366
360
  model: modelName,
367
361
  contents: [{
368
362
  role: "user",
369
363
  parts: parts
370
364
  }],
371
- systemInstruction: systemPrompt,
372
- generationConfig
373
- });
365
+ config: generationConfig
366
+ };
367
+
368
+ if (systemPrompt) {
369
+ requestOptions.systemInstruction = systemPrompt;
370
+ }
371
+
372
+ console.log('[GeminiProvider] imageGeneration request:', JSON.stringify(requestOptions, null, 2));
373
+
374
+ const response = await this.client.models.generateContent(requestOptions);
374
375
 
375
- const response = result.response;
376
376
  const imagePart = response.candidates?.[0]?.content?.parts?.find(
377
377
  part => part.inlineData && part.inlineData.mimeType?.startsWith('image/')
378
378
  );
379
379
 
380
380
  if (!imagePart || !imagePart.inlineData) {
381
+ // Fallback: Check if it returned a URI or other format, or just text
382
+ const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
383
+ if (textPart) {
384
+ console.warn('[GeminiProvider] Model returned text instead of image:', textPart.text);
385
+ }
381
386
  throw new Error('No image data in response');
382
387
  }
383
388
 
@@ -93,7 +93,7 @@ export class OpenAIProvider extends BaseLLMProvider {
93
93
 
94
94
  const schema = typeof options.responseFormat === 'object'
95
95
  ? options.responseFormat.schema
96
- : null;
96
+ : options.responseSchema || null;
97
97
 
98
98
  switch (formatType) {
99
99
  case 'json':
@@ -115,7 +115,7 @@ export class OpenAIProvider extends BaseLLMProvider {
115
115
 
116
116
  case 'json_schema':
117
117
  if (!schema) {
118
- throw new Error('schema required when using json_schema format');
118
+ throw new Error('responseSchema required when using json_schema format');
119
119
  }
120
120
  console.log('[OpenAIProvider] Using Strict JSON mode with schema');
121
121
  return {