@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/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
+ }