llm-fns 1.0.18 → 1.0.19

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.
@@ -14,12 +14,7 @@ export interface CreateFetcherDependencies {
14
14
  prefix?: string;
15
15
  /** Time-to-live for cache entries, in milliseconds. */
16
16
  ttl?: number;
17
- /** Request timeout in milliseconds. If not provided, no timeout is applied.**Restoring Corrected File**
18
-
19
- I'm now generating the corrected version of `src/createCachedFetcher.ts`. The primary fix is removing the extraneous text from the `set` method signature within the `CacheLike` interface. I've ensured the syntax is correct, and I'm confident the test run should now pass. After this is output, I plan to assess its integration within the wider project.
20
-
21
-
22
- */
17
+ /** Request timeout in milliseconds. If not provided, no timeout is applied. */
23
18
  timeout?: number;
24
19
  /** User-Agent string for requests. */
25
20
  userAgent?: string;
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- // src/createCachedFetcher.ts
3
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
4
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
4
  };
@@ -7,20 +6,43 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
6
  exports.CachedResponse = void 0;
8
7
  exports.createCachedFetcher = createCachedFetcher;
9
8
  const crypto_1 = __importDefault(require("crypto"));
10
- // A custom Response class to correctly handle the `.url` property on cache HITs.
11
- // This is an implementation detail and doesn't need to be exported.
12
9
  class CachedResponse extends Response {
13
10
  #finalUrl;
14
11
  constructor(body, init, finalUrl) {
15
12
  super(body, init);
16
13
  this.#finalUrl = finalUrl;
17
14
  }
18
- // Override the read-only `url` property
19
15
  get url() {
20
16
  return this.#finalUrl;
21
17
  }
22
18
  }
23
19
  exports.CachedResponse = CachedResponse;
