@contentgrowth/llm-service 0.6.92 → 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.92",
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,44 +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 candidates directly on result
139
- // Extract function calls from parts
140
- let toolCalls = null;
141
- if (result.candidates?.[0]?.content?.parts) {
142
- const functionCallParts = result.candidates[0].content.parts.filter(p => p.functionCall);
143
- if (functionCallParts.length > 0) {
144
- toolCalls = functionCallParts.map(p => p.functionCall);
145
- }
137
+ if (systemPrompt) {
138
+ requestOptions.systemInstruction = systemPrompt;
146
139
  }
147
140
 
148
- // Extract text content from parts
149
- let textContent = '';
141
+ if (tools && tools.length > 0) {
142
+ requestOptions.tools = [{ functionDeclarations: tools.map(t => t.function) }];
143
+ }
144
+
145
+ console.log('[GeminiProvider] generateContent request:', JSON.stringify(requestOptions, null, 2));
146
+
147
+ let response;
150
148
  try {
151
- if (result.candidates?.[0]?.content?.parts) {
152
- // Concatenate text from parts
153
- textContent = result.candidates[0].content.parts
154
- .filter(p => p.text)
155
- .map(p => p.text)
156
- .join('');
157
- } else if (result.text) {
158
- textContent = typeof result.text === 'function' ? result.text() : result.text;
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);
162
+ }
163
+
164
+ const parts = candidate.content?.parts || [];
165
+
166
+ // Extract text and function calls
167
+ let textContent = '';
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);
159
177
  }
160
- } catch (e) {
161
- // This is expected behavior for tool-only responses
162
178
  }
163
179
 
164
180
  // Validate that we have EITHER content OR tool calls
165
181
  if (!textContent && (!toolCalls || toolCalls.length === 0)) {
166
182
  console.error('[GeminiProvider] Model returned empty response (no text, no tool calls)');
167
- console.error('[GeminiProvider] Full result:', JSON.stringify(result, null, 2));
183
+ console.error('[GeminiProvider] Contents:', JSON.stringify(contents, null, 2));
168
184
  throw new LLMServiceException(
169
185
  'Model returned empty response. This usually means the prompt or schema is confusing the model.',
170
186
  500
@@ -197,15 +213,17 @@ export class GeminiProvider extends BaseLLMProvider {
197
213
 
198
214
  const schema = typeof options.responseFormat === 'object'
199
215
  ? options.responseFormat.schema
200
- : null;
216
+ : options.responseSchema || null;
201
217
 
202
218
  if (formatType === 'json' || formatType === 'json_schema') {
203
219
  config.responseMimeType = 'application/json';
204
220
 
205
221
  // CRITICAL: Must provide schema for "Strict Mode" to avoid markdown wrappers
206
222
  if (schema) {
223
+ // Use responseSchema for strict structured output
224
+ // Must convert to Gemini Schema format (Uppercase types)
207
225
  config.responseSchema = this._convertToGeminiSchema(schema);
208
- console.log('[GeminiProvider] Using Strict JSON mode with schema');
226
+ console.log('[GeminiProvider] Using Strict JSON mode with schema (responseSchema)');
209
227
  } else {
210
228
  console.warn('[GeminiProvider] Using legacy JSON mode without schema - may produce markdown wrappers');
211
229
  }
@@ -216,25 +234,15 @@ export class GeminiProvider extends BaseLLMProvider {
216
234
  }
217
235
 
218
236
  _convertToGeminiSchema(jsonSchema) {
219
- // SchemaType constants for Gemini schema conversion
220
- const SchemaType = {
221
- STRING: 'STRING',
222
- NUMBER: 'NUMBER',
223
- INTEGER: 'INTEGER',
224
- BOOLEAN: 'BOOLEAN',
225
- ARRAY: 'ARRAY',
226
- OBJECT: 'OBJECT'
227
- };
228
-
229
237
  const convertType = (type) => {
230
238
  switch (type) {
231
- case 'string': return SchemaType.STRING;
232
- case 'number': return SchemaType.NUMBER;
233
- case 'integer': return SchemaType.INTEGER;
234
- case 'boolean': return SchemaType.BOOLEAN;
235
- case 'array': return SchemaType.ARRAY;
236
- case 'object': return SchemaType.OBJECT;
237
- 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';
238
246
  }
239
247
  };
240
248
 
@@ -348,23 +356,33 @@ export class GeminiProvider extends BaseLLMProvider {
348
356
  }
349
357
 
350
358
  // Use the new @google/genai API
351
- const result = await this.client.models.generateContent({
359
+ const requestOptions = {
352
360
  model: modelName,
353
361
  contents: [{
354
362
  role: "user",
355
363
  parts: parts
356
364
  }],
357
- systemInstruction: systemPrompt,
358
- generationConfig
359
- });
365
+ config: generationConfig
366
+ };
367
+
368
+ if (systemPrompt) {
369
+ requestOptions.systemInstruction = systemPrompt;
370
+ }
360
371
 
361
- // New SDK returns candidates directly on result, not result.response
362
- const imagePart = result.candidates?.[0]?.content?.parts?.find(
372
+ console.log('[GeminiProvider] imageGeneration request:', JSON.stringify(requestOptions, null, 2));
373
+
374
+ const response = await this.client.models.generateContent(requestOptions);
375
+
376
+ const imagePart = response.candidates?.[0]?.content?.parts?.find(
363
377
  part => part.inlineData && part.inlineData.mimeType?.startsWith('image/')
364
378
  );
365
379
 
366
380
  if (!imagePart || !imagePart.inlineData) {
367
- console.error('[GeminiProvider] No image in response. Result:', JSON.stringify(result, null, 2));
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
+ }
368
386
  throw new Error('No image data in response');
369
387
  }
370
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 {