@almadar/llm 1.0.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/LICENSE +72 -0
- package/dist/chunk-KH4JNOLT.js +174 -0
- package/dist/chunk-KH4JNOLT.js.map +1 -0
- package/dist/chunk-MJS33AAS.js +234 -0
- package/dist/chunk-MJS33AAS.js.map +1 -0
- package/dist/chunk-PV3G5PJS.js +633 -0
- package/dist/chunk-PV3G5PJS.js.map +1 -0
- package/dist/chunk-WM7QVK2Z.js +192 -0
- package/dist/chunk-WM7QVK2Z.js.map +1 -0
- package/dist/client.d.ts +136 -0
- package/dist/client.js +39 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +477 -0
- package/dist/index.js.map +1 -0
- package/dist/json-parser.d.ts +43 -0
- package/dist/json-parser.js +15 -0
- package/dist/json-parser.js.map +1 -0
- package/dist/rate-limiter-9XAWfHwe.d.ts +98 -0
- package/dist/structured-output.d.ts +113 -0
- package/dist/structured-output.js +16 -0
- package/dist/structured-output.js.map +1 -0
- package/package.json +55 -0
- package/src/client.ts +967 -0
- package/src/continuation.ts +290 -0
- package/src/index.ts +87 -0
- package/src/json-parser.ts +273 -0
- package/src/rate-limiter.ts +237 -0
- package/src/structured-output.ts +330 -0
- package/src/token-tracker.ts +116 -0
- package/src/truncation-detector.ts +308 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncation Detector
|
|
3
|
+
*
|
|
4
|
+
* Utilities for detecting when LLM output has been truncated and
|
|
5
|
+
* extracting usable content from partial responses.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { LLMFinishReason } from './client.js';
|
|
11
|
+
import { autoCloseJson } from './json-parser.js';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export type TruncationReason =
|
|
18
|
+
| 'finish_reason'
|
|
19
|
+
| 'json_incomplete'
|
|
20
|
+
| 'bracket_mismatch'
|
|
21
|
+
| 'none';
|
|
22
|
+
|
|
23
|
+
export interface TruncationResult {
|
|
24
|
+
isTruncated: boolean;
|
|
25
|
+
reason: TruncationReason;
|
|
26
|
+
partialContent?: string;
|
|
27
|
+
lastCompleteElement?: unknown;
|
|
28
|
+
missingCloseBrackets?: number;
|
|
29
|
+
missingCloseBraces?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Main Detection Function
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export function detectTruncation(
|
|
37
|
+
response: string,
|
|
38
|
+
finishReason: LLMFinishReason,
|
|
39
|
+
): TruncationResult {
|
|
40
|
+
if (finishReason === 'length') {
|
|
41
|
+
const bracketInfo = countBrackets(response);
|
|
42
|
+
return {
|
|
43
|
+
isTruncated: true,
|
|
44
|
+
reason: 'finish_reason',
|
|
45
|
+
partialContent: response,
|
|
46
|
+
lastCompleteElement: findLastCompleteElement(response),
|
|
47
|
+
missingCloseBrackets: bracketInfo.missingCloseBrackets,
|
|
48
|
+
missingCloseBraces: bracketInfo.missingCloseBraces,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
JSON.parse(response);
|
|
54
|
+
return { isTruncated: false, reason: 'none' };
|
|
55
|
+
} catch {
|
|
56
|
+
// JSON is invalid, check if due to truncation
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (finishReason === 'stop' || finishReason === null) {
|
|
60
|
+
const trimmed = response.trim();
|
|
61
|
+
|
|
62
|
+
const isMidContent =
|
|
63
|
+
trimmed.endsWith(',') ||
|
|
64
|
+
trimmed.endsWith(':') ||
|
|
65
|
+
trimmed.endsWith('": ') ||
|
|
66
|
+
/:\s*$/.test(trimmed) ||
|
|
67
|
+
/,\s*$/.test(trimmed);
|
|
68
|
+
|
|
69
|
+
if (isMidContent) {
|
|
70
|
+
const bracketInfo = countBrackets(response);
|
|
71
|
+
return {
|
|
72
|
+
isTruncated: true,
|
|
73
|
+
reason: 'json_incomplete',
|
|
74
|
+
partialContent: response,
|
|
75
|
+
lastCompleteElement: findLastCompleteElement(response),
|
|
76
|
+
missingCloseBrackets: bracketInfo.missingCloseBrackets,
|
|
77
|
+
missingCloseBraces: bracketInfo.missingCloseBraces,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const closed = autoCloseJson(trimmed);
|
|
83
|
+
JSON.parse(closed);
|
|
84
|
+
return { isTruncated: false, reason: 'none' };
|
|
85
|
+
} catch {
|
|
86
|
+
return { isTruncated: false, reason: 'none' };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const bracketInfo = countBrackets(response);
|
|
91
|
+
if (
|
|
92
|
+
bracketInfo.missingCloseBrackets > 0 ||
|
|
93
|
+
bracketInfo.missingCloseBraces > 0
|
|
94
|
+
) {
|
|
95
|
+
return {
|
|
96
|
+
isTruncated: true,
|
|
97
|
+
reason: 'bracket_mismatch',
|
|
98
|
+
partialContent: response,
|
|
99
|
+
lastCompleteElement: findLastCompleteElement(response),
|
|
100
|
+
missingCloseBrackets: bracketInfo.missingCloseBrackets,
|
|
101
|
+
missingCloseBraces: bracketInfo.missingCloseBraces,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { isTruncated: false, reason: 'none' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Helper Functions
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
function countBrackets(json: string): {
|
|
113
|
+
openBrackets: number;
|
|
114
|
+
closeBrackets: number;
|
|
115
|
+
openBraces: number;
|
|
116
|
+
closeBraces: number;
|
|
117
|
+
missingCloseBrackets: number;
|
|
118
|
+
missingCloseBraces: number;
|
|
119
|
+
} {
|
|
120
|
+
let inString = false;
|
|
121
|
+
let escaped = false;
|
|
122
|
+
let openBrackets = 0;
|
|
123
|
+
let closeBrackets = 0;
|
|
124
|
+
let openBraces = 0;
|
|
125
|
+
let closeBraces = 0;
|
|
126
|
+
|
|
127
|
+
for (const char of json) {
|
|
128
|
+
if (escaped) {
|
|
129
|
+
escaped = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (char === '\\' && inString) {
|
|
133
|
+
escaped = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (char === '"') {
|
|
137
|
+
inString = !inString;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (inString) continue;
|
|
141
|
+
|
|
142
|
+
switch (char) {
|
|
143
|
+
case '[':
|
|
144
|
+
openBrackets++;
|
|
145
|
+
break;
|
|
146
|
+
case ']':
|
|
147
|
+
closeBrackets++;
|
|
148
|
+
break;
|
|
149
|
+
case '{':
|
|
150
|
+
openBraces++;
|
|
151
|
+
break;
|
|
152
|
+
case '}':
|
|
153
|
+
closeBraces++;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
openBrackets,
|
|
160
|
+
closeBrackets,
|
|
161
|
+
openBraces,
|
|
162
|
+
closeBraces,
|
|
163
|
+
missingCloseBrackets: Math.max(0, openBrackets - closeBrackets),
|
|
164
|
+
missingCloseBraces: Math.max(0, openBraces - closeBraces),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function findLastCompleteElement(json: string): unknown | null {
|
|
169
|
+
const autoClosed = autoCloseJson(json);
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(autoClosed);
|
|
172
|
+
} catch {
|
|
173
|
+
// Auto-close didn't work
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const trimmed = json.trim();
|
|
177
|
+
|
|
178
|
+
if (trimmed.startsWith('[')) {
|
|
179
|
+
const lastCompleteIndex = findLastCompleteArrayElement(trimmed);
|
|
180
|
+
if (lastCompleteIndex > 0) {
|
|
181
|
+
const subset = trimmed.substring(0, lastCompleteIndex) + ']';
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(subset);
|
|
184
|
+
} catch {
|
|
185
|
+
// Continue
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (trimmed.startsWith('{')) {
|
|
191
|
+
const closed = autoCloseJson(trimmed);
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(closed);
|
|
194
|
+
} catch {
|
|
195
|
+
const lastCompleteIndex = findLastCompleteObjectProperty(trimmed);
|
|
196
|
+
if (lastCompleteIndex > 0) {
|
|
197
|
+
const subset = trimmed.substring(0, lastCompleteIndex) + '}';
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(subset);
|
|
200
|
+
} catch {
|
|
201
|
+
// Give up
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function findLastCompleteArrayElement(json: string): number {
|
|
211
|
+
let depth = 0;
|
|
212
|
+
let inString = false;
|
|
213
|
+
let escaped = false;
|
|
214
|
+
let lastCompleteElementEnd = -1;
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < json.length; i++) {
|
|
217
|
+
const char = json[i];
|
|
218
|
+
|
|
219
|
+
if (escaped) {
|
|
220
|
+
escaped = false;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (char === '\\' && inString) {
|
|
224
|
+
escaped = true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (char === '"') {
|
|
228
|
+
inString = !inString;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (inString) continue;
|
|
232
|
+
|
|
233
|
+
if (char === '[' || char === '{') {
|
|
234
|
+
depth++;
|
|
235
|
+
} else if (char === ']' || char === '}') {
|
|
236
|
+
depth--;
|
|
237
|
+
if (depth === 1) {
|
|
238
|
+
lastCompleteElementEnd = i + 1;
|
|
239
|
+
}
|
|
240
|
+
} else if (char === ',' && depth === 1) {
|
|
241
|
+
lastCompleteElementEnd = i;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return lastCompleteElementEnd > 0 ? lastCompleteElementEnd : -1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function findLastCompleteObjectProperty(json: string): number {
|
|
249
|
+
let depth = 0;
|
|
250
|
+
let inString = false;
|
|
251
|
+
let escaped = false;
|
|
252
|
+
let lastCommaIndex = -1;
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < json.length; i++) {
|
|
255
|
+
const char = json[i];
|
|
256
|
+
|
|
257
|
+
if (escaped) {
|
|
258
|
+
escaped = false;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (char === '\\' && inString) {
|
|
262
|
+
escaped = true;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (char === '"') {
|
|
266
|
+
inString = !inString;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (inString) continue;
|
|
270
|
+
|
|
271
|
+
if (char === '[' || char === '{') {
|
|
272
|
+
depth++;
|
|
273
|
+
} else if (char === ']' || char === '}') {
|
|
274
|
+
depth--;
|
|
275
|
+
} else if (char === ',' && depth === 1) {
|
|
276
|
+
lastCommaIndex = i;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return lastCommaIndex > 0 ? lastCommaIndex : -1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function isLikelyTruncated(content: string): boolean {
|
|
284
|
+
const trimmed = content.trim();
|
|
285
|
+
if (!trimmed) return false;
|
|
286
|
+
|
|
287
|
+
const brackets = countBrackets(trimmed);
|
|
288
|
+
if (
|
|
289
|
+
brackets.missingCloseBrackets > 0 ||
|
|
290
|
+
brackets.missingCloseBraces > 0
|
|
291
|
+
) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const abruptEndings = [
|
|
296
|
+
/,\s*$/,
|
|
297
|
+
/:\s*$/,
|
|
298
|
+
/"\s*:\s*$/,
|
|
299
|
+
/\[\s*$/,
|
|
300
|
+
/{\s*$/,
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
for (const pattern of abruptEndings) {
|
|
304
|
+
if (pattern.test(trimmed)) return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return false;
|
|
308
|
+
}
|