@contentgrowth/llm-service 0.3.0 → 0.5.0
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/README.md
CHANGED
|
@@ -81,6 +81,126 @@ ConfigManager.setConfigProvider(new MyDatabaseConfigProvider());
|
|
|
81
81
|
const service = new LLMService(env);
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
### JSON Mode & Structured Outputs
|
|
85
|
+
|
|
86
|
+
The service supports native JSON mode for OpenAI and Gemini, guaranteeing valid JSON responses without escaping issues.
|
|
87
|
+
|
|
88
|
+
#### Basic JSON Mode
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
const response = await llmService.chatCompletion(
|
|
92
|
+
messages,
|
|
93
|
+
tenantId,
|
|
94
|
+
'You are a helpful assistant. Always respond in JSON.',
|
|
95
|
+
{ responseFormat: 'json' } // ← Enable JSON mode
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Response includes auto-parsed JSON
|
|
99
|
+
console.log(response.parsedContent); // Already parsed object
|
|
100
|
+
console.log(response.content); // Raw JSON string
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### JSON Mode with Schema Validation (Structured Outputs)
|
|
104
|
+
|
|
105
|
+
Define a schema to guarantee the response structure:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
const schema = {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
answer: { type: 'string' },
|
|
112
|
+
confidence: { type: 'number' },
|
|
113
|
+
sources: {
|
|
114
|
+
type: 'array',
|
|
115
|
+
items: { type: 'string' },
|
|
116
|
+
nullable: true
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
required: ['answer', 'confidence']
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const response = await llmService.chatCompletion(
|
|
123
|
+
messages,
|
|
124
|
+
tenantId,
|
|
125
|
+
systemPrompt,
|
|
126
|
+
{
|
|
127
|
+
responseFormat: 'json_schema',
|
|
128
|
+
responseSchema: schema,
|
|
129
|
+
schemaName: 'question_answer'
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Guaranteed to match schema
|
|
134
|
+
const { answer, confidence, sources } = response.parsedContent;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Convenience Method
|
|
138
|
+
|
|
139
|
+
For JSON-only responses, use `chatCompletionJson()` to get parsed objects directly:
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
// Returns parsed object directly (not response wrapper)
|
|
143
|
+
const data = await llmService.chatCompletionJson(
|
|
144
|
+
messages,
|
|
145
|
+
tenantId,
|
|
146
|
+
systemPrompt,
|
|
147
|
+
schema // optional
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
console.log(data.answer); // Direct access to fields
|
|
151
|
+
console.log(data.confidence); // No .parsedContent needed
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Flexible Call Signatures
|
|
155
|
+
|
|
156
|
+
The `chatCompletion()` method intelligently detects whether you're passing tools, options, or both:
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
// All these work!
|
|
160
|
+
await chatCompletion(messages, tenant, prompt);
|
|
161
|
+
await chatCompletion(messages, tenant, prompt, tools);
|
|
162
|
+
await chatCompletion(messages, tenant, prompt, { responseFormat: 'json' });
|
|
163
|
+
await chatCompletion(messages, tenant, prompt, tools, { responseFormat: 'json' });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### Supported Options
|
|
167
|
+
|
|
168
|
+
- `responseFormat`: `'text'` (default), `'json'`, or `'json_schema'`
|
|
169
|
+
- `responseSchema`: JSON schema object (required for `json_schema` mode)
|
|
170
|
+
- `schemaName`: Name for the schema (optional, for `json_schema` mode)
|
|
171
|
+
- `strictSchema`: Enforce strict validation (default: `true`)
|
|
172
|
+
- `autoParse`: Auto-parse JSON responses (default: `true`)
|
|
173
|
+
- `temperature`: Override temperature
|
|
174
|
+
- `maxTokens`: Override max tokens
|
|
175
|
+
- `tier`: Model tier (`'default'`, `'fast'`, `'smart'`)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
## Testing
|
|
179
|
+
|
|
180
|
+
### Running JSON Mode Tests
|
|
181
|
+
|
|
182
|
+
1. **Create `.env` file** (copy from `.env.example`):
|
|
183
|
+
```bash
|
|
184
|
+
cp .env.example .env
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
2. **Add your API keys** to `.env`:
|
|
188
|
+
```ini
|
|
189
|
+
LLM_PROVIDER=openai # or gemini
|
|
190
|
+
OPENAI_API_KEY=sk-your-key-here
|
|
191
|
+
GEMINI_API_KEY=your-gemini-key-here
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
3. **Run tests**:
|
|
195
|
+
```bash
|
|
196
|
+
npm run test:json # Run comprehensive test suite
|
|
197
|
+
npm run examples:json # Run interactive examples
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
See [TESTING.md](./TESTING.md) for detailed testing documentation.
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
84
204
|
## Publishing
|
|
85
205
|
|
|
86
206
|
To publish this package to NPM:
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentgrowth/llm-service",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Unified LLM Service for Content Growth",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
|
-
"test:live": "node test-live.js"
|
|
9
|
+
"test:live": "node test-live.js",
|
|
10
|
+
"test:json": "node test-json-mode.js",
|
|
11
|
+
"examples:json": "node examples-json-mode.js",
|
|
12
|
+
"prepublishOnly": "echo '\n📦 Package contents:\n' && npm pack --dry-run && echo '\n⚠️ Review the files above before publishing!\n'"
|
|
10
13
|
},
|
|
11
14
|
"author": "Content Growth",
|
|
12
15
|
"license": "MIT",
|
|
@@ -22,8 +22,9 @@ export class BaseLLMProvider {
|
|
|
22
22
|
* @param {Array} messages
|
|
23
23
|
* @param {string} systemPrompt
|
|
24
24
|
* @param {Array} tools
|
|
25
|
+
* @param {Object} options - Generation options (responseFormat, temperature, etc.)
|
|
25
26
|
*/
|
|
26
|
-
async chatCompletion(messages, systemPrompt, tools) {
|
|
27
|
+
async chatCompletion(messages, systemPrompt, tools, options = {}) {
|
|
27
28
|
throw new Error('Method not implemented');
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -28,24 +28,36 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
28
28
|
return { text: response.content };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
async chatCompletion(messages, systemPrompt, tools = null) {
|
|
31
|
+
async chatCompletion(messages, systemPrompt, tools = null, options = {}) {
|
|
32
32
|
return this._chatCompletionWithModel(
|
|
33
33
|
messages,
|
|
34
34
|
systemPrompt,
|
|
35
35
|
tools,
|
|
36
36
|
this.defaultModel,
|
|
37
37
|
this.config.maxTokens,
|
|
38
|
-
this.config.temperature
|
|
38
|
+
this.config.temperature,
|
|
39
|
+
options
|
|
39
40
|
);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature) {
|
|
43
|
+
async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
|
|
43
44
|
const modelConfig = {
|
|
44
45
|
model: modelName,
|
|
45
46
|
systemInstruction: systemPrompt,
|
|
46
47
|
tools: tools ? [{ functionDeclarations: tools.map(t => t.function) }] : undefined,
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
// Add JSON mode support for Gemini
|
|
51
|
+
if (options.responseFormat) {
|
|
52
|
+
modelConfig.generationConfig = this._buildGenerationConfig(options, maxTokens, temperature);
|
|
53
|
+
} else if (options.temperature !== undefined || options.maxTokens !== undefined) {
|
|
54
|
+
// Apply temperature/maxTokens overrides even without JSON mode
|
|
55
|
+
modelConfig.generationConfig = {
|
|
56
|
+
temperature: options.temperature ?? temperature,
|
|
57
|
+
maxOutputTokens: options.maxTokens ?? maxTokens,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
const model = this.client.getGenerativeModel(modelConfig);
|
|
50
62
|
|
|
51
63
|
// Pre-process messages to handle the 'system' role for Gemini
|
|
@@ -117,12 +129,108 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
117
129
|
const response = result.response;
|
|
118
130
|
const toolCalls = response.functionCalls();
|
|
119
131
|
|
|
132
|
+
// Return with parsed JSON if applicable
|
|
120
133
|
return {
|
|
121
134
|
content: response.text(),
|
|
122
135
|
tool_calls: toolCalls ? toolCalls.map(fc => ({ type: 'function', function: fc })) : null,
|
|
136
|
+
_responseFormat: options.responseFormat,
|
|
137
|
+
...(options.responseFormat && this._shouldAutoParse(options) ? {
|
|
138
|
+
parsedContent: this._safeJsonParse(response.text())
|
|
139
|
+
} : {})
|
|
123
140
|
};
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
_buildGenerationConfig(options, maxTokens, temperature) {
|
|
144
|
+
const config = {
|
|
145
|
+
temperature: options.temperature ?? temperature,
|
|
146
|
+
maxOutputTokens: options.maxTokens ?? maxTokens,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
switch (options.responseFormat) {
|
|
150
|
+
case 'json':
|
|
151
|
+
case 'json_schema':
|
|
152
|
+
config.responseMimeType = 'application/json';
|
|
153
|
+
|
|
154
|
+
if (options.responseSchema) {
|
|
155
|
+
config.responseSchema = this._convertToGeminiSchema(options.responseSchema);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return config;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_convertToGeminiSchema(jsonSchema) {
|
|
164
|
+
// SchemaType constants for Gemini schema conversion
|
|
165
|
+
const SchemaType = {
|
|
166
|
+
STRING: 'STRING',
|
|
167
|
+
NUMBER: 'NUMBER',
|
|
168
|
+
INTEGER: 'INTEGER',
|
|
169
|
+
BOOLEAN: 'BOOLEAN',
|
|
170
|
+
ARRAY: 'ARRAY',
|
|
171
|
+
OBJECT: 'OBJECT'
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const convertType = (type) => {
|
|
175
|
+
switch (type) {
|
|
176
|
+
case 'string': return SchemaType.STRING;
|
|
177
|
+
case 'number': return SchemaType.NUMBER;
|
|
178
|
+
case 'integer': return SchemaType.INTEGER;
|
|
179
|
+
case 'boolean': return SchemaType.BOOLEAN;
|
|
180
|
+
case 'array': return SchemaType.ARRAY;
|
|
181
|
+
case 'object': return SchemaType.OBJECT;
|
|
182
|
+
default: return SchemaType.STRING;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const convert = (schema) => {
|
|
187
|
+
const result = {
|
|
188
|
+
type: convertType(schema.type),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (schema.properties) {
|
|
192
|
+
result.properties = {};
|
|
193
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
194
|
+
result.properties[key] = convert(value);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (schema.items) {
|
|
199
|
+
result.items = convert(schema.items);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (schema.required) {
|
|
203
|
+
result.required = schema.required;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (schema.nullable) {
|
|
207
|
+
result.nullable = schema.nullable;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (schema.description) {
|
|
211
|
+
result.description = schema.description;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return convert(jsonSchema);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
_shouldAutoParse(options) {
|
|
221
|
+
return options.autoParse !== false; // Default true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_safeJsonParse(content) {
|
|
225
|
+
if (!content) return null;
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(content);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.warn('[GeminiProvider] Failed to auto-parse JSON response:', e.message);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
126
234
|
async executeTools(tool_calls, messages, tenantId, toolImplementations, env) {
|
|
127
235
|
const toolResults = await Promise.all(
|
|
128
236
|
tool_calls.map(async (toolCall, index) => {
|
|
@@ -192,4 +300,48 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
192
300
|
_getModelForTier(tier) {
|
|
193
301
|
return this.models[tier] || this.models.default;
|
|
194
302
|
}
|
|
303
|
+
|
|
304
|
+
async videoGeneration(prompt, images, modelName, systemPrompt, options = {}) {
|
|
305
|
+
const model = this.client.getGenerativeModel({
|
|
306
|
+
model: modelName,
|
|
307
|
+
systemInstruction: systemPrompt,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Prepare image parts
|
|
311
|
+
const imageParts = images.map(img => ({
|
|
312
|
+
inlineData: {
|
|
313
|
+
data: img.data, // Base64 string
|
|
314
|
+
mimeType: img.mimeType
|
|
315
|
+
}
|
|
316
|
+
}));
|
|
317
|
+
|
|
318
|
+
const result = await model.generateContent({
|
|
319
|
+
contents: [{
|
|
320
|
+
role: "user",
|
|
321
|
+
parts: [
|
|
322
|
+
{ text: prompt },
|
|
323
|
+
...imageParts
|
|
324
|
+
]
|
|
325
|
+
}]
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const response = result.response;
|
|
329
|
+
|
|
330
|
+
// Check for video attachment/URI in the response
|
|
331
|
+
// This structure depends on the specific API response for Veo
|
|
332
|
+
// Assuming it might return a file URI or a specific part type
|
|
333
|
+
|
|
334
|
+
// Fallback: Return text if no specific video part is found,
|
|
335
|
+
// but try to find a URI in the text if possible.
|
|
336
|
+
const text = response.text();
|
|
337
|
+
|
|
338
|
+
// TODO: Update this once Veo API response structure is fully documented/available
|
|
339
|
+
// For now, we return the text which might contain the URI or status.
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
content: text,
|
|
343
|
+
// potential video URI extraction
|
|
344
|
+
videoUri: text.match(/https?:\/\/[^\s]+/) ? text.match(/https?:\/\/[^\s]+/)[0] : null
|
|
345
|
+
};
|
|
346
|
+
}
|
|
195
347
|
}
|
|
@@ -27,36 +27,86 @@ export class OpenAIProvider extends BaseLLMProvider {
|
|
|
27
27
|
return { text: response.content };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
async chatCompletion(messages, systemPrompt, tools = null) {
|
|
30
|
+
async chatCompletion(messages, systemPrompt, tools = null, options = {}) {
|
|
31
31
|
return this._chatCompletionWithModel(
|
|
32
32
|
messages,
|
|
33
33
|
systemPrompt,
|
|
34
34
|
tools,
|
|
35
35
|
this.defaultModel,
|
|
36
36
|
this.config.maxTokens,
|
|
37
|
-
this.config.temperature
|
|
37
|
+
this.config.temperature,
|
|
38
|
+
options
|
|
38
39
|
);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature) {
|
|
42
|
+
async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
|
|
42
43
|
const requestPayload = {
|
|
43
44
|
model: modelName,
|
|
44
|
-
temperature: temperature,
|
|
45
|
-
max_tokens: maxTokens,
|
|
45
|
+
temperature: options.temperature ?? temperature,
|
|
46
|
+
max_tokens: options.maxTokens ?? maxTokens,
|
|
46
47
|
messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
47
48
|
tools: tools,
|
|
48
49
|
tool_choice: tools ? 'auto' : undefined,
|
|
49
50
|
};
|
|
50
51
|
|
|
52
|
+
// Add JSON mode support if requested
|
|
53
|
+
if (options.responseFormat) {
|
|
54
|
+
requestPayload.response_format = this._buildResponseFormat(options);
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
const response = await this.client.chat.completions.create(requestPayload);
|
|
52
58
|
const message = response.choices[0].message;
|
|
53
59
|
|
|
60
|
+
// Return with parsed JSON if applicable
|
|
54
61
|
return {
|
|
55
62
|
content: message.content,
|
|
56
63
|
tool_calls: message.tool_calls,
|
|
64
|
+
// Add metadata about response format
|
|
65
|
+
_responseFormat: options.responseFormat,
|
|
66
|
+
// Auto-parse JSON if requested
|
|
67
|
+
...(options.responseFormat && this._shouldAutoParse(options) ? {
|
|
68
|
+
parsedContent: this._safeJsonParse(message.content)
|
|
69
|
+
} : {})
|
|
57
70
|
};
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
_buildResponseFormat(options) {
|
|
74
|
+
switch (options.responseFormat) {
|
|
75
|
+
case 'json':
|
|
76
|
+
return { type: 'json_object' };
|
|
77
|
+
|
|
78
|
+
case 'json_schema':
|
|
79
|
+
if (!options.responseSchema) {
|
|
80
|
+
throw new Error('responseSchema required when using json_schema format');
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
type: 'json_schema',
|
|
84
|
+
json_schema: {
|
|
85
|
+
name: options.schemaName || 'response_schema',
|
|
86
|
+
strict: options.strictSchema ?? true,
|
|
87
|
+
schema: options.responseSchema
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_shouldAutoParse(options) {
|
|
97
|
+
return options.autoParse !== false; // Default true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_safeJsonParse(content) {
|
|
101
|
+
if (!content) return null;
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(content);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.warn('[OpenAIProvider] Failed to auto-parse JSON response:', e.message);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
60
110
|
async executeTools(tool_calls, messages, tenantId, toolImplementations, env) {
|
|
61
111
|
const toolResults = await Promise.all(
|
|
62
112
|
tool_calls.map(async (toolCall) => {
|
|
@@ -85,4 +135,8 @@ export class OpenAIProvider extends BaseLLMProvider {
|
|
|
85
135
|
_getModelForTier(tier) {
|
|
86
136
|
return this.models[tier] || this.models.default;
|
|
87
137
|
}
|
|
138
|
+
|
|
139
|
+
async videoGeneration(prompt, images, modelName, systemPrompt, options = {}) {
|
|
140
|
+
throw new Error('Video generation is not supported by OpenAI provider yet.');
|
|
141
|
+
}
|
|
88
142
|
}
|
package/src/llm-service.js
CHANGED
|
@@ -60,33 +60,129 @@ export class LLMService {
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Interact with LLM for generation
|
|
63
|
+
*
|
|
64
|
+
* Intelligent signature detection supports:
|
|
65
|
+
* - chatCompletion(messages, tenantId, systemPrompt)
|
|
66
|
+
* - chatCompletion(messages, tenantId, systemPrompt, tools)
|
|
67
|
+
* - chatCompletion(messages, tenantId, systemPrompt, options)
|
|
68
|
+
* - chatCompletion(messages, tenantId, systemPrompt, tools, options)
|
|
69
|
+
*
|
|
70
|
+
* @param {Array} messages - Conversation messages
|
|
71
|
+
* @param {string} tenantId - Tenant identifier
|
|
72
|
+
* @param {string} systemPrompt - System instructions
|
|
73
|
+
* @param {Array|Object} toolsOrOptions - Tools array or options object
|
|
74
|
+
* @param {Object} optionsParam - Options object (if tools provided)
|
|
75
|
+
* @returns {Object} Response with content, tool_calls, and optionally parsedContent
|
|
63
76
|
*/
|
|
64
|
-
async chatCompletion(messages, tenantId, systemPrompt,
|
|
77
|
+
async chatCompletion(messages, tenantId, systemPrompt, toolsOrOptions = null, optionsParam = {}) {
|
|
65
78
|
const provider = await this._getProvider(tenantId);
|
|
66
79
|
|
|
67
80
|
if (!systemPrompt?.trim()) {
|
|
68
81
|
throw new LLMServiceException('No prompt set for bot', 503);
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
|
|
84
|
+
// Intelligent parameter detection
|
|
85
|
+
const { tools, options } = this._parseToolsAndOptions(toolsOrOptions, optionsParam);
|
|
86
|
+
|
|
87
|
+
return provider.chatCompletion(messages, systemPrompt, tools, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Helper to intelligently detect tools vs options parameters
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
_parseToolsAndOptions(param1, param2) {
|
|
95
|
+
// If param1 is null/undefined, use defaults
|
|
96
|
+
if (param1 === null || param1 === undefined) {
|
|
97
|
+
return { tools: null, options: param2 || {} };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If param1 is an array, it's tools
|
|
101
|
+
if (Array.isArray(param1)) {
|
|
102
|
+
return { tools: param1, options: param2 || {} };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// If param1 is an object, check if it looks like options or tools
|
|
106
|
+
if (typeof param1 === 'object') {
|
|
107
|
+
// Check for known options keys
|
|
108
|
+
const optionsKeys = ['responseFormat', 'responseSchema', 'temperature', 'maxTokens', 'tier', 'autoParse', 'strictSchema', 'schemaName'];
|
|
109
|
+
const hasOptionsKeys = optionsKeys.some(key => key in param1);
|
|
110
|
+
|
|
111
|
+
if (hasOptionsKeys) {
|
|
112
|
+
// param1 is options
|
|
113
|
+
return { tools: null, options: param1 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Could be tools in object format (legacy support)
|
|
117
|
+
// Treat as tools if it doesn't have options keys
|
|
118
|
+
return { tools: param1, options: param2 || {} };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Default: treat as tools
|
|
122
|
+
return { tools: param1, options: param2 || {} };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convenience method for JSON-only responses
|
|
127
|
+
* Automatically enables JSON mode and returns parsed object
|
|
128
|
+
*
|
|
129
|
+
* @param {Array} messages - Conversation messages
|
|
130
|
+
* @param {string} tenantId - Tenant identifier
|
|
131
|
+
* @param {string} systemPrompt - System instructions
|
|
132
|
+
* @param {Object} schema - Optional JSON schema for validation
|
|
133
|
+
* @param {Array} tools - Optional tools array
|
|
134
|
+
* @returns {Object} Parsed JSON object
|
|
135
|
+
* @throws {LLMServiceException} If JSON parsing fails
|
|
136
|
+
*/
|
|
137
|
+
async chatCompletionJson(messages, tenantId, systemPrompt, schema = null, tools = null) {
|
|
138
|
+
const options = {
|
|
139
|
+
responseFormat: schema ? 'json_schema' : 'json',
|
|
140
|
+
responseSchema: schema,
|
|
141
|
+
autoParse: true
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const response = await this.chatCompletion(messages, tenantId, systemPrompt, tools, options);
|
|
145
|
+
|
|
146
|
+
// Return parsed content directly or throw if parsing failed
|
|
147
|
+
if (response.parsedContent !== null && response.parsedContent !== undefined) {
|
|
148
|
+
return response.parsedContent;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fallback: try to parse the raw content
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(response.content);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
throw new LLMServiceException(
|
|
156
|
+
'LLM returned invalid JSON despite JSON mode being enabled',
|
|
157
|
+
500,
|
|
158
|
+
{ rawContent: response.content, parseError: e.message }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
72
161
|
}
|
|
73
162
|
|
|
74
163
|
/**
|
|
75
164
|
* Wrap of chatCompletion to handle toolcalls from LLM.
|
|
165
|
+
* @param {Array} messages - Conversation messages
|
|
166
|
+
* @param {string} tenantId - Tenant identifier
|
|
167
|
+
* @param {string} systemPrompt - System instructions
|
|
168
|
+
* @param {Array} tools - Tools array
|
|
169
|
+
* @param {Object} options - Options object (for responseFormat, etc.)
|
|
170
|
+
* @returns {Object} Response with content, tool_calls, and optionally parsedContent
|
|
76
171
|
*/
|
|
77
|
-
async chatWithTools(messages, tenantId, systemPrompt, tools = []) {
|
|
172
|
+
async chatWithTools(messages, tenantId, systemPrompt, tools = [], options = {}) {
|
|
78
173
|
const provider = await this._getProvider(tenantId);
|
|
79
174
|
|
|
80
175
|
let currentMessages = [...messages];
|
|
81
176
|
|
|
82
|
-
// Initial call
|
|
177
|
+
// Initial call - pass options to enable JSON mode, etc.
|
|
83
178
|
const initialResponse = await provider.chatCompletion(
|
|
84
179
|
currentMessages,
|
|
85
180
|
systemPrompt,
|
|
86
|
-
tools
|
|
181
|
+
tools,
|
|
182
|
+
options
|
|
87
183
|
);
|
|
88
184
|
|
|
89
|
-
let { content, tool_calls } = initialResponse;
|
|
185
|
+
let { content, tool_calls, parsedContent } = initialResponse;
|
|
90
186
|
|
|
91
187
|
// Tool execution loop
|
|
92
188
|
while (tool_calls) {
|
|
@@ -96,18 +192,29 @@ export class LLMService {
|
|
|
96
192
|
// Execute tools using the provider's helper (which formats results for that provider)
|
|
97
193
|
await provider.executeTools(tool_calls, currentMessages, tenantId, this.toolImplementations, this.env);
|
|
98
194
|
|
|
99
|
-
// Next call
|
|
195
|
+
// Next call - also pass options
|
|
100
196
|
const nextResponse = await provider.chatCompletion(
|
|
101
197
|
currentMessages,
|
|
102
198
|
systemPrompt,
|
|
103
|
-
tools
|
|
199
|
+
tools,
|
|
200
|
+
options
|
|
104
201
|
);
|
|
105
202
|
|
|
106
203
|
content = nextResponse.content;
|
|
107
204
|
tool_calls = nextResponse.tool_calls;
|
|
205
|
+
parsedContent = nextResponse.parsedContent; // Preserve parsedContent from final response
|
|
108
206
|
}
|
|
109
207
|
|
|
110
|
-
|
|
208
|
+
// Return both content and parsedContent (if available)
|
|
209
|
+
return { content, parsedContent, toolCalls: tool_calls };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate a video
|
|
214
|
+
*/
|
|
215
|
+
async videoGeneration(prompt, images, tenantId, modelName, systemPrompt, options = {}) {
|
|
216
|
+
const provider = await this._getProvider(tenantId);
|
|
217
|
+
return provider.videoGeneration(prompt, images, modelName, systemPrompt, options);
|
|
111
218
|
}
|
|
112
219
|
|
|
113
220
|
/**
|