@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/src/types.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ZodError } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Token usage statistics returned by every provider call.
|
|
5
|
+
*/
|
|
6
|
+
export interface TokenUsage {
|
|
7
|
+
promptTokens: number;
|
|
8
|
+
completionTokens: number;
|
|
9
|
+
totalTokens: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Raw response envelope from an LLM provider.
|
|
14
|
+
*/
|
|
15
|
+
export interface LLMResponse {
|
|
16
|
+
/** The raw text content from the LLM */
|
|
17
|
+
content: string;
|
|
18
|
+
/** Token usage statistics */
|
|
19
|
+
usage: TokenUsage;
|
|
20
|
+
/** Model identifier used for generation */
|
|
21
|
+
model: string;
|
|
22
|
+
/** Response ID for debugging/logging */
|
|
23
|
+
responseId?: string;
|
|
24
|
+
/** Finish reason (e.g., 'stop', 'length', 'content_filter') */
|
|
25
|
+
finishReason?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options passed through to the provider for a single generation call.
|
|
30
|
+
*/
|
|
31
|
+
export interface GenerateOptions {
|
|
32
|
+
/** Model to use (provider-specific) */
|
|
33
|
+
model?: string;
|
|
34
|
+
/** Temperature for response randomness (0-1) */
|
|
35
|
+
temperature?: number;
|
|
36
|
+
/** Maximum tokens in response */
|
|
37
|
+
maxTokens?: number;
|
|
38
|
+
/** Request timeout in milliseconds */
|
|
39
|
+
timeout?: number;
|
|
40
|
+
/** Arbitrary extra data the caller wants to thread through (e.g. domain preferences) */
|
|
41
|
+
extra?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Provider configuration — everything needed to construct a provider instance.
|
|
46
|
+
*/
|
|
47
|
+
export interface ProviderConfig {
|
|
48
|
+
apiKey: string;
|
|
49
|
+
baseUrl?: string;
|
|
50
|
+
defaultModel?: string;
|
|
51
|
+
defaultTemperature?: number;
|
|
52
|
+
defaultMaxTokens?: number;
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Result of attempting to extract JSON from an LLM response string.
|
|
58
|
+
*/
|
|
59
|
+
export interface JsonExtractionResult {
|
|
60
|
+
success: boolean;
|
|
61
|
+
json?: unknown;
|
|
62
|
+
error?: string;
|
|
63
|
+
raw: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generic validation result — parameterized over the domain data type.
|
|
68
|
+
*/
|
|
69
|
+
export interface ValidationResult<T> {
|
|
70
|
+
valid: boolean;
|
|
71
|
+
data?: T;
|
|
72
|
+
zodError?: ZodError;
|
|
73
|
+
errors?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Configuration for the generic LLMService.
|
|
78
|
+
*/
|
|
79
|
+
export interface LLMServiceConfig {
|
|
80
|
+
/** Maximum number of retry attempts for validation failures */
|
|
81
|
+
maxRetries?: number;
|
|
82
|
+
/** Preferred provider (will fallback to available if not configured) */
|
|
83
|
+
preferredProvider?: 'openai' | 'anthropic' | 'mock';
|
|
84
|
+
/** Default model override */
|
|
85
|
+
defaultModel?: string;
|
|
86
|
+
/** Default temperature */
|
|
87
|
+
defaultTemperature?: number;
|
|
88
|
+
/** Default max tokens */
|
|
89
|
+
defaultMaxTokens?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default LLM configuration values.
|
|
94
|
+
* Apps can override these via LLMServiceConfig or environment variables.
|
|
95
|
+
*/
|
|
96
|
+
export const LLM_DEFAULTS = {
|
|
97
|
+
openaiModel: 'gpt-4o-mini',
|
|
98
|
+
anthropicModel: 'claude-sonnet-4-20250514',
|
|
99
|
+
/**
|
|
100
|
+
* Default sampling temperature.
|
|
101
|
+
* Note: base gpt-5 models (gpt-5, gpt-5.1, gpt-5.2) require temperature=1.0
|
|
102
|
+
* and the OpenAI provider enforces this automatically.
|
|
103
|
+
*/
|
|
104
|
+
temperature: 0.7,
|
|
105
|
+
maxTokens: 8192,
|
|
106
|
+
timeoutMs: 60000,
|
|
107
|
+
} as const;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LLMErrorCode } from '../errors'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maps an LLM error code to an HTTP status code.
|
|
5
|
+
* Use this in Next.js API routes to return appropriate HTTP responses
|
|
6
|
+
* when an LLM generation call fails.
|
|
7
|
+
*/
|
|
8
|
+
export function llmErrorToHttpStatus(code: LLMErrorCode | string): number {
|
|
9
|
+
switch (code) {
|
|
10
|
+
case 'RATE_LIMITED':
|
|
11
|
+
return 429
|
|
12
|
+
case 'CONTENT_FILTERED':
|
|
13
|
+
case 'INVALID_REQUEST':
|
|
14
|
+
return 400
|
|
15
|
+
case 'AUTHENTICATION_ERROR':
|
|
16
|
+
case 'SERVICE_UNAVAILABLE':
|
|
17
|
+
return 503
|
|
18
|
+
case 'TIMEOUT':
|
|
19
|
+
return 504
|
|
20
|
+
case 'CONTEXT_LENGTH_EXCEEDED':
|
|
21
|
+
return 422
|
|
22
|
+
default:
|
|
23
|
+
return 500
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { JsonExtractionResult } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract a JSON object from LLM response content.
|
|
5
|
+
*
|
|
6
|
+
* Tries three strategies in order:
|
|
7
|
+
* 1. Direct JSON.parse of the trimmed content
|
|
8
|
+
* 2. Extract from markdown code blocks (```json ... ```)
|
|
9
|
+
* 3. Find JSON object boundaries ({ ... })
|
|
10
|
+
*/
|
|
11
|
+
export function extractJson(content: string): JsonExtractionResult {
|
|
12
|
+
const trimmed = content.trim();
|
|
13
|
+
|
|
14
|
+
// Strategy 1: direct parse
|
|
15
|
+
try {
|
|
16
|
+
const json = JSON.parse(trimmed);
|
|
17
|
+
return { success: true, json, raw: trimmed };
|
|
18
|
+
} catch {
|
|
19
|
+
// continue to extraction strategies
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Strategy 2: markdown code blocks
|
|
23
|
+
const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
24
|
+
if (codeBlockMatch) {
|
|
25
|
+
try {
|
|
26
|
+
const json = JSON.parse(codeBlockMatch[1].trim());
|
|
27
|
+
return { success: true, json, raw: codeBlockMatch[1].trim() };
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: `JSON in code block is invalid: ${e instanceof Error ? e.message : 'Parse error'}`,
|
|
32
|
+
raw: trimmed,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Strategy 3: find JSON object boundaries
|
|
38
|
+
const firstBrace = trimmed.indexOf('{');
|
|
39
|
+
const lastBrace = trimmed.lastIndexOf('}');
|
|
40
|
+
|
|
41
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
42
|
+
const jsonCandidate = trimmed.slice(firstBrace, lastBrace + 1);
|
|
43
|
+
try {
|
|
44
|
+
const json = JSON.parse(jsonCandidate);
|
|
45
|
+
return { success: true, json, raw: jsonCandidate };
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: `Found JSON-like content but it's invalid: ${e instanceof Error ? e.message : 'Parse error'}`,
|
|
50
|
+
raw: trimmed,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: 'No valid JSON object found in response',
|
|
58
|
+
raw: trimmed,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ZodError } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a correction prompt for schema validation errors.
|
|
5
|
+
*
|
|
6
|
+
* Domain-agnostic: accepts the raw previous response and a ZodError,
|
|
7
|
+
* formats them into a prompt asking the LLM to fix specific field errors.
|
|
8
|
+
*/
|
|
9
|
+
export function buildSchemaErrorCorrectionPrompt(
|
|
10
|
+
previousResponse: string,
|
|
11
|
+
zodError: ZodError
|
|
12
|
+
): string {
|
|
13
|
+
const errorDetails = zodError.errors
|
|
14
|
+
.map((e) => {
|
|
15
|
+
const path = e.path.length > 0 ? e.path.join('.') : 'root';
|
|
16
|
+
return `- Path "${path}": ${e.message}`;
|
|
17
|
+
})
|
|
18
|
+
.join('\n');
|
|
19
|
+
|
|
20
|
+
return `Your previous response contained JSON that did not conform to the required schema.
|
|
21
|
+
|
|
22
|
+
## Previous Response
|
|
23
|
+
|
|
24
|
+
\`\`\`json
|
|
25
|
+
${previousResponse}
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
## Validation Errors
|
|
29
|
+
|
|
30
|
+
The following schema validation errors were found:
|
|
31
|
+
|
|
32
|
+
${errorDetails}
|
|
33
|
+
|
|
34
|
+
## Instructions
|
|
35
|
+
|
|
36
|
+
Please generate a corrected JSON response that:
|
|
37
|
+
1. Fixes ALL the validation errors listed above
|
|
38
|
+
2. Maintains the same content and intent
|
|
39
|
+
3. Follows the exact schema structure required
|
|
40
|
+
4. Contains ONLY valid JSON with no text before or after
|
|
41
|
+
|
|
42
|
+
Common fixes needed:
|
|
43
|
+
- Ensure all required fields are present
|
|
44
|
+
- Check that enum values match the schema exactly
|
|
45
|
+
- Verify number fields contain numbers, not strings
|
|
46
|
+
- Ensure arrays are not empty where minimum length is 1
|
|
47
|
+
- Check that nested objects have all required properties
|
|
48
|
+
|
|
49
|
+
Respond with ONLY the corrected JSON object.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build a correction prompt for JSON parsing errors.
|
|
54
|
+
*
|
|
55
|
+
* Domain-agnostic: accepts the raw response and a parse error message.
|
|
56
|
+
*/
|
|
57
|
+
export function buildParseErrorCorrectionPrompt(
|
|
58
|
+
previousResponse: string,
|
|
59
|
+
parseError: string
|
|
60
|
+
): string {
|
|
61
|
+
const truncatedResponse =
|
|
62
|
+
previousResponse.length > 2000
|
|
63
|
+
? previousResponse.slice(0, 2000) + '\n... [truncated]'
|
|
64
|
+
: previousResponse;
|
|
65
|
+
|
|
66
|
+
return `Your previous response could not be parsed as valid JSON.
|
|
67
|
+
|
|
68
|
+
## Previous Response (excerpt)
|
|
69
|
+
|
|
70
|
+
\`\`\`
|
|
71
|
+
${truncatedResponse}
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
## Parse Error
|
|
75
|
+
|
|
76
|
+
${parseError}
|
|
77
|
+
|
|
78
|
+
## Instructions
|
|
79
|
+
|
|
80
|
+
Please generate a valid JSON response that:
|
|
81
|
+
1. Contains ONLY the JSON object - no markdown, no explanations
|
|
82
|
+
2. Does not wrap the JSON in code blocks
|
|
83
|
+
3. Uses proper JSON syntax (double quotes for strings, no trailing commas)
|
|
84
|
+
4. Properly escapes any special characters in strings
|
|
85
|
+
|
|
86
|
+
Common JSON issues:
|
|
87
|
+
- Using single quotes instead of double quotes
|
|
88
|
+
- Trailing commas after the last item in arrays/objects
|
|
89
|
+
- Unescaped special characters in strings (\\n, \\t, \\\", etc.)
|
|
90
|
+
- Missing commas between object properties
|
|
91
|
+
- Extra text before or after the JSON
|
|
92
|
+
|
|
93
|
+
Respond with ONLY the corrected JSON object.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build a generic retry prompt when all corrections have failed.
|
|
98
|
+
*
|
|
99
|
+
* Domain-agnostic: just re-states the original request and asks for a simpler output.
|
|
100
|
+
*/
|
|
101
|
+
export function buildGenericRetryPrompt(
|
|
102
|
+
originalPrompt: string,
|
|
103
|
+
attemptNumber: number
|
|
104
|
+
): string {
|
|
105
|
+
return `The previous attempt to generate a response encountered issues. This is attempt ${attemptNumber}.
|
|
106
|
+
|
|
107
|
+
## Original Request
|
|
108
|
+
|
|
109
|
+
${originalPrompt}
|
|
110
|
+
|
|
111
|
+
## Instructions
|
|
112
|
+
|
|
113
|
+
Please generate a fresh response that:
|
|
114
|
+
1. Follows the exact JSON schema structure
|
|
115
|
+
2. Contains only valid, parseable JSON
|
|
116
|
+
3. Includes all required fields
|
|
117
|
+
4. Has accurate data throughout
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
Respond with ONLY the JSON object.`;
|
|
122
|
+
}
|