20
+ /**
21
+ * Creates a deterministic hash of headers for cache key generation.
22
+ * Headers are sorted alphabetically to ensure consistency.
23
+ */
24
+ function hashHeaders(headers) {
25
+ if (!headers)
26
+ return '';
27
+ let headerEntries;
28
+ if (headers instanceof Headers) {
29
+ headerEntries = Array.from(headers.entries());
30
+ }
31
+ else if (Array.isArray(headers)) {
32
+ headerEntries = headers;
33
+ }
34
+ else {
35
+ headerEntries = Object.entries(headers);
36
+ }
37
+ if (headerEntries.length === 0)
38
+ return '';
39
+ // Sort alphabetically by key for deterministic ordering
40
+ headerEntries.sort((a, b) => a[0].localeCompare(b[0]));
41
+ const headerString = headerEntries
42
+ .map(([key, value]) => `${key}:${value}`)
43
+ .join('|');
44
+ return crypto_1.default.createHash('md5').update(headerString).digest('hex');
45
+ }
24
46
  /**
25
47
  * Factory function that creates a `fetch` replacement with a caching layer.
26
48
  * @param deps - Dependencies including the cache instance, prefix, TTL, and timeout.
@@ -30,8 +52,6 @@ function createCachedFetcher(deps) {
30
52
  const { cache, prefix = 'http-cache', ttl, timeout, userAgent, fetch: customFetch, shouldCache } = deps;
31
53
  const fetchImpl = customFetch ?? fetch;
32
54
  const fetchWithTimeout = async (url, options) => {
33
- // Correctly merge headers using Headers API to handle various input formats (plain object, Headers instance, array)
34
- // and avoid issues with spreading Headers objects which can lead to lost headers or Symbol errors.
35
55
  const headers = new Headers(options?.headers);
36
56
  if (userAgent) {
37
57
  headers.set('User-Agent', userAgent);
@@ -70,10 +90,7 @@ function createCachedFetcher(deps) {
70
90
  clearTimeout(timeoutId);
71
91
  }
72
92
  };
73
- // This is the actual fetcher implementation, returned by the factory.
74
- // It "closes over" the dependencies provided to the factory.
75
93
  return async (url, options) => {
76
- // Determine the request method. Default to GET for fetch.
77
94
  let method = 'GET';
78
95
  if (options?.method) {
79
96
  method = options.method;
@@ -87,7 +104,7 @@ function createCachedFetcher(deps) {
87
104
  return fetchWithTimeout(url, options);
88
105
  }
89
106
  let cacheKey = `${prefix}:${urlString}`;
90
- // If POST (or others with body), append hash of body to cache key
107
+ // Hash body for POST requests
91
108
  if (method.toUpperCase() === 'POST' && options?.body) {
92
109
  let bodyStr = '';
93
110
  if (typeof options.body === 'string') {
@@ -97,7 +114,6 @@ function createCachedFetcher(deps) {
97
114
  bodyStr = options.body.toString();
98
115
  }
99
116
  else {
100
- // Fallback for other types, though mostly we expect string/JSON here
101
117
  try {
102
118
  bodyStr = JSON.stringify(options.body);
103
119
  }
@@ -105,13 +121,17 @@ function createCachedFetcher(deps) {
105
121
  bodyStr = 'unserializable';
106
122
  }
107
123
  }
108
- const hash = crypto_1.default.createHash('md5').update(bodyStr).digest('hex');
109
- cacheKey += `:${hash}`;
124
+ const bodyHash = crypto_1.default.createHash('md5').update(bodyStr).digest('hex');
125
+ cacheKey += `:body:${bodyHash}`;
126
+ }
127
+ // Hash all request headers into cache key
128
+ const headersHash = hashHeaders(options?.headers);
129
+ if (headersHash) {
130
+ cacheKey += `:headers:${headersHash}`;
110
131
  }
111
132
  // 1. Check the cache
112
133
  const cachedItem = await cache.get(cacheKey);
113
134
  if (cachedItem) {
114
- // Decode the base64 body back into a Buffer.
115
135
  const body = Buffer.from(cachedItem.bodyBase64, 'base64');
116
136
  return new CachedResponse(body, {
117
137
  status: cachedItem.status,
@@ -135,7 +155,6 @@ function createCachedFetcher(deps) {
135
155
  }
136
156
  }
137
157
  else {
138
- // Default behavior: check for .error in JSON responses
139
158
  const contentType = response.headers.get('content-type');
140
159
  if (contentType && contentType.includes('application/json')) {
141
160
  const checkClone = response.clone();
@@ -154,7 +173,6 @@ function createCachedFetcher(deps) {
154
173
  if (isCacheable) {
155
174
  const responseClone = response.clone();
156
175
  const bodyBuffer = await responseClone.arrayBuffer();
157
- // Convert ArrayBuffer to a base64 string for safe JSON serialization.
158
176
  const bodyBase64 = Buffer.from(bodyBuffer).toString('base64');
159
177
  const headers = Object.fromEntries(response.headers.entries());
160
178
  const itemToCache = {
@@ -1,6 +1,10 @@
1
1
  import OpenAI from 'openai';
2
- import { PromptFunction, LlmPromptOptions } from "./createLlmClient.js";
3
- export type JsonSchemaLlmClientOptions = Omit<LlmPromptOptions, 'messages' | 'response_format'> & {
2
+ import { PromptFunction, LlmCommonOptions } from "./createLlmClient.js";
3
+ /**
4
+ * Options for JSON schema prompt functions.
5
+ * Extends common options with JSON-specific settings.
6
+ */
7
+ export interface JsonSchemaLlmClientOptions extends LlmCommonOptions {
4
8
  maxRetries?: number;
5
9
  /**
6
10
  * If true, passes `response_format: { type: 'json_object' }` to the model.
@@ -22,7 +26,7 @@ export type JsonSchemaLlmClientOptions = Omit<LlmPromptOptions, 'messages' | 're
22
26
  * If not provided, an AJV-based validator will be used.
23
27
  */
24
28
  validator?: (data: any) => any;
25
- };
29
+ }
26
30
  export interface CreateJsonSchemaLlmClientParams {
27
31
  prompt: PromptFunction;
28
32
  fallbackPrompt?: PromptFunction;
@@ -9,7 +9,7 @@ const createLlmRetryClient_js_1 = require("./createLlmRetryClient.js");
9
9
  function createJsonSchemaLlmClient(params) {
10
10
  const { prompt, fallbackPrompt, disableJsonFixer = false } = params;
11
11
  const llmRetryClient = (0, createLlmRetryClient_js_1.createLlmRetryClient)({ prompt, fallbackPrompt });
12
- const ajv = new ajv_1.default({ strict: false }); // Initialize AJV
12
+ const ajv = new ajv_1.default({ strict: false });
13
13
  async function _tryToFixJson(brokenResponse, schemaJsonString, errorDetails, options) {
14
14
  const fixupPrompt = `
15
15
  An attempt to generate a JSON object resulted in the following output, which is either not valid JSON or does not conform to the required schema.
@@ -37,7 +37,7 @@ ${brokenResponse}
37
37
  const response_format = useResponseFormat
38
38
  ? { type: 'json_object' }
39
39
  : undefined;
40
- const { maxRetries, useResponseFormat: _useResponseFormat, ...restOptions } = options || {};
40
+ const { maxRetries, useResponseFormat: _useResponseFormat, beforeValidation, validator, ...restOptions } = options || {};
41
41
  const completion = await prompt({
42
42
  messages,
43
43
  response_format,
@@ -51,7 +51,6 @@ ${brokenResponse}
51
51
  }
52
52
  async function _parseOrFixJson(llmResponseString, schemaJsonString, options) {
53
53
  let jsonDataToParse = llmResponseString.trim();
54
- // Robust handling for responses wrapped in markdown code blocks
55
54
  const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)\s*```/;
56
55
  const match = codeBlockRegex.exec(jsonDataToParse);
57
56
  if (match && match[1]) {
@@ -65,9 +64,8 @@ ${brokenResponse}
65
64
  }
66
65
  catch (parseError) {
67
66
  if (disableJsonFixer) {
68
- throw parseError; // re-throw original error
67
+ throw parseError;
69
68
  }
70
- // Attempt a one-time fix before failing.
71
69
  const errorDetails = `JSON Parse Error: ${parseError.message}`;
72
70
  const fixedResponse = await _tryToFixJson(jsonDataToParse, schemaJsonString, errorDetails, options);
73
71
  if (fixedResponse) {
@@ -75,11 +73,10 @@ ${brokenResponse}
75
73
  return JSON.parse(fixedResponse);
76
74
  }
77
75
  catch (e) {
78
- // Fix-up failed, throw original error.
79
76
  throw parseError;
80
77
  }
81
78
  }
82
- throw parseError; // if no fixed response
79
+ throw parseError;
83
80
  }
84
81
  }
85
82
  async function _validateOrFix(jsonData, validator, schemaJsonString, options) {
@@ -93,7 +90,6 @@ ${brokenResponse}
93
90
  if (disableJsonFixer) {
94
91
  throw validationError;
95
92
  }
96
- // Attempt a one-time fix for schema validation errors.
97
93
  const errorDetails = `Schema Validation Error: ${validationError.message}`;
98
94
  const fixedResponse = await _tryToFixJson(JSON.stringify(jsonData, null, 2), schemaJsonString, errorDetails, options);
99
95
  if (fixedResponse) {
@@ -105,11 +101,10 @@ ${brokenResponse}
105
101
  return validator(fixedJsonData);
106
102
  }
107
103
  catch (e) {
108
- // Fix-up failed, throw original validation error
109
104
  throw validationError;
110
105
  }
111
106
  }
112
- throw validationError; // if no fixed response
107
+ throw validationError;
113
108
  }
