@startsimpli/llm 0.1.1
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/dist/index.d.mts +353 -0
- package/dist/index.d.ts +353 -0
- package/dist/index.js +851 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +835 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +40 -0
- package/src/__tests__/json-extraction.test.ts +45 -0
- package/src/__tests__/llm-service.test.ts +108 -0
- package/src/__tests__/mock-provider.test.ts +91 -0
- package/src/__tests__/retry.test.ts +48 -0
- package/src/errors.ts +31 -0
- package/src/index.ts +37 -0
- package/src/providers/anthropic.ts +273 -0
- package/src/providers/interface.ts +44 -0
- package/src/providers/mock.ts +134 -0
- package/src/providers/openai.ts +273 -0
- package/src/service.ts +366 -0
- package/src/types.ts +107 -0
- package/src/utils/http-status.ts +25 -0
- package/src/utils/json-extraction.ts +60 -0
- package/src/utils/retry.ts +122 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var LLMProviderError = class extends Error {
|
|
7
|
+
constructor(message, code, retryable = false, statusCode, provider) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.retryable = retryable;
|
|
11
|
+
this.statusCode = statusCode;
|
|
12
|
+
this.provider = provider;
|
|
13
|
+
this.name = "LLMProviderError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/types.ts
|
|
18
|
+
var LLM_DEFAULTS = {
|
|
19
|
+
openaiModel: "gpt-4o-mini",
|
|
20
|
+
anthropicModel: "claude-sonnet-4-20250514",
|
|
21
|
+
/**
|
|
22
|
+
* Default sampling temperature.
|
|
23
|
+
* Note: base gpt-5 models (gpt-5, gpt-5.1, gpt-5.2) require temperature=1.0
|
|
24
|
+
* and the OpenAI provider enforces this automatically.
|
|
25
|
+
*/
|
|
26
|
+
temperature: 0.7,
|
|
27
|
+
maxTokens: 8192,
|
|
28
|
+
timeoutMs: 6e4
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/providers/openai.ts
|
|
32
|
+
var OPENAI_MODELS = [
|
|
33
|
+
"gpt-5.2",
|
|
34
|
+
"gpt-5.1",
|
|
35
|
+
"gpt-5",
|
|
36
|
+
"gpt-4o",
|
|
37
|
+
"gpt-4o-mini"
|
|
38
|
+
];
|
|
39
|
+
var MODEL_COSTS = {
|
|
40
|
+
"gpt-5.2": { input: 5e-3, output: 0.015 },
|
|
41
|
+
"gpt-5.1": { input: 5e-3, output: 0.015 },
|
|
42
|
+
"gpt-5": { input: 5e-3, output: 0.015 },
|
|
43
|
+
"gpt-4o": { input: 25e-4, output: 0.01 },
|
|
44
|
+
"gpt-4o-mini": { input: 15e-5, output: 6e-4 }
|
|
45
|
+
};
|
|
46
|
+
var DEFAULTS = {
|
|
47
|
+
baseUrl: "https://api.openai.com/v1",
|
|
48
|
+
model: LLM_DEFAULTS.openaiModel,
|
|
49
|
+
temperature: LLM_DEFAULTS.temperature,
|
|
50
|
+
maxTokens: LLM_DEFAULTS.maxTokens,
|
|
51
|
+
timeoutMs: LLM_DEFAULTS.timeoutMs
|
|
52
|
+
};
|
|
53
|
+
var OpenAIProvider = class {
|
|
54
|
+
constructor(config) {
|
|
55
|
+
this.name = "openai";
|
|
56
|
+
this.availableModels = OPENAI_MODELS;
|
|
57
|
+
this.config = {
|
|
58
|
+
apiKey: config.apiKey,
|
|
59
|
+
baseUrl: config.baseUrl ?? DEFAULTS.baseUrl,
|
|
60
|
+
defaultModel: config.defaultModel ?? DEFAULTS.model,
|
|
61
|
+
defaultTemperature: config.defaultTemperature ?? DEFAULTS.temperature,
|
|
62
|
+
defaultMaxTokens: config.defaultMaxTokens ?? DEFAULTS.maxTokens,
|
|
63
|
+
timeoutMs: config.timeoutMs ?? DEFAULTS.timeoutMs
|
|
64
|
+
};
|
|
65
|
+
this.defaultModel = this.config.defaultModel;
|
|
66
|
+
}
|
|
67
|
+
isAvailable() {
|
|
68
|
+
return Boolean(this.config.apiKey);
|
|
69
|
+
}
|
|
70
|
+
async generate(userPrompt, systemPrompt, options) {
|
|
71
|
+
if (!this.isAvailable()) {
|
|
72
|
+
throw new LLMProviderError(
|
|
73
|
+
"OpenAI API key not configured",
|
|
74
|
+
"AUTHENTICATION_ERROR",
|
|
75
|
+
false,
|
|
76
|
+
void 0,
|
|
77
|
+
this.name
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const model = options?.model ?? this.config.defaultModel;
|
|
81
|
+
const isBaseGpt5 = model === "gpt-5" || model === "gpt-5.1" || model === "gpt-5.2";
|
|
82
|
+
const isFineTuned = model.startsWith("ft:");
|
|
83
|
+
const temperature = isBaseGpt5 && !isFineTuned ? 1 : options?.temperature ?? this.config.defaultTemperature;
|
|
84
|
+
const maxTokens = options?.maxTokens ?? this.config.defaultMaxTokens;
|
|
85
|
+
const timeout = options?.timeout ?? this.config.timeoutMs;
|
|
86
|
+
const requestBody = {
|
|
87
|
+
model,
|
|
88
|
+
messages: [
|
|
89
|
+
{ role: "system", content: systemPrompt },
|
|
90
|
+
{ role: "user", content: userPrompt }
|
|
91
|
+
],
|
|
92
|
+
temperature,
|
|
93
|
+
max_tokens: maxTokens,
|
|
94
|
+
response_format: { type: "json_object" }
|
|
95
|
+
};
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(requestBody),
|
|
106
|
+
signal: controller.signal
|
|
107
|
+
});
|
|
108
|
+
clearTimeout(timeoutId);
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const errorData = await response.json().catch(() => ({
|
|
111
|
+
error: { message: "Unknown error" }
|
|
112
|
+
}));
|
|
113
|
+
throw this.handleApiError(response.status, errorData);
|
|
114
|
+
}
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
if (!data.choices?.[0]?.message?.content) {
|
|
117
|
+
throw new LLMProviderError(
|
|
118
|
+
"Empty response from OpenAI",
|
|
119
|
+
"UNKNOWN_ERROR",
|
|
120
|
+
true,
|
|
121
|
+
void 0,
|
|
122
|
+
this.name
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
content: data.choices[0].message.content,
|
|
127
|
+
usage: {
|
|
128
|
+
promptTokens: data.usage.prompt_tokens,
|
|
129
|
+
completionTokens: data.usage.completion_tokens,
|
|
130
|
+
totalTokens: data.usage.total_tokens
|
|
131
|
+
},
|
|
132
|
+
model: data.model,
|
|
133
|
+
responseId: data.id,
|
|
134
|
+
finishReason: data.choices[0].finish_reason
|
|
135
|
+
};
|
|
136
|
+
} catch (error) {
|
|
137
|
+
clearTimeout(timeoutId);
|
|
138
|
+
if (error instanceof LLMProviderError) {
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
142
|
+
throw new LLMProviderError(
|
|
143
|
+
`Request timed out after ${timeout}ms`,
|
|
144
|
+
"TIMEOUT",
|
|
145
|
+
true,
|
|
146
|
+
void 0,
|
|
147
|
+
this.name
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
throw new LLMProviderError(
|
|
151
|
+
`OpenAI request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
152
|
+
"UNKNOWN_ERROR",
|
|
153
|
+
true,
|
|
154
|
+
void 0,
|
|
155
|
+
this.name
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
estimateCost(promptTokens, completionTokens, model) {
|
|
160
|
+
const costs = MODEL_COSTS[model ?? this.config.defaultModel];
|
|
161
|
+
if (!costs) return void 0;
|
|
162
|
+
return promptTokens / 1e3 * costs.input + completionTokens / 1e3 * costs.output;
|
|
163
|
+
}
|
|
164
|
+
handleApiError(statusCode, errorResponse) {
|
|
165
|
+
const message = errorResponse.error?.message ?? "Unknown OpenAI error";
|
|
166
|
+
const errorType = errorResponse.error?.type ?? "";
|
|
167
|
+
const errorCode = errorResponse.error?.code ?? "";
|
|
168
|
+
let code;
|
|
169
|
+
let retryable = false;
|
|
170
|
+
if (statusCode === 401) {
|
|
171
|
+
code = "AUTHENTICATION_ERROR";
|
|
172
|
+
} else if (statusCode === 429) {
|
|
173
|
+
code = "RATE_LIMITED";
|
|
174
|
+
retryable = true;
|
|
175
|
+
} else if (statusCode === 400) {
|
|
176
|
+
if (errorCode === "context_length_exceeded") {
|
|
177
|
+
code = "CONTEXT_LENGTH_EXCEEDED";
|
|
178
|
+
} else if (errorType === "invalid_request_error") {
|
|
179
|
+
code = "INVALID_REQUEST";
|
|
180
|
+
} else {
|
|
181
|
+
code = "UNKNOWN_ERROR";
|
|
182
|
+
}
|
|
183
|
+
} else if (statusCode === 403) {
|
|
184
|
+
code = errorCode === "content_filter" ? "CONTENT_FILTERED" : "AUTHENTICATION_ERROR";
|
|
185
|
+
} else if (statusCode >= 500) {
|
|
186
|
+
code = "SERVICE_UNAVAILABLE";
|
|
187
|
+
retryable = true;
|
|
188
|
+
} else {
|
|
189
|
+
code = "UNKNOWN_ERROR";
|
|
190
|
+
}
|
|
191
|
+
return new LLMProviderError(message, code, retryable, statusCode, this.name);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
function createOpenAIProvider() {
|
|
195
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
196
|
+
if (!apiKey) return null;
|
|
197
|
+
return new OpenAIProvider({
|
|
198
|
+
apiKey,
|
|
199
|
+
baseUrl: process.env.OPENAI_BASE_URL,
|
|
200
|
+
defaultModel: process.env.OPENAI_MODEL
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/providers/anthropic.ts
|
|
205
|
+
var ANTHROPIC_MODELS = [
|
|
206
|
+
"claude-opus-4-6",
|
|
207
|
+
"claude-opus-4-20250514",
|
|
208
|
+
"claude-sonnet-4-6",
|
|
209
|
+
"claude-sonnet-4-20250514",
|
|
210
|
+
"claude-3-5-sonnet-20241022",
|
|
211
|
+
"claude-3-5-haiku-20241022",
|
|
212
|
+
"claude-3-opus-20240229",
|
|
213
|
+
"claude-3-sonnet-20240229",
|
|
214
|
+
"claude-3-haiku-20240307"
|
|
215
|
+
];
|
|
216
|
+
var MODEL_COSTS2 = {
|
|
217
|
+
"claude-opus-4-6": { input: 0.015, output: 0.075 },
|
|
218
|
+
"claude-opus-4-20250514": { input: 0.015, output: 0.075 },
|
|
219
|
+
"claude-sonnet-4-6": { input: 3e-3, output: 0.015 },
|
|
220
|
+
"claude-sonnet-4-20250514": { input: 3e-3, output: 0.015 },
|
|
221
|
+
"claude-3-5-sonnet-20241022": { input: 3e-3, output: 0.015 },
|
|
222
|
+
"claude-3-5-haiku-20241022": { input: 8e-4, output: 4e-3 },
|
|
223
|
+
"claude-3-opus-20240229": { input: 0.015, output: 0.075 },
|
|
224
|
+
"claude-3-sonnet-20240229": { input: 3e-3, output: 0.015 },
|
|
225
|
+
"claude-3-haiku-20240307": { input: 25e-5, output: 125e-5 }
|
|
226
|
+
};
|
|
227
|
+
var DEFAULTS2 = {
|
|
228
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
229
|
+
model: LLM_DEFAULTS.anthropicModel,
|
|
230
|
+
temperature: LLM_DEFAULTS.temperature,
|
|
231
|
+
maxTokens: LLM_DEFAULTS.maxTokens,
|
|
232
|
+
timeoutMs: LLM_DEFAULTS.timeoutMs,
|
|
233
|
+
apiVersion: "2023-06-01"
|
|
234
|
+
};
|
|
235
|
+
var AnthropicProvider = class {
|
|
236
|
+
constructor(config, apiVersion) {
|
|
237
|
+
this.name = "anthropic";
|
|
238
|
+
this.availableModels = ANTHROPIC_MODELS;
|
|
239
|
+
this.config = {
|
|
240
|
+
apiKey: config.apiKey,
|
|
241
|
+
baseUrl: config.baseUrl ?? DEFAULTS2.baseUrl,
|
|
242
|
+
defaultModel: config.defaultModel ?? DEFAULTS2.model,
|
|
243
|
+
defaultTemperature: config.defaultTemperature ?? DEFAULTS2.temperature,
|
|
244
|
+
defaultMaxTokens: config.defaultMaxTokens ?? DEFAULTS2.maxTokens,
|
|
245
|
+
timeoutMs: config.timeoutMs ?? DEFAULTS2.timeoutMs
|
|
246
|
+
};
|
|
247
|
+
this.defaultModel = this.config.defaultModel;
|
|
248
|
+
this.apiVersion = apiVersion ?? DEFAULTS2.apiVersion;
|
|
249
|
+
}
|
|
250
|
+
isAvailable() {
|
|
251
|
+
return Boolean(this.config.apiKey);
|
|
252
|
+
}
|
|
253
|
+
async generate(userPrompt, systemPrompt, options) {
|
|
254
|
+
if (!this.isAvailable()) {
|
|
255
|
+
throw new LLMProviderError(
|
|
256
|
+
"Anthropic API key not configured",
|
|
257
|
+
"AUTHENTICATION_ERROR",
|
|
258
|
+
false,
|
|
259
|
+
void 0,
|
|
260
|
+
this.name
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const model = options?.model ?? this.config.defaultModel;
|
|
264
|
+
const temperature = options?.temperature ?? this.config.defaultTemperature;
|
|
265
|
+
const maxTokens = options?.maxTokens ?? this.config.defaultMaxTokens;
|
|
266
|
+
const timeout = options?.timeout ?? this.config.timeoutMs;
|
|
267
|
+
const jsonWrappedPrompt = `${userPrompt}
|
|
268
|
+
|
|
269
|
+
Please respond with a valid JSON object only. Do not include any text before or after the JSON.`;
|
|
270
|
+
const requestBody = {
|
|
271
|
+
model,
|
|
272
|
+
max_tokens: maxTokens,
|
|
273
|
+
messages: [{ role: "user", content: jsonWrappedPrompt }],
|
|
274
|
+
system: systemPrompt,
|
|
275
|
+
temperature
|
|
276
|
+
};
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
279
|
+
try {
|
|
280
|
+
const response = await fetch(`${this.config.baseUrl}/messages`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: {
|
|
283
|
+
"Content-Type": "application/json",
|
|
284
|
+
"x-api-key": this.config.apiKey,
|
|
285
|
+
"anthropic-version": this.apiVersion
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify(requestBody),
|
|
288
|
+
signal: controller.signal
|
|
289
|
+
});
|
|
290
|
+
clearTimeout(timeoutId);
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
const errorData = await response.json().catch(() => ({
|
|
293
|
+
error: { type: "unknown", message: "Unknown error" }
|
|
294
|
+
}));
|
|
295
|
+
throw this.handleApiError(response.status, errorData);
|
|
296
|
+
}
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
const textContent = data.content.find((c) => c.type === "text");
|
|
299
|
+
if (!textContent?.text) {
|
|
300
|
+
throw new LLMProviderError(
|
|
301
|
+
"Empty response from Anthropic",
|
|
302
|
+
"UNKNOWN_ERROR",
|
|
303
|
+
true,
|
|
304
|
+
void 0,
|
|
305
|
+
this.name
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
content: textContent.text,
|
|
310
|
+
usage: {
|
|
311
|
+
promptTokens: data.usage.input_tokens,
|
|
312
|
+
completionTokens: data.usage.output_tokens,
|
|
313
|
+
totalTokens: data.usage.input_tokens + data.usage.output_tokens
|
|
314
|
+
},
|
|
315
|
+
model: data.model,
|
|
316
|
+
responseId: data.id,
|
|
317
|
+
finishReason: data.stop_reason ?? void 0
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
clearTimeout(timeoutId);
|
|
321
|
+
if (error instanceof LLMProviderError) {
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
325
|
+
throw new LLMProviderError(
|
|
326
|
+
`Request timed out after ${timeout}ms`,
|
|
327
|
+
"TIMEOUT",
|
|
328
|
+
true,
|
|
329
|
+
void 0,
|
|
330
|
+
this.name
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
throw new LLMProviderError(
|
|
334
|
+
`Anthropic request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
335
|
+
"UNKNOWN_ERROR",
|
|
336
|
+
true,
|
|
337
|
+
void 0,
|
|
338
|
+
this.name
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
estimateCost(promptTokens, completionTokens, model) {
|
|
343
|
+
const costs = MODEL_COSTS2[model ?? this.config.defaultModel];
|
|
344
|
+
if (!costs) return void 0;
|
|
345
|
+
return promptTokens / 1e3 * costs.input + completionTokens / 1e3 * costs.output;
|
|
346
|
+
}
|
|
347
|
+
handleApiError(statusCode, errorResponse) {
|
|
348
|
+
const message = errorResponse.error?.message ?? "Unknown Anthropic error";
|
|
349
|
+
const errorType = errorResponse.error?.type ?? "";
|
|
350
|
+
let code;
|
|
351
|
+
let retryable = false;
|
|
352
|
+
if (statusCode === 401) {
|
|
353
|
+
code = "AUTHENTICATION_ERROR";
|
|
354
|
+
} else if (statusCode === 429) {
|
|
355
|
+
code = "RATE_LIMITED";
|
|
356
|
+
retryable = true;
|
|
357
|
+
} else if (statusCode === 400) {
|
|
358
|
+
if (errorType === "invalid_request_error") {
|
|
359
|
+
code = message.includes("token") || message.includes("length") ? "CONTEXT_LENGTH_EXCEEDED" : "INVALID_REQUEST";
|
|
360
|
+
} else {
|
|
361
|
+
code = "UNKNOWN_ERROR";
|
|
362
|
+
}
|
|
363
|
+
} else if (statusCode === 403) {
|
|
364
|
+
code = "AUTHENTICATION_ERROR";
|
|
365
|
+
} else if (statusCode === 529 || statusCode >= 500) {
|
|
366
|
+
code = "SERVICE_UNAVAILABLE";
|
|
367
|
+
retryable = true;
|
|
368
|
+
} else {
|
|
369
|
+
code = "UNKNOWN_ERROR";
|
|
370
|
+
}
|
|
371
|
+
return new LLMProviderError(message, code, retryable, statusCode, this.name);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
function createAnthropicProvider() {
|
|
375
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
376
|
+
if (!apiKey) return null;
|
|
377
|
+
return new AnthropicProvider({
|
|
378
|
+
apiKey,
|
|
379
|
+
baseUrl: process.env.ANTHROPIC_BASE_URL,
|
|
380
|
+
defaultModel: process.env.ANTHROPIC_MODEL
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/providers/mock.ts
|
|
385
|
+
var mockBehavior = {};
|
|
386
|
+
function setMockBehavior(behavior) {
|
|
387
|
+
mockBehavior = behavior;
|
|
388
|
+
}
|
|
389
|
+
function resetMockBehavior() {
|
|
390
|
+
mockBehavior = {};
|
|
391
|
+
}
|
|
392
|
+
function getDefaultResponse() {
|
|
393
|
+
return JSON.stringify({ mock: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
394
|
+
}
|
|
395
|
+
function getInvalidResponse() {
|
|
396
|
+
return JSON.stringify({ incomplete: true });
|
|
397
|
+
}
|
|
398
|
+
var MockLLMProvider = class {
|
|
399
|
+
constructor() {
|
|
400
|
+
this.name = "mock";
|
|
401
|
+
this.availableModels = ["mock-model-1", "mock-model-2"];
|
|
402
|
+
this.defaultModel = "mock-model-1";
|
|
403
|
+
this.callCount = 0;
|
|
404
|
+
}
|
|
405
|
+
async generate(_userPrompt, _systemPrompt, _options) {
|
|
406
|
+
this.callCount++;
|
|
407
|
+
if (mockBehavior.error) {
|
|
408
|
+
throw new LLMProviderError(
|
|
409
|
+
mockBehavior.error.message,
|
|
410
|
+
mockBehavior.error.code,
|
|
411
|
+
mockBehavior.error.code === "RATE_LIMITED",
|
|
412
|
+
mockBehavior.error.code === "RATE_LIMITED" ? 429 : 503,
|
|
413
|
+
"mock"
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
if (mockBehavior.delay) {
|
|
417
|
+
await new Promise((resolve) => setTimeout(resolve, mockBehavior.delay));
|
|
418
|
+
}
|
|
419
|
+
if (mockBehavior.customResponse) {
|
|
420
|
+
return {
|
|
421
|
+
content: mockBehavior.customResponse,
|
|
422
|
+
usage: { promptTokens: 100, completionTokens: 500, totalTokens: 600 },
|
|
423
|
+
model: this.defaultModel,
|
|
424
|
+
responseId: `mock-${this.callCount}`,
|
|
425
|
+
finishReason: "stop"
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (mockBehavior.returnInvalidJson) {
|
|
429
|
+
return {
|
|
430
|
+
content: getInvalidResponse(),
|
|
431
|
+
usage: { promptTokens: 100, completionTokens: 200, totalTokens: 300 },
|
|
432
|
+
model: this.defaultModel,
|
|
433
|
+
responseId: `mock-${this.callCount}`,
|
|
434
|
+
finishReason: "stop"
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
content: getDefaultResponse(),
|
|
439
|
+
usage: { promptTokens: 150, completionTokens: 800, totalTokens: 950 },
|
|
440
|
+
model: this.defaultModel,
|
|
441
|
+
responseId: `mock-${this.callCount}`,
|
|
442
|
+
finishReason: "stop"
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
isAvailable() {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
estimateCost(promptTokens, completionTokens, _model) {
|
|
449
|
+
return (promptTokens + completionTokens) * 1e-5;
|
|
450
|
+
}
|
|
451
|
+
getCallCount() {
|
|
452
|
+
return this.callCount;
|
|
453
|
+
}
|
|
454
|
+
resetCallCount() {
|
|
455
|
+
this.callCount = 0;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// src/utils/json-extraction.ts
|
|
460
|
+
function extractJson(content) {
|
|
461
|
+
const trimmed = content.trim();
|
|
462
|
+
try {
|
|
463
|
+
const json = JSON.parse(trimmed);
|
|
464
|
+
return { success: true, json, raw: trimmed };
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
468
|
+
if (codeBlockMatch) {
|
|
469
|
+
try {
|
|
470
|
+
const json = JSON.parse(codeBlockMatch[1].trim());
|
|
471
|
+
return { success: true, json, raw: codeBlockMatch[1].trim() };
|
|
472
|
+
} catch (e) {
|
|
473
|
+
return {
|
|
474
|
+
success: false,
|
|
475
|
+
error: `JSON in code block is invalid: ${e instanceof Error ? e.message : "Parse error"}`,
|
|
476
|
+
raw: trimmed
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const firstBrace = trimmed.indexOf("{");
|
|
481
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
482
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
483
|
+
const jsonCandidate = trimmed.slice(firstBrace, lastBrace + 1);
|
|
484
|
+
try {
|
|
485
|
+
const json = JSON.parse(jsonCandidate);
|
|
486
|
+
return { success: true, json, raw: jsonCandidate };
|
|
487
|
+
} catch (e) {
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
error: `Found JSON-like content but it's invalid: ${e instanceof Error ? e.message : "Parse error"}`,
|
|
491
|
+
raw: trimmed
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
success: false,
|
|
497
|
+
error: "No valid JSON object found in response",
|
|
498
|
+
raw: trimmed
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/utils/http-status.ts
|
|
503
|
+
function llmErrorToHttpStatus(code) {
|
|
504
|
+
switch (code) {
|
|
505
|
+
case "RATE_LIMITED":
|
|
506
|
+
return 429;
|
|
507
|
+
case "CONTENT_FILTERED":
|
|
508
|
+
case "INVALID_REQUEST":
|
|
509
|
+
return 400;
|
|
510
|
+
case "AUTHENTICATION_ERROR":
|
|
511
|
+
case "SERVICE_UNAVAILABLE":
|
|
512
|
+
return 503;
|
|
513
|
+
case "TIMEOUT":
|
|
514
|
+
return 504;
|
|
515
|
+
case "CONTEXT_LENGTH_EXCEEDED":
|
|
516
|
+
return 422;
|
|
517
|
+
default:
|
|
518
|
+
return 500;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/utils/retry.ts
|
|
523
|
+
function buildSchemaErrorCorrectionPrompt(previousResponse, zodError) {
|
|
524
|
+
const errorDetails = zodError.errors.map((e) => {
|
|
525
|
+
const path = e.path.length > 0 ? e.path.join(".") : "root";
|
|
526
|
+
return `- Path "${path}": ${e.message}`;
|
|
527
|
+
}).join("\n");
|
|
528
|
+
return `Your previous response contained JSON that did not conform to the required schema.
|
|
529
|
+
|
|
530
|
+
## Previous Response
|
|
531
|
+
|
|
532
|
+
\`\`\`json
|
|
533
|
+
${previousResponse}
|
|
534
|
+
\`\`\`
|
|
535
|
+
|
|
536
|
+
## Validation Errors
|
|
537
|
+
|
|
538
|
+
The following schema validation errors were found:
|
|
539
|
+
|
|
540
|
+
${errorDetails}
|
|
541
|
+
|
|
542
|
+
## Instructions
|
|
543
|
+
|
|
544
|
+
Please generate a corrected JSON response that:
|
|
545
|
+
1. Fixes ALL the validation errors listed above
|
|
546
|
+
2. Maintains the same content and intent
|
|
547
|
+
3. Follows the exact schema structure required
|
|
548
|
+
4. Contains ONLY valid JSON with no text before or after
|
|
549
|
+
|
|
550
|
+
Common fixes needed:
|
|
551
|
+
- Ensure all required fields are present
|
|
552
|
+
- Check that enum values match the schema exactly
|
|
553
|
+
- Verify number fields contain numbers, not strings
|
|
554
|
+
- Ensure arrays are not empty where minimum length is 1
|
|
555
|
+
- Check that nested objects have all required properties
|
|
556
|
+
|
|
557
|
+
Respond with ONLY the corrected JSON object.`;
|
|
558
|
+
}
|
|
559
|
+
function buildParseErrorCorrectionPrompt(previousResponse, parseError) {
|
|
560
|
+
const truncatedResponse = previousResponse.length > 2e3 ? previousResponse.slice(0, 2e3) + "\n... [truncated]" : previousResponse;
|
|
561
|
+
return `Your previous response could not be parsed as valid JSON.
|
|
562
|
+
|
|
563
|
+
## Previous Response (excerpt)
|
|
564
|
+
|
|
565
|
+
\`\`\`
|
|
566
|
+
${truncatedResponse}
|
|
567
|
+
\`\`\`
|
|
568
|
+
|
|
569
|
+
## Parse Error
|
|
570
|
+
|
|
571
|
+
${parseError}
|
|
572
|
+
|
|
573
|
+
## Instructions
|
|
574
|
+
|
|
575
|
+
Please generate a valid JSON response that:
|
|
576
|
+
1. Contains ONLY the JSON object - no markdown, no explanations
|
|
577
|
+
2. Does not wrap the JSON in code blocks
|
|
578
|
+
3. Uses proper JSON syntax (double quotes for strings, no trailing commas)
|
|
579
|
+
4. Properly escapes any special characters in strings
|
|
580
|
+
|
|
581
|
+
Common JSON issues:
|
|
582
|
+
- Using single quotes instead of double quotes
|
|
583
|
+
- Trailing commas after the last item in arrays/objects
|
|
584
|
+
- Unescaped special characters in strings (\\n, \\t, \\", etc.)
|
|
585
|
+
- Missing commas between object properties
|
|
586
|
+
- Extra text before or after the JSON
|
|
587
|
+
|
|
588
|
+
Respond with ONLY the corrected JSON object.`;
|
|
589
|
+
}
|
|
590
|
+
function buildGenericRetryPrompt(originalPrompt, attemptNumber) {
|
|
591
|
+
return `The previous attempt to generate a response encountered issues. This is attempt ${attemptNumber}.
|
|
592
|
+
|
|
593
|
+
## Original Request
|
|
594
|
+
|
|
595
|
+
${originalPrompt}
|
|
596
|
+
|
|
597
|
+
## Instructions
|
|
598
|
+
|
|
599
|
+
Please generate a fresh response that:
|
|
600
|
+
1. Follows the exact JSON schema structure
|
|
601
|
+
2. Contains only valid, parseable JSON
|
|
602
|
+
3. Includes all required fields
|
|
603
|
+
4. Has accurate data throughout
|
|
604
|
+
|
|
605
|
+
Focus on generating a complete, valid response rather than a complex one. A simpler response that is fully valid is better than a complex one with errors.
|
|
606
|
+
|
|
607
|
+
Respond with ONLY the JSON object.`;
|
|
608
|
+
}
|
|
609
|
+
var DEFAULT_CONFIG = {
|
|
610
|
+
maxRetries: 2,
|
|
611
|
+
preferredProvider: "openai",
|
|
612
|
+
defaultModel: "",
|
|
613
|
+
defaultTemperature: LLM_DEFAULTS.temperature,
|
|
614
|
+
defaultMaxTokens: LLM_DEFAULTS.maxTokens
|
|
615
|
+
};
|
|
616
|
+
var LLMService = class {
|
|
617
|
+
constructor(config) {
|
|
618
|
+
this.providers = /* @__PURE__ */ new Map();
|
|
619
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
620
|
+
this.initializeProviders();
|
|
621
|
+
}
|
|
622
|
+
initializeProviders() {
|
|
623
|
+
if (process.env.LLM_PROVIDER === "mock") {
|
|
624
|
+
this.providers.set("mock", new MockLLMProvider());
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const openai = createOpenAIProvider();
|
|
628
|
+
if (openai) this.providers.set("openai", openai);
|
|
629
|
+
const anthropic = createAnthropicProvider();
|
|
630
|
+
if (anthropic) this.providers.set("anthropic", anthropic);
|
|
631
|
+
}
|
|
632
|
+
getProvider() {
|
|
633
|
+
const preferred = this.providers.get(this.config.preferredProvider);
|
|
634
|
+
if (preferred?.isAvailable()) return preferred;
|
|
635
|
+
for (const provider of this.providers.values()) {
|
|
636
|
+
if (provider.isAvailable()) return provider;
|
|
637
|
+
}
|
|
638
|
+
throw new LLMProviderError(
|
|
639
|
+
"No LLM providers available. Please configure OPENAI_API_KEY or ANTHROPIC_API_KEY.",
|
|
640
|
+
"AUTHENTICATION_ERROR",
|
|
641
|
+
false
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
/** Check if any LLM provider is available. */
|
|
645
|
+
isAvailable() {
|
|
646
|
+
for (const provider of this.providers.values()) {
|
|
647
|
+
if (provider.isAvailable()) return true;
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
/** Get list of available provider names. */
|
|
652
|
+
getAvailableProviders() {
|
|
653
|
+
const available = [];
|
|
654
|
+
for (const [name, provider] of this.providers.entries()) {
|
|
655
|
+
if (provider.isAvailable()) available.push(name);
|
|
656
|
+
}
|
|
657
|
+
return available;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Simple chat-style generation with no schema validation.
|
|
661
|
+
* Returns the raw LLM response.
|
|
662
|
+
*/
|
|
663
|
+
async chat(userPrompt, systemPrompt, options) {
|
|
664
|
+
const provider = this.getProvider();
|
|
665
|
+
const generateOptions = {
|
|
666
|
+
temperature: options?.temperature ?? this.config.defaultTemperature,
|
|
667
|
+
maxTokens: options?.maxTokens ?? this.config.defaultMaxTokens,
|
|
668
|
+
...options
|
|
669
|
+
};
|
|
670
|
+
return provider.generate(userPrompt, systemPrompt, generateOptions);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Generate structured output validated against a Zod schema.
|
|
674
|
+
*
|
|
675
|
+
* @param userPrompt - User's request
|
|
676
|
+
* @param systemPrompt - System instructions
|
|
677
|
+
* @param schema - Zod schema to validate the parsed JSON against
|
|
678
|
+
* @param options - Generation options
|
|
679
|
+
*/
|
|
680
|
+
async generate(userPrompt, systemPrompt, schema, options) {
|
|
681
|
+
const provider = this.getProvider();
|
|
682
|
+
const genOptions = {
|
|
683
|
+
model: this.config.defaultModel || void 0,
|
|
684
|
+
temperature: this.config.defaultTemperature,
|
|
685
|
+
maxTokens: this.config.defaultMaxTokens,
|
|
686
|
+
...options
|
|
687
|
+
};
|
|
688
|
+
let lastError;
|
|
689
|
+
let lastResponse;
|
|
690
|
+
let validationAttempts = 0;
|
|
691
|
+
try {
|
|
692
|
+
lastResponse = await provider.generate(userPrompt, systemPrompt, genOptions);
|
|
693
|
+
validationAttempts++;
|
|
694
|
+
const extraction = extractJson(lastResponse.content);
|
|
695
|
+
if (!extraction.success) {
|
|
696
|
+
const result = await this.retryWithCorrection(
|
|
697
|
+
provider,
|
|
698
|
+
userPrompt,
|
|
699
|
+
systemPrompt,
|
|
700
|
+
lastResponse,
|
|
701
|
+
"parse",
|
|
702
|
+
extraction.error,
|
|
703
|
+
schema,
|
|
704
|
+
genOptions,
|
|
705
|
+
validationAttempts
|
|
706
|
+
);
|
|
707
|
+
if (result.data) {
|
|
708
|
+
return this.buildSuccess(result.data, result.response ?? lastResponse, result.attempts);
|
|
709
|
+
}
|
|
710
|
+
lastError = new Error(extraction.error);
|
|
711
|
+
validationAttempts = result.attempts;
|
|
712
|
+
} else {
|
|
713
|
+
const validation = this.validate(extraction.json, schema);
|
|
714
|
+
if (validation.valid && validation.data) {
|
|
715
|
+
return this.buildSuccess(validation.data, lastResponse, validationAttempts);
|
|
716
|
+
}
|
|
717
|
+
const result = await this.retryWithCorrection(
|
|
718
|
+
provider,
|
|
719
|
+
userPrompt,
|
|
720
|
+
systemPrompt,
|
|
721
|
+
lastResponse,
|
|
722
|
+
"schema",
|
|
723
|
+
validation.zodError,
|
|
724
|
+
schema,
|
|
725
|
+
genOptions,
|
|
726
|
+
validationAttempts
|
|
727
|
+
);
|
|
728
|
+
if (result.data) {
|
|
729
|
+
return this.buildSuccess(result.data, result.response ?? lastResponse, result.attempts);
|
|
730
|
+
}
|
|
731
|
+
lastError = validation.zodError;
|
|
732
|
+
validationAttempts = result.attempts;
|
|
733
|
+
}
|
|
734
|
+
} catch (error) {
|
|
735
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
736
|
+
}
|
|
737
|
+
return this.buildError(lastError ?? new Error("Unknown error"), lastResponse, validationAttempts);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Register an additional provider instance (useful for custom/third-party providers).
|
|
741
|
+
*/
|
|
742
|
+
registerProvider(name, provider) {
|
|
743
|
+
this.providers.set(name, provider);
|
|
744
|
+
}
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
// Private helpers
|
|
747
|
+
// ---------------------------------------------------------------------------
|
|
748
|
+
async retryWithCorrection(provider, originalPrompt, systemPrompt, previousResponse, errorType, error, schema, options, currentAttempts) {
|
|
749
|
+
let attempts = currentAttempts;
|
|
750
|
+
for (let retry = 0; retry < this.config.maxRetries; retry++) {
|
|
751
|
+
attempts++;
|
|
752
|
+
try {
|
|
753
|
+
const correctionPrompt = errorType === "parse" ? buildParseErrorCorrectionPrompt(previousResponse.content, error) : buildSchemaErrorCorrectionPrompt(previousResponse.content, error);
|
|
754
|
+
const response = await provider.generate(correctionPrompt, systemPrompt, options);
|
|
755
|
+
const extraction = extractJson(response.content);
|
|
756
|
+
if (!extraction.success) {
|
|
757
|
+
if (retry === this.config.maxRetries - 1) {
|
|
758
|
+
const genericPrompt = buildGenericRetryPrompt(originalPrompt, attempts);
|
|
759
|
+
const finalResponse = await provider.generate(genericPrompt, systemPrompt, options);
|
|
760
|
+
attempts++;
|
|
761
|
+
const finalExtraction = extractJson(finalResponse.content);
|
|
762
|
+
if (finalExtraction.success) {
|
|
763
|
+
const validation2 = this.validate(finalExtraction.json, schema);
|
|
764
|
+
if (validation2.valid && validation2.data) {
|
|
765
|
+
return { data: validation2.data, response: finalResponse, attempts };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const validation = this.validate(extraction.json, schema);
|
|
772
|
+
if (validation.valid && validation.data) {
|
|
773
|
+
return { data: validation.data, response, attempts };
|
|
774
|
+
}
|
|
775
|
+
if (validation.zodError) {
|
|
776
|
+
previousResponse = response;
|
|
777
|
+
error = validation.zodError;
|
|
778
|
+
errorType = "schema";
|
|
779
|
+
}
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return { attempts };
|
|
784
|
+
}
|
|
785
|
+
validate(data, schema) {
|
|
786
|
+
const result = schema.safeParse(data);
|
|
787
|
+
if (result.success) {
|
|
788
|
+
return { valid: true, data: result.data };
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
valid: false,
|
|
792
|
+
zodError: result.error,
|
|
793
|
+
errors: result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`)
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
buildSuccess(data, response, validationAttempts) {
|
|
797
|
+
return {
|
|
798
|
+
success: true,
|
|
799
|
+
data,
|
|
800
|
+
metadata: {
|
|
801
|
+
model: response.model,
|
|
802
|
+
promptTokens: response.usage.promptTokens,
|
|
803
|
+
completionTokens: response.usage.completionTokens,
|
|
804
|
+
validationAttempts
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
buildError(error, response, validationAttempts) {
|
|
809
|
+
const isProviderError = error instanceof LLMProviderError;
|
|
810
|
+
let code = "GENERATION_FAILED";
|
|
811
|
+
let retryable = false;
|
|
812
|
+
if (isProviderError) {
|
|
813
|
+
code = error.code;
|
|
814
|
+
retryable = error.retryable;
|
|
815
|
+
} else if (error instanceof zod.ZodError) {
|
|
816
|
+
code = "VALIDATION_FAILED";
|
|
817
|
+
retryable = true;
|
|
818
|
+
}
|
|
819
|
+
const result = {
|
|
820
|
+
success: false,
|
|
821
|
+
error: { code, message: error.message, retryable }
|
|
822
|
+
};
|
|
823
|
+
if (response) {
|
|
824
|
+
result.metadata = {
|
|
825
|
+
model: response.model,
|
|
826
|
+
promptTokens: response.usage.promptTokens,
|
|
827
|
+
completionTokens: response.usage.completionTokens,
|
|
828
|
+
validationAttempts: validationAttempts ?? 1
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
exports.AnthropicProvider = AnthropicProvider;
|
|
836
|
+
exports.LLMProviderError = LLMProviderError;
|
|
837
|
+
exports.LLMService = LLMService;
|
|
838
|
+
exports.LLM_DEFAULTS = LLM_DEFAULTS;
|
|
839
|
+
exports.MockLLMProvider = MockLLMProvider;
|
|
840
|
+
exports.OpenAIProvider = OpenAIProvider;
|
|
841
|
+
exports.buildGenericRetryPrompt = buildGenericRetryPrompt;
|
|
842
|
+
exports.buildParseErrorCorrectionPrompt = buildParseErrorCorrectionPrompt;
|
|
843
|
+
exports.buildSchemaErrorCorrectionPrompt = buildSchemaErrorCorrectionPrompt;
|
|
844
|
+
exports.createAnthropicProvider = createAnthropicProvider;
|
|
845
|
+
exports.createOpenAIProvider = createOpenAIProvider;
|
|
846
|
+
exports.extractJson = extractJson;
|
|
847
|
+
exports.llmErrorToHttpStatus = llmErrorToHttpStatus;
|
|
848
|
+
exports.resetMockBehavior = resetMockBehavior;
|
|
849
|
+
exports.setMockBehavior = setMockBehavior;
|
|
850
|
+
//# sourceMappingURL=index.js.map
|
|
851
|
+
//# sourceMappingURL=index.js.map
|