@contentgrowth/llm-service 0.6.2 → 0.6.4
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
package/src/llm/json-utils.js
CHANGED
|
@@ -12,6 +12,19 @@ export function extractJsonFromResponse(text) {
|
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Helper to sanitize JSON strings with unescaped control characters
|
|
16
|
+
function sanitizeJsonString(str) {
|
|
17
|
+
// Find all JSON strings: "..."
|
|
18
|
+
// Handle escaped quotes \" and backslashes \\
|
|
19
|
+
return str.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
|
|
20
|
+
// Replace unescaped control characters inside the string
|
|
21
|
+
return match
|
|
22
|
+
.replace(/\n/g, "\\n")
|
|
23
|
+
.replace(/\r/g, "\\r")
|
|
24
|
+
.replace(/\t/g, "\\t");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
// Helper function to attempt JSON parsing with escape sequence normalization
|
|
16
29
|
function tryParseJson(jsonStr) {
|
|
17
30
|
// First, try to parse as-is
|
|
@@ -36,10 +49,16 @@ export function extractJsonFromResponse(text) {
|
|
|
36
49
|
} catch (e2) {
|
|
37
50
|
// Log this failure too
|
|
38
51
|
console.warn('Normalized JSON parse also failed:', e2.message);
|
|
39
|
-
|
|
52
|
+
// Fall through to sanitization
|
|
40
53
|
}
|
|
41
|
-
}
|
|
42
|
-
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Try sanitizing unescaped control characters (common LLM error)
|
|
57
|
+
try {
|
|
58
|
+
const sanitized = sanitizeJsonString(jsonStr);
|
|
59
|
+
return JSON.parse(sanitized);
|
|
60
|
+
} catch (e3) {
|
|
61
|
+
// If all attempts fail, throw the original error
|
|
43
62
|
throw e;
|
|
44
63
|
}
|
|
45
64
|
}
|
|
@@ -130,13 +130,30 @@ export class GeminiProvider extends BaseLLMProvider {
|
|
|
130
130
|
const response = result.response;
|
|
131
131
|
const toolCalls = response.functionCalls();
|
|
132
132
|
|
|
133
|
+
let textContent = '';
|
|
134
|
+
try {
|
|
135
|
+
textContent = response.text();
|
|
136
|
+
} catch (e) {
|
|
137
|
+
// response.text() throws if there is no text content (e.g. only tool calls)
|
|
138
|
+
// This is expected behavior for tool-only responses
|
|
139
|
+
}
|
|
140
|
+
// Validate that we have EITHER content OR tool calls
|
|
141
|
+
if (!textContent && (!toolCalls || toolCalls.length === 0)) {
|
|
142
|
+
console.error('[GeminiProvider] Model returned empty response (no text, no tool calls)');
|
|
143
|
+
console.error('[GeminiProvider] Last message:', JSON.stringify(lastMessage, null, 2));
|
|
144
|
+
throw new LLMServiceException(
|
|
145
|
+
'Model returned empty response. This usually means the prompt or schema is confusing the model.',
|
|
146
|
+
500
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
133
150
|
// Return with parsed JSON if applicable
|
|
134
151
|
return {
|
|
135
|
-
content:
|
|
152
|
+
content: textContent,
|
|
136
153
|
tool_calls: toolCalls ? toolCalls.map(fc => ({ type: 'function', function: fc })) : null,
|
|
137
154
|
_responseFormat: options.responseFormat,
|
|
138
155
|
...(options.responseFormat && this._shouldAutoParse(options) ? {
|
|
139
|
-
parsedContent: this._safeJsonParse(
|
|
156
|
+
parsedContent: this._safeJsonParse(textContent)
|
|
140
157
|
} : {})
|
|
141
158
|
};
|
|
142
159
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import { BaseLLMProvider } from './base-provider.js';
|
|
3
3
|
import { extractJsonFromResponse } from '../json-utils.js';
|
|
4
|
+
import { LLMServiceException } from '../../llm-service.js';
|
|
4
5
|
|
|
5
6
|
export class OpenAIProvider extends BaseLLMProvider {
|
|
6
7
|
constructor(config) {
|
|
@@ -58,6 +59,15 @@ export class OpenAIProvider extends BaseLLMProvider {
|
|
|
58
59
|
const response = await this.client.chat.completions.create(requestPayload);
|
|
59
60
|
const message = response.choices[0].message;
|
|
60
61
|
|
|
62
|
+
// Validate that we have EITHER content OR tool calls
|
|
63
|
+
if (!message.content && (!message.tool_calls || message.tool_calls.length === 0)) {
|
|
64
|
+
console.error('[OpenAIProvider] Model returned empty response (no text, no tool calls)');
|
|
65
|
+
throw new LLMServiceException(
|
|
66
|
+
'Model returned empty response. This usually means the prompt or schema is confusing the model.',
|
|
67
|
+
500
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
// Return with parsed JSON if applicable
|
|
62
72
|
return {
|
|
63
73
|
content: message.content,
|