114
109
  }
115
110
  function _getJsonPromptConfig(messages, schema, options) {
@@ -120,12 +115,9 @@ Do NOT include any other text, explanations, or markdown formatting (like \`\`\`
120
115
 
121
116
  JSON schema:
122
117
  ${schemaJsonString}`;
123
- // Clone messages to avoid mutating the input
124
118
  const finalMessages = [...messages];
125
- // Find the first system message to append instructions to
126
119
  const systemMessageIndex = finalMessages.findIndex(m => m.role === 'system');
127
120
  if (systemMessageIndex !== -1) {
128
- // Append to existing system message
129
121
  const existingContent = finalMessages[systemMessageIndex].content;
130
122
  finalMessages[systemMessageIndex] = {
131
123
  ...finalMessages[systemMessageIndex],
@@ -133,7 +125,6 @@ ${schemaJsonString}`;
133
125
  };
134
126
  }
135
127
  else {
136
- // Prepend new system message
137
128
  finalMessages.unshift({
138
129
  role: 'system',
139
130
  content: commonPromptFooter
@@ -146,7 +137,6 @@ ${schemaJsonString}`;
146
137
  return { finalMessages, schemaJsonString, response_format };
147
138
  }
148
139
  async function promptJson(messages, schema, options) {
149
- // Default validator using AJV
150
140
  const defaultValidator = (data) => {
151
141
  try {
152
142
  const validate = ajv.compile(schema);
@@ -180,7 +170,6 @@ The response provided was not valid JSON. Please correct it.`;
180
170
  return validatedData;
181
171
  }
182
172
  catch (validationError) {
183
- // We assume the validator throws an error with a meaningful message
184
173
  const rawResponseForError = JSON.stringify(jsonData, null, 2);
185
174
  const errorDetails = validationError.message;
186
175
  const errorMessage = `Your previous response resulted in an error.
@@ -190,8 +179,10 @@ The response was valid JSON but did not conform to the required schema. Please r
190
179
  throw new createLlmRetryClient_js_1.LlmRetryError(errorMessage, 'CUSTOM_ERROR', validationError, rawResponseForError);
191
180
  }
192
181
  };
182
+ const { maxRetries, useResponseFormat: _useResponseFormat, beforeValidation, validator: _validator, ...restOptions } = options || {};
193
183
  const retryOptions = {
194
- ...options,
184
+ ...restOptions,
185
+ maxRetries,
195
186
  messages: finalMessages,
196
187
  response_format,
197
188
  validate: processResponse
@@ -21,12 +21,24 @@ export type OpenRouterResponseFormat = {
21
21
  };
22
22
  };
23
23
  /**
24
- * Options for the individual "prompt" function calls.
25
- * These can override defaults or add call-specific parameters.
26
- * 'messages' is a required property, inherited from OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming.
24
+ * Request-level options passed to the OpenAI SDK.
25
+ * These are separate from the body parameters.
27
26
  */
28
- export interface LlmPromptOptions extends Omit<OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, 'model' | 'response_format' | 'modalities' | 'messages'> {
29
- messages: string | OpenAI.Chat.Completions.ChatCompletionMessageParam[];
27
+ export interface LlmRequestOptions {
28
+ headers?: Record<string, string>;
29
+ signal?: AbortSignal;
30
+ timeout?: number;
31
+ }
32
+ /**
33
+ * Merges two LlmRequestOptions objects.
34
+ * Headers are merged (override wins on conflict), other properties are replaced.
35
+ */
36
+ export declare function mergeRequestOptions(base?: LlmRequestOptions, override?: LlmRequestOptions): LlmRequestOptions | undefined;
37
+ /**
38
+ * Common options shared by all prompt functions.
39
+ * Does NOT include messages - those are handled separately.
40
+ */
41
+ export interface LlmCommonOptions {
30
42
  model?: ModelConfig;
31
43
  retries?: number;
32
44
  /** @deprecated Use `reasoning` object instead. */
@@ -35,6 +47,31 @@ export interface LlmPromptOptions extends Omit<OpenAI.Chat.Completions.ChatCompl
35
47
  image_config?: {
36
48
  aspect_ratio?: string;
37
49
  };
50
+ requestOptions?: LlmRequestOptions;
51
+ temperature?: number;
52
+ max_tokens?: number;
53
+ top_p?: number;
54
+ frequency_penalty?: number;
55
+ presence_penalty?: number;
56
+ stop?: string | string[];
57
+ reasoning_effort?: 'low' | 'medium' | 'high';
58
+ seed?: number;
59
+ user?: string;
60
+ tools?: OpenAI.Chat.Completions.ChatCompletionTool[];
61
+ tool_choice?: OpenAI.Chat.Completions.ChatCompletionToolChoiceOption;
62
+ }
63
+ /**
64
+ * Options for the individual "prompt" function calls.
65
+ * Allows messages as string or array for convenience.
66
+ */
67
+ export interface LlmPromptOptions extends LlmCommonOptions {
68
+ messages: string | OpenAI.Chat.Completions.ChatCompletionMessageParam[];
69
+ }
70
+ /**
71
+ * Internal normalized params - messages is always an array.
72
+ */
73
+ export interface LlmPromptParams extends LlmCommonOptions {
74
+ messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
38
75
  }
39
76
  /**
40
77
  * Options required to create an instance of the LlmClient.
@@ -45,8 +82,13 @@ export interface CreateLlmClientParams {
45
82
  defaultModel: ModelConfig;
46
83
  maxConversationChars?: number;
47
84
  queue?: PQueue;
85
+ defaultRequestOptions?: LlmRequestOptions;
48
86
  }
49
- export declare function normalizeOptions(arg1: string | LlmPromptOptions, arg2?: Omit<LlmPromptOptions, 'messages'>): LlmPromptOptions;
87
+ /**
88
+ * Normalizes input arguments to LlmPromptParams.
89
+ * Handles string shorthand and messages-as-string.
90
+ */
91
+ export declare function normalizeOptions(arg1: string | LlmPromptOptions, arg2?: LlmCommonOptions): LlmPromptParams;
50
92
  /**
51
93
  * Factory function that creates a GPT "prompt" function.
52
94
  * @param params - The core dependencies (API key, base URL, default model).
@@ -54,15 +96,15 @@ export declare function normalizeOptions(arg1: string | LlmPromptOptions, arg2?:
54
96
  */
55
97
  export declare function createLlmClient(params: CreateLlmClientParams): {
56
98
  prompt: {
57
- (content: string, options?: Omit<LlmPromptOptions, "messages">): Promise<OpenAI.Chat.Completions.ChatCompletion>;
99
+ (content: string, options?: LlmCommonOptions): Promise<OpenAI.Chat.Completions.ChatCompletion>;
58
100
  (options: LlmPromptOptions): Promise<OpenAI.Chat.Completions.ChatCompletion>;
59
101
  };
60
102
  promptText: {
61
- (content: string, options?: Omit<LlmPromptOptions, "messages">): Promise<string>;
103
+ (content: string, options?: LlmCommonOptions): Promise<string>;
62
104
  (options: LlmPromptOptions): Promise<string>;
63
105
  };
64
106
  promptImage: {
65
- (content: string, options?: Omit<LlmPromptOptions, "messages">): Promise<Buffer>;
107
+ (content: string, options?: LlmCommonOptions): Promise<Buffer>;
66
108
  (options: LlmPromptOptions): Promise<Buffer>;
67
109
  };
68
110
  };
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.countChars = countChars;
4
4
  exports.truncateSingleMessage = truncateSingleMessage;
5
5
  exports.truncateMessages = truncateMessages;
6
+ exports.mergeRequestOptions = mergeRequestOptions;
6
7
  exports.normalizeOptions = normalizeOptions;
7
8
  exports.createLlmClient = createLlmClient;
8
9
  const retryUtils_js_1 = require("./retryUtils.js");
@@ -49,14 +50,12 @@ function truncateSingleMessage(message, charLimit) {
49
50
  return messageCopy;
50
51
  }
51
52
  if (Array.isArray(messageCopy.content)) {
52
- // Complex case: multipart message.
53
- // Strategy: consolidate text, remove images if needed, then truncate text.
54
53
  const textParts = messageCopy.content.filter((p) => p.type === 'text');
55
54
  const imageParts = messageCopy.content.filter((p) => p.type === 'image_url');
56
55
  let combinedText = textParts.map((p) => p.text).join('\n');
57
56
  let keptImages = [...imageParts];
58
57
  while (combinedText.length + (keptImages.length * 2500) > charLimit && keptImages.length > 0) {
59
- keptImages.pop(); // remove images from the end
58
+ keptImages.pop();
60
59
  }
61
60
  const imageChars = keptImages.length * 2500;
62
61
  const textCharLimit = charLimit - imageChars;
@@ -89,7 +88,6 @@ function truncateMessages(messages, limit) {
89
88
  }
90
89
  const mutableOtherMessages = JSON.parse(JSON.stringify(otherMessages));
91
90
  let excessChars = totalChars - limit;
92
- // Truncate messages starting from the second one.
93
91
  for (let i = 1; i < mutableOtherMessages.length; i++) {
94
92
  if (excessChars <= 0)
95
93
  break;
@@ -100,7 +98,6 @@ function truncateMessages(messages, limit) {
100
98
  mutableOtherMessages[i] = truncateSingleMessage(message, newCharCount);
101
99
  excessChars -= charsToCut;
102
100
  }
103
- // If still over limit, truncate the first message.
104
101
  if (excessChars > 0) {
105
102
  const firstMessage = mutableOtherMessages[0];
106
103
  const firstMessageChars = countChars(firstMessage);
@@ -108,7 +105,6 @@ function truncateMessages(messages, limit) {
108
105
  const newCharCount = firstMessageChars - charsToCut;
109
106
  mutableOtherMessages[0] = truncateSingleMessage(firstMessage, newCharCount);
110
107
  }
111
- // Filter out empty messages (char count is 0)
112
108
  const finalMessages = mutableOtherMessages.filter(msg => countChars(msg) > 0);
113
109
  return systemMessage ? [systemMessage, ...finalMessages] : finalMessages;
114
110
  }
@@ -135,7 +131,6 @@ function concatMessageText(messages) {
135
131
  }
136
132
  function getPromptSummary(messages) {
137
133
  const fullText = concatMessageText(messages);
138
- // Replace multiple whitespace chars with a single space and trim.
139
134
  const cleanedText = fullText.replace(/\s+/g, ' ').trim();
140
135
  if (cleanedText.length <= 50) {
141
136
  return cleanedText;
@@ -149,6 +144,30 @@ function getPromptSummary(messages) {
149
144
  const middle = cleanedText.substring(midStart, midEnd);
150
145
  return `${start}...${middle}...${end}`;
151
146
  }
147
+ /**
148
+ * Merges two LlmRequestOptions objects.
149
+ * Headers are merged (override wins on conflict), other properties are replaced.
150
+ */
151
+ function mergeRequestOptions(base, override) {
152
+ if (!base && !override)
153
+ return undefined;
154
+ if (!base)
155
+ return override;
156
+ if (!override)
157
+ return base;
158
+ return {
159
+ ...base,
160
+ ...override,
161
+ headers: {
162
+ ...base.headers,
163
+ ...override.headers
164
+ }
165
+ };
166
+ }
167
+ /**
168
+ * Normalizes input arguments to LlmPromptParams.
169
+ * Handles string shorthand and messages-as-string.
170
+ */
152
171
  function normalizeOptions(arg1, arg2) {
153
172
  if (typeof arg1 === 'string') {
154
173
  return {
@@ -171,14 +190,12 @@ function normalizeOptions(arg1, arg2) {
171
190
  * @returns An async function `prompt` ready to make OpenAI calls.
172
191
  */
173
192
  function createLlmClient(params) {
174
- const { openai, defaultModel: factoryDefaultModel, maxConversationChars, queue } = params;
175
- const getCompletionParams = (options) => {
176
- const { model: callSpecificModel, messages, reasoning_effort, retries, ...restApiOptions } = options;
177
- // Ensure messages is an array (it should be if normalized, but for safety/types)
178
- const messagesArray = typeof messages === 'string'
179
- ? [{ role: 'user', content: messages }]
193
+ const { openai, defaultModel: factoryDefaultModel, maxConversationChars, queue, defaultRequestOptions } = params;
194
+ const getCompletionParams = (promptParams) => {
195
+ const { model: callSpecificModel, messages, retries, requestOptions, ...restApiOptions } = promptParams;
196
+ const finalMessages = maxConversationChars
197
+ ? truncateMessages(messages, maxConversationChars)
180
198
  : messages;
181
- const finalMessages = maxConversationChars ? truncateMessages(messagesArray, maxConversationChars) : messagesArray;
182
199
  const baseConfig = typeof factoryDefaultModel === 'object' && factoryDefaultModel !== null
183
200
  ? factoryDefaultModel
184
201
  : (typeof factoryDefaultModel === 'string' ? { model: factoryDefaultModel } : {});
@@ -196,15 +213,16 @@ function createLlmClient(params) {
196
213
  messages: finalMessages,
197
214
  ...restApiOptions,
198
215
  };
199
- return { completionParams, modelToUse, finalMessages, retries };
216
+ const mergedRequestOptions = mergeRequestOptions(defaultRequestOptions, requestOptions);
217
+ return { completionParams, modelToUse, finalMessages, retries, requestOptions: mergedRequestOptions };
200
218
  };
201
219
  async function prompt(arg1, arg2) {
202
- const options = normalizeOptions(arg1, arg2);
203
- const { completionParams, finalMessages, retries } = getCompletionParams(options);
220
+ const promptParams = normalizeOptions(arg1, arg2);
221
+ const { completionParams, finalMessages, retries, requestOptions } = getCompletionParams(promptParams);
204
222
  const promptSummary = getPromptSummary(finalMessages);
205
223
  const apiCall = async () => {
206
224
  const task = () => (0, retryUtils_js_1.executeWithRetry)(async () => {
207
- return openai.chat.completions.create(completionParams);
225
+ return openai.chat.completions.create(completionParams, requestOptions);
208
226
  }, async (completion) => {
209
227
  if (completion.error) {
210
228
  return {
@@ -213,7 +231,6 @@ function createLlmClient(params) {
213
231
  }
214
232
  return { isValid: true, data: completion };
215
233
  }, retries ?? 3, undefined, (error) => {
216
- // Do not retry if the API key is invalid (401) or if the error code explicitly states it.
217
234
  if (error?.status === 401 || error?.code === 'invalid_api_key') {
218
235
  return false;
219
236
  }
@@ -225,8 +242,8 @@ function createLlmClient(params) {
225
242
  return apiCall();
226
243
  }
227
244
  async function promptText(arg1, arg2) {
228
- const options = normalizeOptions(arg1, arg2);
229
- const response = await prompt(options);
245
+ const promptParams = normalizeOptions(arg1, arg2);
246
+ const response = await prompt(promptParams);
230
247
  const content = response.choices[0]?.message?.content;
231
248
  if (content === null || content === undefined) {
232
249
  throw new Error("LLM returned no text content.");
@@ -234,8 +251,8 @@ function createLlmClient(params) {
234
251
  return content;
235
252
  }
236
253
  async function promptImage(arg1, arg2) {
237
- const options = normalizeOptions(arg1, arg2);
238
- const response = await prompt(options);
254
+ const promptParams = normalizeOptions(arg1, arg2);
255
+ const response = await prompt(promptParams);
239
256
  const message = response.choices[0]?.message;
240
257
  if (message.images && Array.isArray(message.images) && message.images.length > 0) {
241
258
  const imageUrl = message.images[0].image_url.url;
@@ -37,4 +37,76 @@ const createLlmClient_js_1 = require("./createLlmClient.js");
37
37
  temperature: 0.7
38
38
  });
39
39
  });
40
+ (0, vitest_1.it)('should include requestOptions when provided', () => {
41
+ const result = (0, createLlmClient_js_1.normalizeOptions)('Hello world', {
42
+ temperature: 0.5,
43
+ requestOptions: {
44
+ headers: { 'X-Custom': 'value' },
45
+ timeout: 5000
46
+ }
47
+ });
48
+ (0, vitest_1.expect)(result).toEqual({
49
+ messages: [{ role: 'user', content: 'Hello world' }],
50
+ temperature: 0.5,
51
+ requestOptions: {
52
+ headers: { 'X-Custom': 'value' },
53
+ timeout: 5000
54
+ }
55
+ });
56
+ });
57
+ });
58
+ (0, vitest_1.describe)('mergeRequestOptions', () => {
59
+ (0, vitest_1.it)('should return undefined when both are undefined', () => {
60
+ const result = (0, createLlmClient_js_1.mergeRequestOptions)(undefined, undefined);
61
+ (0, vitest_1.expect)(result).toBeUndefined();
62
+ });
63
+ (0, vitest_1.it)('should return override when base is undefined', () => {
64
+ const override = { timeout: 5000 };
65
+ const result = (0, createLlmClient_js_1.mergeRequestOptions)(undefined, override);
66
+ (0, vitest_1.expect)(result).toBe(override);
67
+ });
68
+ (0, vitest_1.it)('should return base when override is undefined', () => {
69
+ const base = { timeout: 5000 };
70
+ const result = (0, createLlmClient_js_1.mergeRequestOptions)(base, undefined);
71
+ (0, vitest_1.expect)(result).toBe(base);
72
+ });
73
+ (0, vitest_1.it)('should merge headers from both', () => {
74
+ const base = {
75
+ headers: { 'X-Base': 'base-value' },
76
+ timeout: 5000
77
+ };
78
+ const override = {
79
+ headers: { 'X-Override': 'override-value' }
80
+ };
81
+ const result = (0, createLlmClient_js_1.mergeRequestOptions)(base, override);
82
+ (0, vitest_1.expect)(result).toEqual({
83
+ headers: {
84
+ 'X-Base': 'base-value',
85
+ 'X-Override': 'override-value'
86
+ },
87
+ timeout: 5000
88
+ });
89
+ });
90
+ (0, vitest_1.it)('should override scalar properties', () => {
91
+ const base = { timeout: 5000 };
92
+ const override = { timeout: 10000 };
93
+ const result = (0, createLlmClient_js_1.mergeRequestOptions)(base, override);
94
+ (0, vitest_1.expect)(result).toEqual({ timeout: 10000, headers: {} });
95
+ });
96
+ (0, vitest_1.it)('should override conflicting headers', () => {
97
+ const base = {
98
+ headers: { 'X-Shared': 'base-value', 'X-Base': 'base' }
99
+ };
100
+ const override = {
101
+ headers: { 'X-Shared': 'override-value', 'X-Override': 'override' }
102
+ };
103
+ const result = (0, createLlmClient_js_1.mergeRequestOptions)(base, override);
104
+ (0, vitest_1.expect)(result).toEqual({
105
+ headers: {
106
+ 'X-Shared': 'override-value',
107
+ 'X-Base': 'base',
108
+ 'X-Override': 'override'
109
+ }
110
+ });
111
+ });
40
112
  });
@@ -1,5 +1,5 @@
1
1
  import OpenAI from 'openai';
2
- import { PromptFunction, LlmPromptOptions } from "./createLlmClient.js";
2
+ import { PromptFunction, LlmCommonOptions, LlmPromptOptions } from "./createLlmClient.js";
3
3
  export declare class LlmRetryError extends Error {
4
4
  readonly message: string;
5
5
  readonly type: 'JSON_PARSE_ERROR' | 'CUSTOM_ERROR';
@@ -23,26 +23,30 @@ export interface LlmRetryResponseInfo {
23
23
  conversation: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
24
24
  attemptNumber: number;
25
25
  }
26
- export type LlmRetryOptions<T = any> = LlmPromptOptions & {
26
+ /**
27
+ * Options for retry prompt functions.
28
+ * Extends common options with retry-specific settings.
29
+ */
30
+ export interface LlmRetryOptions<T = any> extends LlmCommonOptions {
27
31
  maxRetries?: number;
28
32
  validate?: (response: any, info: LlmRetryResponseInfo) => Promise<T>;
29
- };
33
+ }
30
34
  export interface CreateLlmRetryClientParams {
31
35
  prompt: PromptFunction;
32
36
  fallbackPrompt?: PromptFunction;
33
37
  }
34
38
  export declare function createLlmRetryClient(params: CreateLlmRetryClientParams): {
35
39
  promptRetry: {
36
- <T = OpenAI.Chat.Completions.ChatCompletion>(content: string, options?: Omit<LlmRetryOptions<T>, "messages">): Promise<T>;
37
- <T = OpenAI.Chat.Completions.ChatCompletion>(options: LlmRetryOptions<T>): Promise<T>;
40
+ <T = OpenAI.Chat.Completions.ChatCompletion>(content: string, options?: LlmRetryOptions<T>): Promise<T>;
41
+ <T = OpenAI.Chat.Completions.ChatCompletion>(options: LlmPromptOptions & LlmRetryOptions<T>): Promise<T>;
38
42
  };
39
43
  promptTextRetry: {
40
- <T = string>(content: string, options?: Omit<LlmRetryOptions<T>, "messages">): Promise<T>;
41
- <T = string>(options: LlmRetryOptions<T>): Promise<T>;
44
+ <T = string>(content: string, options?: LlmRetryOptions<T>): Promise<T>;
45
+ <T = string>(options: LlmPromptOptions & LlmRetryOptions<T>): Promise<T>;
42
46
  };
43
47
  promptImageRetry: {
44
- <T = Buffer<ArrayBufferLike>>(content: string, options?: Omit<LlmRetryOptions<T>, "messages">): Promise<T>;
45
- <T = Buffer<ArrayBufferLike>>(options: LlmRetryOptions<T>): Promise<T>;
48
+ <T = Buffer<ArrayBufferLike>>(content: string, options?: LlmRetryOptions<T>): Promise<T>;
49
+ <T = Buffer<ArrayBufferLike>>(options: LlmPromptOptions & LlmRetryOptions<T>): Promise<T>;
46
50
  };
47
51
  };
48
52
  export type LlmRetryClient = ReturnType<typeof createLlmRetryClient>;
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LlmRetryAttemptError = exports.LlmRetryExhaustedError = exports.LlmRetryError = void 0;
4
4
  exports.createLlmRetryClient = createLlmRetryClient;
5
5
  const createLlmClient_js_1 = require("./createLlmClient.js");
6
- // Custom error for the querier to handle, allowing retries with structured feedback.
7
6
  class LlmRetryError extends Error {
8
7
  message;
9
8
  type;
@@ -28,8 +27,6 @@ class LlmRetryExhaustedError extends Error {
28
27
  }
29
28
  }
30
29
  exports.LlmRetryExhaustedError = LlmRetryExhaustedError;
31
- // This error is thrown by LlmRetryClient for each failed attempt.
32
- // It wraps the underlying error (from API call or validation) and adds context.
33
30
  class LlmRetryAttemptError extends Error {
34
31
  message;
35
32
  mode;
@@ -45,13 +42,19 @@ class LlmRetryAttemptError extends Error {
45
42
  }
46
43
  }
47
44
  exports.LlmRetryAttemptError = LlmRetryAttemptError;
45
+ function normalizeRetryOptions(arg1, arg2) {
46
+ const baseParams = (0, createLlmClient_js_1.normalizeOptions)(arg1, arg2);
47
+ return {
48
+ ...baseParams,
49
+ ...arg2,
50
+ messages: baseParams.messages
51
+ };
52
+ }
48
53
  function constructLlmMessages(initialMessages, attemptNumber, previousError) {
49
54
  if (attemptNumber === 0) {
50
- // First attempt
51
55
  return initialMessages;
52
56
  }
53
57
  if (!previousError) {
54
- // Should not happen for attempt > 0, but as a safeguard...
55
58
  throw new Error("Invariant violation: previousError is missing for a retry attempt.");
56
59
  }
57
60
  const cause = previousError.cause;
@@ -64,10 +67,8 @@ function constructLlmMessages(initialMessages, attemptNumber, previousError) {
64
67
  }
65
68
  function createLlmRetryClient(params) {
66
69
  const { prompt, fallbackPrompt } = params;
67
- async function runPromptLoop(options, responseType) {
68
- const { maxRetries = 3, validate, messages, ...restOptions } = options;
69
- // Ensure messages is an array (normalizeOptions ensures this but types might be loose)
70
- const initialMessages = messages;
70
+ async function runPromptLoop(retryParams, responseType) {
71
+ const { maxRetries = 3, validate, messages: initialMessages, ...restOptions } = retryParams;
71
72
  let lastError;
72
73
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
73
74
  const useFallback = !!fallbackPrompt && attempt > 0;
@@ -111,7 +112,6 @@ function createLlmRetryClient(params) {
111
112
  throw new LlmRetryError("LLM returned no image.", 'CUSTOM_ERROR', undefined, JSON.stringify(completion));
112
113
  }
113
114
  }
114
- // Construct conversation history for success or potential error reporting
115
115
  const finalConversation = [...currentMessages];
116
116
  if (assistantMessage) {
117
117
  finalConversation.push(assistantMessage);
@@ -129,20 +129,13 @@ function createLlmRetryClient(params) {
129
129
  }
130
130
  catch (error) {
131
131
  if (error instanceof LlmRetryError) {
132
- // This is a recoverable error, so we'll create a detailed attempt error and continue the loop.
133
132
  const conversationForError = [...currentMessages];
134
- // If the error contains the raw response (e.g. the invalid text), add it to history
135
- // so the LLM knows what it generated previously.
136
133
  if (error.rawResponse) {
137
134
  conversationForError.push({ role: 'assistant', content: error.rawResponse });
138
135
  }
139
- else if (responseType === 'raw' && error.details) {
140
- // For raw mode, if we have details, maybe we can infer something, but usually rawResponse is key.
141
- }
142
136
  lastError = new LlmRetryAttemptError(`Attempt ${attempt + 1} failed.`, mode, conversationForError, attempt, { cause: error });
143
137
  }
144
138
  else {
145
- // This is a non-recoverable error (e.g., network, API key), so we re-throw it immediately.
146
139
  throw error;
147
140
  }
148
141
  }
@@ -150,16 +143,16 @@ function createLlmRetryClient(params) {
150
143
  throw new LlmRetryExhaustedError(`Operation failed after ${maxRetries + 1} attempts.`, { cause: lastError });
151
144
  }
152
145
  async function promptRetry(arg1, arg2) {
153
- const options = (0, createLlmClient_js_1.normalizeOptions)(arg1, arg2);
154
- return runPromptLoop(options, 'raw');
146
+ const retryParams = normalizeRetryOptions(arg1, arg2);
147
+ return runPromptLoop(retryParams, 'raw');
155
148
  }
156
149
  async function promptTextRetry(arg1, arg2) {
157
- const options = (0, createLlmClient_js_1.normalizeOptions)(arg1, arg2);
158
- return runPromptLoop(options, 'text');
150
+ const retryParams = normalizeRetryOptions(arg1, arg2);
151
+ return runPromptLoop(retryParams, 'text');
159
152
  }
160
153
  async function promptImageRetry(arg1, arg2) {
161
- const options = (0, createLlmClient_js_1.normalizeOptions)(arg1, arg2);
162
- return runPromptLoop(options, 'image');
154
+ const retryParams = normalizeRetryOptions(arg1, arg2);
155
+ return runPromptLoop(retryParams, 'image');
163
156
  }
164
157
  return { promptRetry, promptTextRetry, promptImageRetry };
165
158
  }
@@ -10,27 +10,27 @@ export declare function createLlm(params: CreateLlmFactoryParams): {
10
10
  };
11
11
  promptJson: <T>(messages: import("openai/resources/index.js").ChatCompletionMessageParam[], schema: Record<string, any>, options?: import("./createJsonSchemaLlmClient.js").JsonSchemaLlmClientOptions) => Promise<T>;
12
12
  promptRetry: {
13
- <T = import("openai/resources/index.js").ChatCompletion>(content: string, options?: Omit<import("./createLlmRetryClient.js").LlmRetryOptions<T>, "messages">): Promise<T>;
14
- <T = import("openai/resources/index.js").ChatCompletion>(options: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
13
+ <T = import("openai/resources/index.js").ChatCompletion>(content: string, options?: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
14
+ <T = import("openai/resources/index.js").ChatCompletion>(options: import("./createLlmClient.js").LlmPromptOptions & import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
15
15
  };
16
16
  promptTextRetry: {
17
- <T = string>(content: string, options?: Omit<import("./createLlmRetryClient.js").LlmRetryOptions<T>, "messages">): Promise<T>;
18
- <T = string>(options: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
17
+ <T = string>(content: string, options?: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
18
+ <T = string>(options: import("./createLlmClient.js").LlmPromptOptions & import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
19
19
  };
20
20
  promptImageRetry: {
21
- <T = Buffer<ArrayBufferLike>>(content: string, options?: Omit<import("./createLlmRetryClient.js").LlmRetryOptions<T>, "messages">): Promise<T>;
22
- <T = Buffer<ArrayBufferLike>>(options: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
21
+ <T = Buffer<ArrayBufferLike>>(content: string, options?: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
22
+ <T = Buffer<ArrayBufferLike>>(options: import("./createLlmClient.js").LlmPromptOptions & import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
23
23
  };
24
24
  prompt: {
25
- (content: string, options?: Omit<import("./createLlmClient.js").LlmPromptOptions, "messages">): Promise<import("openai/resources/index.js").ChatCompletion>;
25
+ (content: string, options?: import("./createLlmClient.js").LlmCommonOptions): Promise<import("openai/resources/index.js").ChatCompletion>;
26
26
  (options: import("./createLlmClient.js").LlmPromptOptions): Promise<import("openai/resources/index.js").ChatCompletion>;
27
27
  };
28
28
  promptText: {
29
- (content: string, options?: Omit<import("./createLlmClient.js").LlmPromptOptions, "messages">): Promise<string>;
29
+ (content: string, options?: import("./createLlmClient.js").LlmCommonOptions): Promise<string>;
30
30
  (options: import("./createLlmClient.js").LlmPromptOptions): Promise<string>;
31
31
  };
32
32
  promptImage: {
33
- (content: string, options?: Omit<import("./createLlmClient.js").LlmPromptOptions, "messages">): Promise<Buffer>;
33
+ (content: string, options?: import("./createLlmClient.js").LlmCommonOptions): Promise<Buffer>;
34
34
  (options: import("./createLlmClient.js").LlmPromptOptions): Promise<Buffer>;
35
35
  };
36
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-fns",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/readme.md CHANGED
@@ -27,6 +27,7 @@ const llm = createLlm({
27
27
  // cache: Cache instance (cache-manager)
28
28
  // queue: PQueue instance for concurrency control
29
29
  // maxConversationChars: number (auto-truncation)
30
+ // defaultRequestOptions: { headers, timeout, signal }
30
31
  });
31
32
  ```
32
33
 
@@ -214,8 +215,14 @@ const res = await llm.prompt({
214
215
 
215
216
  // Library Extensions
216
217
  model: "gpt-4o", // Override default model for this call
217
- ttl: 5000, // Cache this specific call for 5s (in ms)
218
218
  retries: 5, // Retry network errors 5 times
219
+
220
+ // Request-level options (headers, timeout, abort signal)
221
+ requestOptions: {
222
+ headers: { 'X-Cache-Salt': 'v2' }, // Affects cache key
223
+ timeout: 60000,
224
+ signal: abortController.signal
225
+ }
219
226
  });
220
227
  ```
221
228
 
@@ -299,7 +306,6 @@ const gameState = await llm.promptZod(
299
306
  model: "google/gemini-flash-1.5",
300
307
  disableJsonFixer: true, // Turn off the automatic JSON repair agent
301
308
  maxRetries: 0, // Fail immediately on error
302
- ttl: 60000 // Cache result
303
309
  }
304
310
  );
305
311
  ```