@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.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