@sprqvntrs/llm 3.13.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 +21 -0
- package/README.md +595 -0
- package/index.ts +53 -0
- package/package.json +56 -0
- package/src/clients/anthropic-client.ts +636 -0
- package/src/clients/openai-client.ts +548 -0
- package/src/clients/openrouter-client.ts +802 -0
- package/src/helpers.ts +185 -0
- package/src/llm.ts +107 -0
- package/src/model-types.ts +61 -0
- package/src/models.ts +98 -0
- package/src/pricing-data.json +1651 -0
- package/src/pricing.ts +36 -0
- package/src/types/client-interface.ts +222 -0
- package/src/utils/debug.ts +120 -0
- package/src/utils/errors.ts +508 -0
- package/src/utils/normalize-null-strings.ts +103 -0
- package/src/utils/resolve-refs.ts +91 -0
- package/src/utils/strip-json-artifacts.ts +98 -0
package/src/pricing.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pricingData from './pricing-data.json';
|
|
2
|
+
import type { LlmUsageCost } from './types/client-interface';
|
|
3
|
+
|
|
4
|
+
export interface ModelPricing {
|
|
5
|
+
inputPerMillion: number;
|
|
6
|
+
outputPerMillion: number;
|
|
7
|
+
cachedInputPerMillion?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const MODEL_PRICING: Record<string, ModelPricing> = pricingData.models;
|
|
11
|
+
|
|
12
|
+
export function calculateUsageCost(
|
|
13
|
+
model: string,
|
|
14
|
+
promptTokens: number,
|
|
15
|
+
completionTokens: number,
|
|
16
|
+
cachedTokens?: number,
|
|
17
|
+
): LlmUsageCost | null {
|
|
18
|
+
const pricing = MODEL_PRICING[model];
|
|
19
|
+
if (!pricing) return null;
|
|
20
|
+
|
|
21
|
+
const cached = cachedTokens ?? 0;
|
|
22
|
+
const nonCachedInput = promptTokens - cached;
|
|
23
|
+
|
|
24
|
+
const inputCost = (nonCachedInput / 1_000_000) * pricing.inputPerMillion;
|
|
25
|
+
const outputCost = (completionTokens / 1_000_000) * pricing.outputPerMillion;
|
|
26
|
+
const cachedInputCost = pricing.cachedInputPerMillion
|
|
27
|
+
? (cached / 1_000_000) * pricing.cachedInputPerMillion
|
|
28
|
+
: 0;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
input: inputCost,
|
|
32
|
+
output: outputCost,
|
|
33
|
+
cachedInput: cachedInputCost,
|
|
34
|
+
total: inputCost + cachedInputCost + outputCost,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalized reasoning effort levels shared across providers.
|
|
5
|
+
*
|
|
6
|
+
* `'none'` means "turn reasoning off" — providers that support it receive an
|
|
7
|
+
* explicit off signal; providers whose reasoning is opt-in simply stay off.
|
|
8
|
+
* Leaving the value `undefined` means "don't send any reasoning preference"
|
|
9
|
+
* and preserves each provider's own default behaviour.
|
|
10
|
+
*/
|
|
11
|
+
export type ReasoningEffortLevel = 'none' | 'low' | 'medium' | 'high';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base configuration for all LLM clients
|
|
15
|
+
*/
|
|
16
|
+
export interface BaseLlmClientConfig {
|
|
17
|
+
/**
|
|
18
|
+
* API key for the provider
|
|
19
|
+
*/
|
|
20
|
+
apiKey: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The model to use
|
|
24
|
+
*/
|
|
25
|
+
model: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Debug logging configuration
|
|
29
|
+
* - If true: always log debug messages
|
|
30
|
+
* - If false: never log debug messages
|
|
31
|
+
* - If undefined: auto-detect based on NODE_ENV (enabled in development)
|
|
32
|
+
*/
|
|
33
|
+
debug?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Request timeout in milliseconds
|
|
37
|
+
* - Default: 120000 (120 seconds / 2 minutes)
|
|
38
|
+
* - Maximum recommended: 600000 (10 minutes)
|
|
39
|
+
* - Set to 0 to disable timeout (not recommended)
|
|
40
|
+
*/
|
|
41
|
+
timeout?: number;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Maximum number of retries for failed requests
|
|
45
|
+
* - Default: 2
|
|
46
|
+
* - Set to 0 to disable retries
|
|
47
|
+
*/
|
|
48
|
+
maxRetries?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Options for batch processing
|
|
53
|
+
*/
|
|
54
|
+
export interface BatchProcessOptions<T, R> {
|
|
55
|
+
/**
|
|
56
|
+
* The items to process
|
|
57
|
+
*/
|
|
58
|
+
items: T[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The function to process each batch
|
|
62
|
+
*/
|
|
63
|
+
processFn: (batch: T[]) => Promise<R[]>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The size of each batch (default: 5)
|
|
67
|
+
*/
|
|
68
|
+
batchSize?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Streaming chunk from an LLM response
|
|
73
|
+
*/
|
|
74
|
+
export interface StreamChunk {
|
|
75
|
+
/**
|
|
76
|
+
* The text content of this chunk
|
|
77
|
+
*/
|
|
78
|
+
text: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether this is the final chunk
|
|
82
|
+
*/
|
|
83
|
+
isComplete: boolean;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Accumulated text so far (including this chunk)
|
|
87
|
+
*/
|
|
88
|
+
accumulatedText?: string;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Optional usage information (only present in final chunk for some providers)
|
|
92
|
+
*/
|
|
93
|
+
usage?: {
|
|
94
|
+
promptTokens?: number;
|
|
95
|
+
completionTokens?: number;
|
|
96
|
+
totalTokens?: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The finish reason from the API (only present in final chunk)
|
|
101
|
+
* Common values: 'stop' (normal), 'length' (truncated), 'content_filter'
|
|
102
|
+
*/
|
|
103
|
+
finishReason?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Cost breakdown for an LLM API call.
|
|
108
|
+
*/
|
|
109
|
+
export interface LlmUsageCost {
|
|
110
|
+
/** Cost for non-cached input tokens */
|
|
111
|
+
input: number;
|
|
112
|
+
/** Cost for output/completion tokens */
|
|
113
|
+
output: number;
|
|
114
|
+
/** Cost for cached input tokens (reduced rate) */
|
|
115
|
+
cachedInput: number;
|
|
116
|
+
/** Total cost: input + cachedInput + output */
|
|
117
|
+
total: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Token usage data from an LLM API call.
|
|
122
|
+
*/
|
|
123
|
+
export interface LlmTokenUsage {
|
|
124
|
+
promptTokens: number;
|
|
125
|
+
completionTokens: number;
|
|
126
|
+
totalTokens: number;
|
|
127
|
+
cachedTokens?: number;
|
|
128
|
+
/** The model ID used for this call */
|
|
129
|
+
model: string;
|
|
130
|
+
/** Calculated cost breakdown, or null if model not in pricing table */
|
|
131
|
+
cost: LlmUsageCost | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Unified interface that all LLM clients must implement
|
|
136
|
+
* This ensures consistent API across different providers
|
|
137
|
+
*/
|
|
138
|
+
export interface LlmClientInterface {
|
|
139
|
+
/**
|
|
140
|
+
* Token usage from the most recent createStructuredResponse call.
|
|
141
|
+
* Reset to null before each call, populated after successful completion.
|
|
142
|
+
*/
|
|
143
|
+
readonly lastUsage: LlmTokenUsage | null;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validates that the client is properly configured
|
|
147
|
+
* @returns true if valid, throws an error with details if not
|
|
148
|
+
*/
|
|
149
|
+
validateConfiguration(): boolean;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Creates a raw response from the provider's API and returns the text content
|
|
153
|
+
* This is a simple, stateless method that extracts text automatically
|
|
154
|
+
*
|
|
155
|
+
* @param prompt The prompt to send to the model
|
|
156
|
+
* @param options Optional configuration
|
|
157
|
+
* @param options.timeout Request timeout in milliseconds (overrides client default)
|
|
158
|
+
* @returns The text content as a string
|
|
159
|
+
*/
|
|
160
|
+
createResponse(prompt: string, options?: { timeout?: number }): Promise<string>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Creates a raw response from the provider's API and returns the full response object
|
|
164
|
+
* Use this when you need access to metadata like usage stats, finish reason, etc.
|
|
165
|
+
*
|
|
166
|
+
* @param prompt The prompt to send to the model
|
|
167
|
+
* @param options Optional configuration
|
|
168
|
+
* @param options.timeout Request timeout in milliseconds (overrides client default)
|
|
169
|
+
* @returns The raw response object from the provider
|
|
170
|
+
*/
|
|
171
|
+
createRawResponse(prompt: string, options?: { timeout?: number }): Promise<unknown>;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Creates a streaming response from the provider's API
|
|
175
|
+
* Returns an async iterator that yields chunks of text as they arrive
|
|
176
|
+
*
|
|
177
|
+
* @param prompt The prompt to send to the model
|
|
178
|
+
* @param options Optional configuration
|
|
179
|
+
* @param options.timeout Request timeout in milliseconds (overrides client default)
|
|
180
|
+
* @returns An async iterable of stream chunks
|
|
181
|
+
*/
|
|
182
|
+
createStreamingResponse(prompt: string, options?: { timeout?: number }): AsyncIterable<StreamChunk>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Creates a structured response using the provider's API with Zod schema validation
|
|
186
|
+
* Includes built-in retry logic, execution time tracking, and error handling
|
|
187
|
+
*
|
|
188
|
+
* @param options.prompt The prompt to send to the model
|
|
189
|
+
* @param options.schema The Zod schema to validate the response against
|
|
190
|
+
* @param options.formatGuidance Optional guidance for formatting the response
|
|
191
|
+
* @param options.reasoningEffort Normalized reasoning effort level ('none' | 'low' | 'medium' | 'high').
|
|
192
|
+
* Omit to leave the provider default untouched; pass 'none' to turn reasoning off.
|
|
193
|
+
* @param options.maxAttempts Maximum number of retry attempts (default: 3)
|
|
194
|
+
* @param options.logExecutionTime Whether to log execution time warnings (default: false)
|
|
195
|
+
* @param options.responseInstructions Additional instructions to append to the prompt (deprecated, use formatGuidance)
|
|
196
|
+
* @param options.useWebSearch Whether to enable web search for this request (default: false)
|
|
197
|
+
* @param options.stream Whether to use streaming for the generation phase (default: true)
|
|
198
|
+
* @param options.timeout Request timeout in milliseconds (overrides client default)
|
|
199
|
+
*/
|
|
200
|
+
createStructuredResponse<T extends z.ZodType>(options: {
|
|
201
|
+
prompt: string;
|
|
202
|
+
schema: T;
|
|
203
|
+
formatGuidance?: string;
|
|
204
|
+
reasoningEffort?: ReasoningEffortLevel;
|
|
205
|
+
maxAttempts?: number;
|
|
206
|
+
logExecutionTime?: boolean;
|
|
207
|
+
responseInstructions?: string;
|
|
208
|
+
useWebSearch?: boolean;
|
|
209
|
+
stream?: boolean;
|
|
210
|
+
timeout?: number;
|
|
211
|
+
}): Promise<z.infer<T>>;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Process a batch of items with parallel processing
|
|
215
|
+
*/
|
|
216
|
+
processBatchWithLLM<T, R>(options: BatchProcessOptions<T, R>): Promise<R[]>;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Generates an embedding vector for the provided text
|
|
220
|
+
*/
|
|
221
|
+
generateEmbedding(value: string): Promise<number[]>;
|
|
222
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug logging utility for LLM clients
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface DebugConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Enable debug logging
|
|
8
|
+
* - If true: always log debug messages
|
|
9
|
+
* - If false: never log debug messages
|
|
10
|
+
* - If undefined: auto-detect based on NODE_ENV (enabled in development)
|
|
11
|
+
*/
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DebugLogger {
|
|
16
|
+
private enabled: boolean;
|
|
17
|
+
private clientName: string;
|
|
18
|
+
|
|
19
|
+
constructor(clientName: string, config?: DebugConfig) {
|
|
20
|
+
this.clientName = clientName;
|
|
21
|
+
|
|
22
|
+
// Auto-detect debug mode based on NODE_ENV if not explicitly set
|
|
23
|
+
if (config?.enabled !== undefined) {
|
|
24
|
+
this.enabled = config.enabled;
|
|
25
|
+
} else {
|
|
26
|
+
this.enabled = process.env.NODE_ENV === 'development';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Log a debug message (only in debug mode)
|
|
32
|
+
*/
|
|
33
|
+
log(message: string, data?: Record<string, any>): void {
|
|
34
|
+
if (!this.enabled) return;
|
|
35
|
+
|
|
36
|
+
const timestamp = new Date().toISOString();
|
|
37
|
+
console.log(`[${timestamp}] [${this.clientName}] ${message}`);
|
|
38
|
+
|
|
39
|
+
if (data) {
|
|
40
|
+
console.log(JSON.stringify(data, null, 2));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Log an error message (always logs, even in production)
|
|
46
|
+
*/
|
|
47
|
+
logError(message: string, error?: unknown, data?: Record<string, any>): void {
|
|
48
|
+
const timestamp = new Date().toISOString();
|
|
49
|
+
console.error(`[${timestamp}] [${this.clientName}] ERROR: ${message}`);
|
|
50
|
+
|
|
51
|
+
if (error) {
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
console.error('Error details:', {
|
|
54
|
+
name: error.name,
|
|
55
|
+
message: error.message,
|
|
56
|
+
stack: error.stack,
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
console.error('Error details:', error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (data) {
|
|
64
|
+
console.error('Additional context:', JSON.stringify(data, null, 2));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Log a warning message (always logs, even in production)
|
|
70
|
+
*/
|
|
71
|
+
logWarning(message: string, data?: Record<string, any>): void {
|
|
72
|
+
const timestamp = new Date().toISOString();
|
|
73
|
+
console.warn(`[${timestamp}] [${this.clientName}] WARNING: ${message}`);
|
|
74
|
+
|
|
75
|
+
if (data) {
|
|
76
|
+
console.warn(JSON.stringify(data, null, 2));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Log execution time if it exceeds the threshold
|
|
82
|
+
*/
|
|
83
|
+
logExecutionTime(operation: string, timeMs: number, threshold: number = 20000): void {
|
|
84
|
+
if (!this.enabled) return;
|
|
85
|
+
|
|
86
|
+
if (timeMs > threshold) {
|
|
87
|
+
this.log(`⚠️ Long execution time for ${operation}: ${timeMs}ms`);
|
|
88
|
+
} else {
|
|
89
|
+
this.log(`✓ ${operation} completed in ${timeMs}ms`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Log API usage information
|
|
95
|
+
*/
|
|
96
|
+
logUsage(usage: Record<string, any>): void {
|
|
97
|
+
if (!this.enabled) return;
|
|
98
|
+
|
|
99
|
+
this.log('API Usage', usage);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Log response preview
|
|
104
|
+
*/
|
|
105
|
+
logResponsePreview(response: string, maxLength: number = 500): void {
|
|
106
|
+
if (!this.enabled) return;
|
|
107
|
+
|
|
108
|
+
this.log('Response Preview', {
|
|
109
|
+
length: response.length,
|
|
110
|
+
preview: response.substring(0, maxLength) + (response.length > maxLength ? '...' : ''),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if debug logging is enabled
|
|
116
|
+
*/
|
|
117
|
+
isEnabled(): boolean {
|
|
118
|
+
return this.enabled;
|
|
119
|
+
}
|
|
120
|
+
}
|