@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/src/helpers.ts ADDED
@@ -0,0 +1,185 @@
1
+ import { z } from 'zod/v4';
2
+ import type { LlmClientInterface } from './types/client-interface';
3
+
4
+ /**
5
+ * Simple schema for language validation response
6
+ */
7
+ const LanguageCheckSchema = z.object({
8
+ isCorrectLanguage: z.boolean(),
9
+ });
10
+
11
+ /**
12
+ * Checks if content is in the expected language using an LLM
13
+ *
14
+ * @param {Object} params - The parameters for checking language
15
+ * @param {LlmClientInterface} params.llm - The LLM client to use for language detection
16
+ * @param {string} params.content - The content to check
17
+ * @param {string} params.expectedLanguage - The expected language of the content
18
+ * @returns {Promise<boolean>} - Whether the content is in the expected language
19
+ */
20
+ export async function isContentInLanguage({
21
+ llm,
22
+ content,
23
+ expectedLanguage,
24
+ }: {
25
+ llm: LlmClientInterface;
26
+ content: string;
27
+ expectedLanguage: string;
28
+ }): Promise<boolean> {
29
+ try {
30
+ const prompt = `
31
+ <task>
32
+ Determine if the following content is written in ${expectedLanguage} language.
33
+ </task>
34
+
35
+ <instructions>
36
+ - Analyze the text and determine if it's written in ${expectedLanguage}
37
+ - Return true if the content is in ${expectedLanguage}
38
+ - Return false if the content is in any other language
39
+ - Ignore small phrases or individual words that might be in other languages
40
+ - Focus on the primary language of the text
41
+ </instructions>
42
+
43
+ <content>
44
+ ${content}
45
+ </content>
46
+ `;
47
+
48
+ const response = await llm.createStructuredResponse({
49
+ prompt,
50
+ schema: LanguageCheckSchema,
51
+ reasoningEffort: 'low', // Language detection doesn't need high reasoning
52
+ });
53
+
54
+ return response.isCorrectLanguage;
55
+ } catch (error) {
56
+ console.error('Error checking language:', error);
57
+ // In case of error, assume the language is correct to avoid blocking the flow
58
+ return true;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Formats unstructured content into a structured format using an LLM
64
+ *
65
+ * This utility function takes unstructured content (typically from another LLM)
66
+ * and formats it according to a specified Zod schema. This is useful when working
67
+ * with LLMs that don't support structured outputs natively.
68
+ *
69
+ * @template T - The Zod schema type
70
+ * @param {Object} params - The parameters for formatting content
71
+ * @param {LlmClientInterface} params.llm - The LLM client to use for formatting
72
+ * @param {string} params.unstructuredContent - The unstructured content to format
73
+ * @param {T} params.schema - The Zod schema to validate against
74
+ * @param {string} [params.additionalInstructions] - Optional additional instructions for the formatting
75
+ * @returns {Promise<z.infer<T>>} - A promise that resolves to the structured content
76
+ */
77
+ export async function formatContentToStructure<T extends z.ZodType>({
78
+ llm,
79
+ unstructuredContent,
80
+ schema,
81
+ additionalInstructions = '',
82
+ }: {
83
+ llm: LlmClientInterface;
84
+ unstructuredContent: string;
85
+ schema: T;
86
+ additionalInstructions?: string;
87
+ }): Promise<z.infer<T>> {
88
+ const conversionPrompt = `
89
+ This is the output of an AI model that is not able to respond in the format we need.
90
+ Please convert the output to the format we need. If you encounter any errors, return false on the success property (if applicable).
91
+ ${additionalInstructions ? `\n${additionalInstructions}` : ''}
92
+ <output>
93
+ ${JSON.stringify(unstructuredContent, null, 2)}
94
+ </output>
95
+ `;
96
+
97
+ const completion = await llm.createStructuredResponse({
98
+ prompt: conversionPrompt,
99
+ schema: schema,
100
+ reasoningEffort: 'low', // Formatting typically doesn't need high reasoning
101
+ });
102
+
103
+ return completion as z.infer<T>;
104
+ }
105
+
106
+ /**
107
+ * Attempts to generate and format content with language validation
108
+ *
109
+ * This function orchestrates the process of generating content with one LLM,
110
+ * checking if it's in the correct language, and formatting it with another LLM.
111
+ *
112
+ * @template T - The Zod schema type
113
+ * @param {Object} params - The parameters for generating and formatting
114
+ * @param {Function} params.generateContent - Function that generates the unstructured content
115
+ * @param {LlmClientInterface} params.languageCheckLlm - The LLM client to use for language validation
116
+ * @param {LlmClientInterface} params.formatterLlm - The LLM client to use for formatting
117
+ * @param {string} params.expectedLanguage - The expected language of the output
118
+ * @param {T} params.schema - The Zod schema to validate against
119
+ * @param {number} [params.maxAttempts=3] - Maximum number of attempts to get correct language
120
+ * @param {string} [params.additionalInstructions] - Optional additional instructions for formatting
121
+ * @returns {Promise<z.infer<T>>} - A promise that resolves to the structured content
122
+ */
123
+ export async function generateAndFormatWithLanguageCheck<T extends z.ZodType>({
124
+ generateContent,
125
+ languageCheckLlm,
126
+ formatterLlm,
127
+ expectedLanguage,
128
+ schema,
129
+ maxAttempts = 3,
130
+ additionalInstructions,
131
+ }: {
132
+ generateContent: () => Promise<string>;
133
+ languageCheckLlm: LlmClientInterface;
134
+ formatterLlm: LlmClientInterface;
135
+ expectedLanguage: string;
136
+ schema: T;
137
+ maxAttempts?: number;
138
+ additionalInstructions?: string;
139
+ }): Promise<z.infer<T> & { languageCorrect: boolean }> {
140
+ let attempts = 0;
141
+
142
+ while (attempts < maxAttempts) {
143
+ attempts++;
144
+
145
+ // Step 1: Generate content
146
+ const unstructuredContent = await generateContent();
147
+
148
+ // Step 2: Check if content is in the expected language
149
+ const isLanguageCorrect = await isContentInLanguage({
150
+ llm: languageCheckLlm,
151
+ content: unstructuredContent,
152
+ expectedLanguage,
153
+ });
154
+
155
+ // Step 3: Format the content regardless of language correctness
156
+ const formattedContent = await formatContentToStructure({
157
+ llm: formatterLlm,
158
+ unstructuredContent,
159
+ schema,
160
+ additionalInstructions,
161
+ });
162
+
163
+ // Add language check result to the response
164
+ const result = {
165
+ ...(formattedContent as object),
166
+ languageCorrect: isLanguageCorrect,
167
+ } as z.infer<T> & { languageCorrect: boolean };
168
+
169
+ // If language is correct, return immediately
170
+ if (isLanguageCorrect) {
171
+ return result;
172
+ }
173
+
174
+ // If we've reached max attempts, return the last result anyway
175
+ if (attempts === maxAttempts) {
176
+ console.warn(`Max attempts reached. Language still incorrect after ${maxAttempts} tries.`);
177
+ return result;
178
+ }
179
+
180
+ console.info(`Incorrect language detected, retrying...`);
181
+ }
182
+
183
+ // This should never be reached due to the while loop conditions
184
+ throw new Error('Unexpected end of content generation process');
185
+ }
package/src/llm.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { OpenAIClient } from './clients/openai-client';
2
+ import { AnthropicClient } from './clients/anthropic-client';
3
+ import { OpenRouterClient } from './clients/openrouter-client';
4
+ import type { LlmClientInterface } from './types/client-interface';
5
+ import type { AnthropicModel } from './model-types';
6
+
7
+ export type LlmProvider = 'openai' | 'anthropic' | 'openrouter';
8
+
9
+ export type LlmClientOptions = {
10
+ apiKey?: string;
11
+ /**
12
+ * Enable debug mode for development.
13
+ * - If true: always log debug messages
14
+ * - If false: never log debug messages
15
+ * - If undefined: auto-detect based on NODE_ENV (enabled in development)
16
+ */
17
+ debug?: boolean;
18
+ /**
19
+ * Some debug/testing contexts need to opt into reasoning endpoints explicitly (OpenAI only).
20
+ */
21
+ useReasoningMode?: boolean;
22
+ /**
23
+ * OpenAI API key for structured formatting (used by Anthropic and OpenRouter clients).
24
+ * If provided, these clients will use OpenAI for reliable structured output formatting.
25
+ */
26
+ openaiApiKey?: string;
27
+ };
28
+
29
+ /**
30
+ * Unified LLM client factory for all AI model interactions.
31
+ * Returns a client that implements the unified LlmClientInterface,
32
+ * providing consistent API across different providers.
33
+ */
34
+ export class LLM {
35
+ /**
36
+ * Get an LLM client that implements the unified interface.
37
+ * Works with OpenAI, Anthropic, and OpenRouter providers transparently.
38
+ *
39
+ * @param provider - The LLM provider ('openai', 'anthropic', or 'openrouter')
40
+ * @param model - The model identifier (e.g., 'gpt-4o', 'claude-sonnet-4-20250514', 'openai/gpt-4o-mini')
41
+ * @param options - Optional configuration overrides
42
+ * @returns Client implementing LlmClientInterface
43
+ * @example
44
+ * const llm = LLM.getClient('openai', 'gpt-4o');
45
+ * const llm2 = LLM.getClient('openrouter', 'openai/gpt-4o-mini');
46
+ * const result = await llm.createStructuredResponse({ prompt, schema });
47
+ */
48
+ static getClient(provider: LlmProvider, model: string, options?: LlmClientOptions): LlmClientInterface {
49
+ switch (provider) {
50
+ case 'openai':
51
+ return this._createOpenAIClient(model, options);
52
+ case 'anthropic':
53
+ return this._createAnthropicClient(model, options);
54
+ case 'openrouter':
55
+ return this._createOpenRouterClient(model, options);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Create an OpenAI client instance
61
+ */
62
+ private static _createOpenAIClient(model: string, options?: LlmClientOptions): OpenAIClient {
63
+ const apiKey = options?.apiKey ?? process.env.OPENAI_API_KEY;
64
+ if (!apiKey) {
65
+ throw new Error('OPENAI_API_KEY is not set');
66
+ }
67
+ return new OpenAIClient({
68
+ apiKey,
69
+ model,
70
+ debug: options?.debug,
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Create an Anthropic client instance
76
+ */
77
+ private static _createAnthropicClient(model: string, options?: LlmClientOptions): AnthropicClient {
78
+ const apiKey = options?.apiKey ?? process.env.ANTHROPIC_API_KEY;
79
+ if (!apiKey) {
80
+ throw new Error('ANTHROPIC_API_KEY is not set');
81
+ }
82
+
83
+ return new AnthropicClient({
84
+ apiKey,
85
+ model: model as AnthropicModel,
86
+ openaiApiKey: options?.openaiApiKey,
87
+ debug: options?.debug,
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Create an OpenRouter client instance
93
+ */
94
+ private static _createOpenRouterClient(model: string, options?: LlmClientOptions): OpenRouterClient {
95
+ const apiKey = options?.apiKey ?? process.env.OPENROUTER_API_KEY;
96
+ if (!apiKey) {
97
+ throw new Error('OPENROUTER_API_KEY is not set');
98
+ }
99
+
100
+ return new OpenRouterClient({
101
+ apiKey,
102
+ model,
103
+ openaiApiKey: options?.openaiApiKey,
104
+ debug: options?.debug,
105
+ });
106
+ }
107
+ }
@@ -0,0 +1,61 @@
1
+ import type { ChatModel as OpenAIChatModel } from 'openai/resources/shared';
2
+ import type { Model as AnthropicModelType } from '@anthropic-ai/sdk/resources/messages/messages';
3
+
4
+ /**
5
+ * OpenAI models released after the SDK's ChatModel union was last cut.
6
+ * Keep here until `openai` ships an SDK that includes them in `ChatModel`,
7
+ * then drop these literals.
8
+ */
9
+ type OpenAINewerModel =
10
+ | 'gpt-5.5'
11
+ | 'gpt-5.5-pro'
12
+ | 'gpt-5.6-terra'
13
+ | 'gpt-5.6-terra-pro'
14
+ | 'gpt-5.6-luna'
15
+ | 'gpt-5.6-luna-pro'
16
+ | 'gpt-5.6-sol'
17
+ | 'gpt-5.6-sol-pro';
18
+
19
+ /**
20
+ * OpenAI model types from the official SDK, plus newer models not yet in the
21
+ * SDK's `ChatModel` union and the embedding models we use directly.
22
+ */
23
+ export type OpenAIModel =
24
+ | OpenAIChatModel
25
+ | OpenAINewerModel
26
+ | 'text-embedding-3-large'
27
+ | 'text-embedding-ada-002';
28
+
29
+ /**
30
+ * Anthropic model types from the official SDK
31
+ */
32
+ export type AnthropicModel = AnthropicModelType;
33
+
34
+ /**
35
+ * OpenRouter model types
36
+ * OpenRouter provides access to 300+ models across multiple providers
37
+ * Models are specified in the format 'provider/model-name'
38
+ * Examples: 'openai/gpt-4', 'anthropic/claude-3-opus', 'google/gemini-2.5-flash-lite-preview-09-2025'
39
+ */
40
+ export type OpenRouterModel = string;
41
+
42
+ /**
43
+ * Map of provider to their available models
44
+ */
45
+ export type ProviderModelMap = {
46
+ openai: OpenAIModel;
47
+ anthropic: AnthropicModel;
48
+ openrouter: OpenRouterModel;
49
+ };
50
+
51
+ /**
52
+ * Provider-specific model configuration with automatic model type inference
53
+ * When you set provider: 'openai', the model field will autocomplete with OpenAI models
54
+ * When you set provider: 'anthropic', the model field will autocomplete with Anthropic models
55
+ */
56
+ export type ModelConfig<P extends keyof ProviderModelMap = keyof ProviderModelMap> = {
57
+ [K in P]: {
58
+ provider: K;
59
+ model: ProviderModelMap[K];
60
+ };
61
+ }[P];
package/src/models.ts ADDED
@@ -0,0 +1,98 @@
1
+ import type { ModelConfig } from './model-types';
2
+
3
+ /**
4
+ * Default model configurations for LLM operations
5
+ * These are used internally by the LLM package for formatting and structure operations
6
+ */
7
+ export const DEFAULT_MODELS = {
8
+ /**
9
+ * Model used for converting unstructured content into structured format
10
+ * This is typically used when an Anthropic response needs to be formatted
11
+ */
12
+ STRUCTURED_FORMATTER: {
13
+ provider: 'openai',
14
+ model: 'gpt-5-mini-2025-08-07',
15
+ } as ModelConfig<'openai'>,
16
+
17
+ /**
18
+ * Model used for language detection and validation
19
+ */
20
+ LANGUAGE_DETECTOR: {
21
+ provider: 'openai',
22
+ model: 'gpt-5-nano-2025-08-07',
23
+ } as ModelConfig<'openai'>,
24
+
25
+ /**
26
+ * Default Anthropic model for generation tasks
27
+ */
28
+ ANTHROPIC_DEFAULT: {
29
+ provider: 'anthropic',
30
+ model: 'claude-sonnet-4-6',
31
+ } as ModelConfig<'anthropic'>,
32
+
33
+ /**
34
+ * Default OpenAI model for generation tasks
35
+ */
36
+ OPENAI_DEFAULT: {
37
+ provider: 'openai',
38
+ model: 'gpt-5.4',
39
+ } as ModelConfig<'openai'>,
40
+ } as const;
41
+
42
+ /**
43
+ * Maps reasoning effort levels to provider-specific parameters
44
+ *
45
+ * Note: this map is legacy — it is exported for backwards compatibility but is not
46
+ * consumed by any client. Clients pass the normalized `ReasoningEffortLevel` straight
47
+ * through to the provider (OpenAI and OpenRouter both accept `reasoning.effort`,
48
+ * including 'none'; Anthropic translates it into an extended-thinking budget).
49
+ */
50
+ export const REASONING_EFFORT_MAP = {
51
+ openai: {
52
+ none: 'none' as const,
53
+ low: undefined, // OpenAI doesn't use reasoning_effort for low
54
+ medium: 'medium' as const,
55
+ high: 'high' as const,
56
+ },
57
+ } as const;
58
+
59
+ /**
60
+ * Base max tokens for Anthropic responses
61
+ * When using thinking mode, actual max_tokens will be: max(ANTHROPIC_MAX_TOKENS, budget_tokens + 4096)
62
+ * to ensure max_tokens > thinking.budget_tokens as required by the API
63
+ */
64
+ export const ANTHROPIC_MAX_TOKENS = 4096;
65
+
66
+ /**
67
+ * Default system prompt used across all LLM clients
68
+ * Instructs the model to generate content directly without asking clarifying questions
69
+ */
70
+ export const DEFAULT_SYSTEM_PROMPT = `You are an expert assistant.
71
+
72
+ When given a task, produce the requested output directly:
73
+ - Do NOT ask clarifying questions
74
+ - Do NOT present multiple options or menus for the user to choose from
75
+ - Do NOT include meta-commentary about the task
76
+ - Make reasonable assumptions when details are unspecified`;
77
+
78
+ /**
79
+ * Web search tool configurations for different providers
80
+ */
81
+ export const WEB_SEARCH_TOOLS = {
82
+ /**
83
+ * Anthropic web search tool configuration
84
+ * See: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/web-search-tool
85
+ */
86
+ ANTHROPIC: {
87
+ type: 'web_search_20250305',
88
+ name: 'web_search',
89
+ },
90
+
91
+ /**
92
+ * OpenAI web search tool configuration for Responses API
93
+ * See: https://cookbook.openai.com/examples/responses_api/responses_example
94
+ */
95
+ OPENAI: {
96
+ type: 'web_search',
97
+ },
98
+ } as const;