@llumiverse/core 1.3.0 → 1.4.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.
Files changed (145) hide show
  1. package/lib/CompletionStream.d.ts +48 -0
  2. package/lib/CompletionStream.d.ts.map +1 -0
  3. package/lib/{esm/CompletionStream.js → CompletionStream.js} +142 -67
  4. package/lib/CompletionStream.js.map +1 -0
  5. package/lib/{types/Driver.d.ts → Driver.d.ts} +38 -5
  6. package/lib/Driver.d.ts.map +1 -0
  7. package/lib/{esm/Driver.js → Driver.js} +151 -106
  8. package/lib/Driver.js.map +1 -0
  9. package/lib/{types/async.d.ts → async.d.ts} +3 -3
  10. package/lib/async.d.ts.map +1 -0
  11. package/lib/{esm/async.js → async.js} +4 -4
  12. package/lib/async.js.map +1 -0
  13. package/lib/{types/conversation-utils.d.ts → conversation-utils.d.ts} +9 -0
  14. package/lib/conversation-utils.d.ts.map +1 -0
  15. package/lib/{esm/conversation-utils.js → conversation-utils.js} +78 -15
  16. package/lib/conversation-utils.js.map +1 -0
  17. package/lib/embeddings.d.ts +58 -0
  18. package/lib/embeddings.d.ts.map +1 -0
  19. package/lib/embeddings.js +131 -0
  20. package/lib/embeddings.js.map +1 -0
  21. package/lib/formatters/commons.d.ts +3 -0
  22. package/lib/formatters/commons.d.ts.map +1 -0
  23. package/lib/formatters/commons.js +4 -0
  24. package/lib/formatters/commons.js.map +1 -0
  25. package/lib/{types/formatters → formatters}/generic.d.ts +2 -2
  26. package/lib/formatters/generic.d.ts.map +1 -0
  27. package/lib/{esm/formatters → formatters}/generic.js +8 -8
  28. package/lib/formatters/generic.js.map +1 -0
  29. package/lib/formatters/index.d.ts +4 -0
  30. package/lib/formatters/index.d.ts.map +1 -0
  31. package/lib/formatters/index.js +4 -0
  32. package/lib/formatters/index.js.map +1 -0
  33. package/lib/{types/formatters → formatters}/nova.d.ts +4 -4
  34. package/lib/formatters/nova.d.ts.map +1 -0
  35. package/lib/{esm/formatters → formatters}/nova.js +19 -17
  36. package/lib/formatters/nova.js.map +1 -0
  37. package/lib/http-agent.d.ts +53 -0
  38. package/lib/http-agent.d.ts.map +1 -0
  39. package/lib/http-agent.js +141 -0
  40. package/lib/http-agent.js.map +1 -0
  41. package/lib/index.d.ts +8 -0
  42. package/lib/index.d.ts.map +1 -0
  43. package/lib/index.js +7 -0
  44. package/lib/index.js.map +1 -0
  45. package/lib/{types/json.d.ts → json.d.ts} +1 -1
  46. package/lib/json.d.ts.map +1 -0
  47. package/lib/{esm/json.js → json.js} +4 -3
  48. package/lib/json.js.map +1 -0
  49. package/lib/logger.d.ts +3 -0
  50. package/lib/logger.d.ts.map +1 -0
  51. package/lib/logger.js +42 -0
  52. package/lib/logger.js.map +1 -0
  53. package/lib/resolver.d.ts +2 -0
  54. package/lib/resolver.d.ts.map +1 -0
  55. package/lib/{esm/resolver.js → resolver.js} +7 -4
  56. package/lib/resolver.js.map +1 -0
  57. package/lib/stream.d.ts.map +1 -0
  58. package/lib/stream.js.map +1 -0
  59. package/lib/{types/validation.d.ts → validation.d.ts} +1 -1
  60. package/lib/validation.d.ts.map +1 -0
  61. package/lib/validation.js +100 -0
  62. package/lib/validation.js.map +1 -0
  63. package/package.json +28 -38
  64. package/src/CompletionStream.ts +208 -115
  65. package/src/Driver.error.test.ts +101 -72
  66. package/src/Driver.ts +208 -152
  67. package/src/async.ts +30 -30
  68. package/src/conversation-utils.ts +92 -16
  69. package/src/embeddings.ts +157 -0
  70. package/src/formatters/commons.ts +2 -2
  71. package/src/formatters/generic.ts +20 -17
  72. package/src/formatters/index.ts +3 -3
  73. package/src/formatters/nova.ts +53 -51
  74. package/src/http-agent.test.ts +281 -0
  75. package/src/http-agent.ts +177 -0
  76. package/src/index.ts +7 -5
  77. package/src/json.ts +6 -5
  78. package/src/logger.ts +42 -0
  79. package/src/resolver.ts +12 -9
  80. package/src/stream.ts +4 -5
  81. package/src/validation.ts +42 -29
  82. package/lib/cjs/CompletionStream.js +0 -293
  83. package/lib/cjs/CompletionStream.js.map +0 -1
  84. package/lib/cjs/Driver.js +0 -300
  85. package/lib/cjs/Driver.js.map +0 -1
  86. package/lib/cjs/async.js +0 -131
  87. package/lib/cjs/async.js.map +0 -1
  88. package/lib/cjs/conversation-utils.js +0 -579
  89. package/lib/cjs/conversation-utils.js.map +0 -1
  90. package/lib/cjs/formatters/commons.js +0 -7
  91. package/lib/cjs/formatters/commons.js.map +0 -1
  92. package/lib/cjs/formatters/generic.js +0 -59
  93. package/lib/cjs/formatters/generic.js.map +0 -1
  94. package/lib/cjs/formatters/index.js +0 -20
  95. package/lib/cjs/formatters/index.js.map +0 -1
  96. package/lib/cjs/formatters/nova.js +0 -99
  97. package/lib/cjs/formatters/nova.js.map +0 -1
  98. package/lib/cjs/index.js +0 -22
  99. package/lib/cjs/index.js.map +0 -1
  100. package/lib/cjs/json.js +0 -29
  101. package/lib/cjs/json.js.map +0 -1
  102. package/lib/cjs/package.json +0 -3
  103. package/lib/cjs/resolver.js +0 -45
  104. package/lib/cjs/resolver.js.map +0 -1
  105. package/lib/cjs/stream.js +0 -30
  106. package/lib/cjs/stream.js.map +0 -1
  107. package/lib/cjs/validation.js +0 -96
  108. package/lib/cjs/validation.js.map +0 -1
  109. package/lib/esm/CompletionStream.js.map +0 -1
  110. package/lib/esm/Driver.js.map +0 -1
  111. package/lib/esm/async.js.map +0 -1
  112. package/lib/esm/conversation-utils.js.map +0 -1
  113. package/lib/esm/formatters/commons.js +0 -4
  114. package/lib/esm/formatters/commons.js.map +0 -1
  115. package/lib/esm/formatters/generic.js.map +0 -1
  116. package/lib/esm/formatters/index.js +0 -4
  117. package/lib/esm/formatters/index.js.map +0 -1
  118. package/lib/esm/formatters/nova.js.map +0 -1
  119. package/lib/esm/index.js +0 -6
  120. package/lib/esm/index.js.map +0 -1
  121. package/lib/esm/json.js.map +0 -1
  122. package/lib/esm/resolver.js.map +0 -1
  123. package/lib/esm/stream.js.map +0 -1
  124. package/lib/esm/validation.js +0 -88
  125. package/lib/esm/validation.js.map +0 -1
  126. package/lib/types/CompletionStream.d.ts +0 -20
  127. package/lib/types/CompletionStream.d.ts.map +0 -1
  128. package/lib/types/Driver.d.ts.map +0 -1
  129. package/lib/types/async.d.ts.map +0 -1
  130. package/lib/types/conversation-utils.d.ts.map +0 -1
  131. package/lib/types/formatters/commons.d.ts +0 -3
  132. package/lib/types/formatters/commons.d.ts.map +0 -1
  133. package/lib/types/formatters/generic.d.ts.map +0 -1
  134. package/lib/types/formatters/index.d.ts +0 -4
  135. package/lib/types/formatters/index.d.ts.map +0 -1
  136. package/lib/types/formatters/nova.d.ts.map +0 -1
  137. package/lib/types/index.d.ts +0 -6
  138. package/lib/types/index.d.ts.map +0 -1
  139. package/lib/types/json.d.ts.map +0 -1
  140. package/lib/types/resolver.d.ts +0 -2
  141. package/lib/types/resolver.d.ts.map +0 -1
  142. package/lib/types/stream.d.ts.map +0 -1
  143. package/lib/types/validation.d.ts.map +0 -1
  144. /package/lib/{types/stream.d.ts → stream.d.ts} +0 -0
  145. /package/lib/{esm/stream.js → stream.js} +0 -0
package/src/Driver.ts CHANGED
@@ -5,74 +5,49 @@
5
5
  */
6
6
 
7
7
  import {
8
- AIModel,
9
- Completion,
10
- CompletionChunkObject,
11
- CompletionStream,
12
- DataSource,
13
- DriverOptions,
14
- EmbeddingsOptions,
15
- EmbeddingsResult,
16
- ExecutionOptions,
17
- ExecutionResponse,
18
- LlumiverseErrorContext,
19
- Logger,
20
- ModelSearchPayload,
21
- PromptOptions,
22
- PromptSegment,
23
- Providers,
24
- TrainingJob,
25
- TrainingOptions,
26
- TrainingPromptOptions,
27
- LlumiverseError
28
- } from "@llumiverse/common";
29
- import { DefaultCompletionStream, FallbackCompletionStream } from "./CompletionStream.js";
30
- import { formatTextPrompt } from "./formatters/index.js";
31
- import { validateResult } from "./validation.js";
32
-
33
- // Helper to create logger methods that support both message-only and object-first signatures
34
- function createConsoleLoggerMethod(consoleMethod: (...args: unknown[]) => void): Logger['info'] {
35
- return ((objOrMsg: any, msgOrNever?: any, ...args: (string | number | boolean)[]) => {
36
- if (typeof objOrMsg === 'string') {
37
- // Message-only: logger.info("message", ...args)
38
- consoleMethod(objOrMsg, msgOrNever, ...args);
39
- } else if (msgOrNever !== undefined) {
40
- // Object-first: logger.info({ obj }, "message", ...args)
41
- consoleMethod(msgOrNever, objOrMsg, ...args);
42
- } else {
43
- // Object-only: logger.info({ obj })
44
- consoleMethod(objOrMsg, ...args);
45
- }
46
- }) as Logger['info'];
47
- }
48
-
49
- const ConsoleLogger: Logger = {
50
- debug: createConsoleLoggerMethod(console.debug.bind(console)),
51
- info: createConsoleLoggerMethod(console.info.bind(console)),
52
- warn: createConsoleLoggerMethod(console.warn.bind(console)),
53
- error: createConsoleLoggerMethod(console.error.bind(console)),
54
- }
55
-
56
- const noop = () => void 0;
57
- const NoopLogger: Logger = {
58
- debug: noop as Logger['debug'],
59
- info: noop as Logger['info'],
60
- warn: noop as Logger['warn'],
61
- error: noop as Logger['error'],
62
- }
63
-
64
- export function createLogger(logger: Logger | "console" | undefined) {
65
- if (logger === "console") {
66
- return ConsoleLogger;
67
- } else if (logger) {
68
- return logger;
69
- } else {
70
- return NoopLogger;
8
+ type AIModel,
9
+ type Completion,
10
+ type CompletionChunkObject,
11
+ type CompletionStream,
12
+ type DataSource,
13
+ type DriverOptions,
14
+ type EmbeddingsOptions,
15
+ type EmbeddingsResult,
16
+ type ExecutionOptions,
17
+ type ExecutionResponse,
18
+ LlumiverseError,
19
+ type LlumiverseErrorContext,
20
+ type Logger,
21
+ type ModelSearchPayload,
22
+ type PromptOptions,
23
+ type PromptSegment,
24
+ type Providers,
25
+ type TrainingJob,
26
+ type TrainingOptions,
27
+ type TrainingPromptOptions,
28
+ } from '@llumiverse/common';
29
+ import type { Agent } from 'undici';
30
+ import { DefaultCompletionStream, FallbackCompletionStream } from './CompletionStream.js';
31
+ import { formatTextPrompt } from './formatters/index.js';
32
+ import {
33
+ createAgentBackedFetch,
34
+ createDriverHttpAgent,
35
+ createDriverHttpAgentScope,
36
+ type DriverHttpAgentScope,
37
+ } from './http-agent.js';
38
+ import { createLogger } from './logger.js';
39
+ import { validateResult } from './validation.js';
40
+
41
+ export { createLogger } from './logger.js';
42
+
43
+ function getObjectProperty(value: unknown, key: string): unknown {
44
+ if (value && typeof value === 'object' && key in value) {
45
+ return (value as Record<string, unknown>)[key];
71
46
  }
47
+ return undefined;
72
48
  }
73
49
 
74
50
  export interface Driver<PromptT = unknown> {
75
-
76
51
  /**
77
52
  *
78
53
  * @param segments
@@ -104,7 +79,13 @@ export interface Driver<PromptT = unknown> {
104
79
  //check that it is possible to connect to the environment
105
80
  validateConnection(): Promise<boolean>;
106
81
 
107
- //generate embeddings for a given text or image
82
+ /**
83
+ * Generate embeddings for one or more inputs.
84
+ * Inputs may be text, image, video, or audio depending on the model and
85
+ * provider. Returns one result item per input, each with one or more
86
+ * output vectors (single-vector for text/image, multi-vector for
87
+ * segmented video/audio or joint-multimodal models).
88
+ */
108
89
  generateEmbeddings(options: EmbeddingsOptions): Promise<EmbeddingsResult>;
109
90
 
110
91
  /**
@@ -117,56 +98,102 @@ export interface Driver<PromptT = unknown> {
117
98
  /**
118
99
  * To be implemented by each driver
119
100
  */
120
- export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOptions, PromptT = unknown> implements Driver<PromptT> {
101
+ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOptions, PromptT = unknown>
102
+ implements Driver<PromptT>
103
+ {
121
104
  options: OptionsT;
122
105
  logger: Logger;
123
106
 
124
107
  abstract provider: Providers | string; // the provider name
125
108
 
109
+ private _httpAgent?: Agent;
110
+ private _driverFetch?: typeof fetch;
111
+
126
112
  constructor(opts: OptionsT) {
127
113
  this.options = opts;
128
114
  this.logger = createLogger(opts.logger);
129
115
  }
130
116
 
117
+ /**
118
+ * Lazily-created undici `Agent` driven by `options.httpTimeout`.
119
+ * Pools sockets for the lifetime of the driver. Subclasses can
120
+ * either pass this directly to an SDK that accepts a `dispatcher`
121
+ * option (rare), or — much more commonly — use {@link getDriverFetch}
122
+ * to get a fetch implementation backed by it.
123
+ *
124
+ * Released via {@link destroy}.
125
+ */
126
+ protected getHttpAgent(): Agent {
127
+ if (!this._httpAgent) {
128
+ this._httpAgent = createDriverHttpAgent(this.options.httpTimeout);
129
+ }
130
+ return this._httpAgent;
131
+ }
132
+
133
+ /**
134
+ * Fetch-compatible function backed by the driver's HTTP agent.
135
+ * Pass to any SDK that accepts a custom `fetch` option (OpenAI,
136
+ * Anthropic, `@google/genai`, Bedrock via Smithy, …) or use as a
137
+ * drop-in replacement for the global `fetch` in drivers that make
138
+ * raw HTTP calls.
139
+ */
140
+ protected getDriverFetch(): typeof fetch {
141
+ if (!this._driverFetch) {
142
+ this._driverFetch = createAgentBackedFetch(this.getHttpAgent());
143
+ }
144
+ return this._driverFetch;
145
+ }
146
+
147
+ public createExecutionHttpAgentScope(options: Pick<ExecutionOptions, 'httpTimeout'>): DriverHttpAgentScope {
148
+ return createDriverHttpAgentScope(this.options.httpTimeout, options.httpTimeout);
149
+ }
150
+
131
151
  async createTrainingPrompt(options: TrainingPromptOptions): Promise<string> {
132
- const prompt = await this.createPrompt(options.segments, { result_schema: options.schema, model: options.model })
152
+ const prompt = await this.createPrompt(options.segments, {
153
+ result_schema: options.schema,
154
+ model: options.model,
155
+ });
133
156
  return JSON.stringify({
134
157
  prompt,
135
- completion: typeof options.completion === 'string' ? options.completion : JSON.stringify(options.completion)
158
+ completion:
159
+ typeof options.completion === 'string' ? options.completion : JSON.stringify(options.completion),
136
160
  });
137
161
  }
138
162
 
139
163
  startTraining(_dataset: DataSource, _options: TrainingOptions): Promise<TrainingJob> {
140
- throw new Error("Method not implemented.");
164
+ throw new Error('Method not implemented.');
141
165
  }
142
166
 
143
167
  cancelTraining(_jobId: string): Promise<TrainingJob> {
144
- throw new Error("Method not implemented.");
168
+ throw new Error('Method not implemented.');
145
169
  }
146
170
 
147
171
  getTrainingJob(_jobId: string): Promise<TrainingJob> {
148
- throw new Error("Method not implemented.");
172
+ throw new Error('Method not implemented.');
149
173
  }
150
174
 
151
175
  validateResult(result: Completion, options: ExecutionOptions) {
152
176
  if (!result.tool_use && !result.error && options.result_schema) {
153
177
  try {
154
178
  result.result = validateResult(result.result, options.result_schema);
155
- } catch (error: any) {
156
- const errorMessage = `[${this.provider}] [${options.model}] ${error.code ? '[' + error.code + '] ' : ''}Result validation error: ${error.message}`;
179
+ } catch (error: unknown) {
180
+ const validationError = error instanceof Error ? error : new Error(String(error));
181
+ const rawCode = getObjectProperty(error, 'code');
182
+ const code = rawCode === 'json_error' || rawCode === 'validation_error' ? rawCode : undefined;
183
+ const errorMessage = `[${this.provider}] [${options.model}] ${code ? `[${code}] ` : ''}Result validation error: ${validationError.message}`;
157
184
  this.logger.error({ err: error, data: result.result }, errorMessage);
158
185
  result.error = {
159
- code: error.code || error.name,
160
- message: error.message,
186
+ code: code || 'validation_error',
187
+ message: validationError.message,
161
188
  data: result.result,
162
- }
189
+ };
163
190
  }
164
191
  }
165
192
  }
166
193
 
167
194
  async execute(segments: PromptSegment[], options: ExecutionOptions): Promise<ExecutionResponse<PromptT>> {
168
195
  const prompt = await this.createPrompt(segments, options);
169
- return this._execute(prompt, options).catch((error: any) => {
196
+ return this._execute(prompt, options).catch((error: unknown) => {
170
197
  // Don't wrap if already a LlumiverseError
171
198
  if (LlumiverseError.isLlumiverseError(error)) {
172
199
  throw error;
@@ -180,45 +207,63 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
180
207
  }
181
208
 
182
209
  async _execute(prompt: PromptT, options: ExecutionOptions): Promise<ExecutionResponse<PromptT>> {
210
+ const httpScope = this.createExecutionHttpAgentScope(options);
183
211
  try {
184
- const start = Date.now();
185
- let result;
186
-
187
- if (this.isImageModel(options.model)) {
188
- this.logger.debug(
189
- `[${this.provider}] Executing prompt on ${options.model}, image pathway.`);
190
- result = await this.requestImageGeneration(prompt, options);
191
- } else {
192
- this.logger.debug(
193
- `[${this.provider}] Executing prompt on ${options.model}, text pathway.`);
194
- result = await this.requestTextCompletion(prompt, options);
195
- this.validateResult(result, options);
196
- }
197
-
198
- const execution_time = Date.now() - start;
199
- return { ...result, prompt, execution_time };
200
- } catch (error) {
201
- // Don't wrap if already a LlumiverseError
202
- if (LlumiverseError.isLlumiverseError(error)) {
203
- throw error;
204
- }
205
- // Log the original error for debugging
206
- this.logger.error({ err: error, data: { provider: this.provider, model: options.model, operation: 'execute', prompt } }, `Error during execution in provider ${this.provider}:`);
207
- throw this.formatLlumiverseError(error, {
208
- provider: this.provider,
209
- model: options.model,
210
- operation: 'execute',
212
+ return await httpScope.run(async () => {
213
+ try {
214
+ const start = Date.now();
215
+ let result: Completion;
216
+
217
+ if (this.isImageModel(options.model)) {
218
+ this.logger.debug(`[${this.provider}] Executing prompt on ${options.model}, image pathway.`);
219
+ result = await this.requestImageGeneration(prompt, options);
220
+ } else {
221
+ this.logger.debug(`[${this.provider}] Executing prompt on ${options.model}, text pathway.`);
222
+ result = await this.requestTextCompletion(prompt, options);
223
+ this.validateResult(result, options);
224
+ }
225
+
226
+ const execution_time = Date.now() - start;
227
+ return { ...result, prompt, execution_time };
228
+ } catch (error) {
229
+ // Don't wrap if already a LlumiverseError
230
+ if (LlumiverseError.isLlumiverseError(error)) {
231
+ throw error;
232
+ }
233
+ // Log the original error for debugging
234
+ this.logger.error(
235
+ {
236
+ err: error,
237
+ data: { provider: this.provider, model: options.model, operation: 'execute', prompt },
238
+ },
239
+ `Error during execution in provider ${this.provider}:`,
240
+ );
241
+ throw this.formatLlumiverseError(error, {
242
+ provider: this.provider,
243
+ model: options.model,
244
+ operation: 'execute',
245
+ });
246
+ }
211
247
  });
248
+ } finally {
249
+ await httpScope.close();
212
250
  }
213
251
  }
214
252
 
253
+ public formatDebugPrompt(prompt: PromptT): PromptT {
254
+ return prompt;
255
+ }
256
+
215
257
  protected isImageModel(_model: string): boolean {
216
258
  return false;
217
259
  }
218
260
 
219
261
  // by default no stream is supported. we block and we return all at once
220
262
  async stream(segments: PromptSegment[], options: ExecutionOptions): Promise<CompletionStream<PromptT>> {
221
- this.logger.debug(options, `Executing prompt with provider ${this.provider} with options: ${JSON.stringify(options)}`);
263
+ this.logger.debug(
264
+ options,
265
+ `Executing prompt with provider ${this.provider} with options: ${JSON.stringify(options)}`,
266
+ );
222
267
  const prompt = await this.createPrompt(segments, options);
223
268
  const canStream = await this.canStream(options);
224
269
  if (canStream) {
@@ -241,7 +286,9 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
241
286
  }
242
287
 
243
288
  public async createPrompt(segments: PromptSegment[], opts: PromptOptions): Promise<PromptT> {
244
- return await (opts.format ? opts.format(segments, opts.result_schema) : this.formatPrompt(segments, opts));
289
+ return await (opts.format
290
+ ? (opts.format(segments, opts.result_schema) as PromptT)
291
+ : this.formatPrompt(segments, opts));
245
292
  }
246
293
 
247
294
  /**
@@ -280,7 +327,7 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
280
327
  _prompt: PromptT,
281
328
  _result: unknown[],
282
329
  _toolUse: unknown[] | undefined,
283
- _options: ExecutionOptions
330
+ _options: ExecutionOptions,
284
331
  ): unknown | undefined {
285
332
  // Default implementation returns undefined - drivers can override
286
333
  return undefined;
@@ -289,90 +336,90 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
289
336
  /**
290
337
  * Format an error into LlumiverseError. Override in driver implementations
291
338
  * to provide provider-specific error parsing.
292
- *
339
+ *
293
340
  * The default implementation uses common patterns:
294
341
  * - Status 429, 408: retryable (rate limit, timeout)
295
342
  * - Status 529: retryable (overloaded)
296
343
  * - Status 5xx: retryable (server errors)
297
- * - Status 4xx (except above): not retryable (client errors)
298
- * - Error messages containing "rate limit", "timeout", etc.: retryable
299
- *
344
+ * - High-confidence transient messages containing "rate limit", "timeout", etc.: retryable
345
+ * - Status 4xx (except above and transient provider quirks): not retryable (client errors)
346
+ *
300
347
  * @param error - The error to format
301
348
  * @param context - Context about where the error occurred
302
349
  * @returns A standardized LlumiverseError
303
350
  */
304
- public formatLlumiverseError(
305
- error: unknown,
306
- context: LlumiverseErrorContext
307
- ): LlumiverseError {
351
+ public formatLlumiverseError(error: unknown, context: LlumiverseErrorContext): LlumiverseError {
308
352
  // Extract status code from common locations (only if numeric)
309
353
  let code: number | undefined;
310
- const rawCode = (error as any)?.status
311
- || (error as any)?.statusCode
312
- || (error as any)?.code;
354
+ const rawCode =
355
+ getObjectProperty(error, 'status') ||
356
+ getObjectProperty(error, 'statusCode') ||
357
+ getObjectProperty(error, 'code');
313
358
 
314
359
  if (typeof rawCode === 'number') {
315
360
  code = rawCode;
316
361
  }
317
362
 
318
363
  // Extract error name if available
319
- const errorName = (error as any)?.name;
364
+ const rawErrorName = getObjectProperty(error, 'name');
365
+ const errorName = typeof rawErrorName === 'string' ? rawErrorName : undefined;
320
366
 
321
367
  // Extract message
322
- const message = error instanceof Error
323
- ? error.message
324
- : String(error);
368
+ const message = error instanceof Error ? error.message : String(error);
325
369
 
326
370
  // Determine retryability
327
371
  const retryable = this.isRetryableError(code, message);
328
372
 
329
- return new LlumiverseError(
330
- `[${this.provider}] ${message}`,
331
- retryable,
332
- context,
333
- error,
334
- code,
335
- errorName
336
- );
373
+ return new LlumiverseError(`[${this.provider}] ${message}`, retryable, context, error, code, errorName);
337
374
  }
338
375
 
339
376
  /**
340
377
  * Determine if an error is retryable based on status code and message.
341
378
  * Can be overridden by drivers for provider-specific logic.
342
- *
379
+ *
343
380
  * @param statusCode - The HTTP status code (if available)
344
381
  * @param message - The error message
345
382
  * @returns True if retryable, false if not retryable, undefined if unknown
346
383
  */
347
384
  protected isRetryableError(statusCode: number | undefined, message: string): boolean | undefined {
348
- // Numeric status codes
349
- if (statusCode !== undefined) {
350
- if (statusCode === 429 || statusCode === 408) return true; // Rate limit, timeout
351
- if (statusCode === 529) return true; // Overloaded
352
- if (statusCode >= 500 && statusCode < 600) return true; // Server errors
353
- return false; // 4xx client errors not retryable
354
- }
355
-
356
- // Message-based detection for non-HTTP errors
357
385
  const lowerMessage = message.toLowerCase();
358
386
 
359
- // Rate limit variations
387
+ // Provider APIs sometimes surface transient failures under misleading
388
+ // client status codes, so high-confidence transient message signals
389
+ // must be honored before the generic 4xx classification below.
390
+ if (lowerMessage.includes('url_rejected-rejected_client_throttled')) return true;
391
+ if (lowerMessage.includes('url_rejected-rejected_rate_limited')) return true;
360
392
  if (lowerMessage.includes('rate') && lowerMessage.includes('limit')) return true;
361
-
362
- // Timeout variations (timeout, timed out, time out)
363
393
  if (lowerMessage.includes('timeout')) return true;
364
394
  if (lowerMessage.includes('timed') && lowerMessage.includes('out')) return true;
365
395
  if (lowerMessage.includes('time') && lowerMessage.includes('out')) return true;
366
-
367
- // Resource exhausted variations
368
396
  if (lowerMessage.includes('resource') && lowerMessage.includes('exhaust')) return true;
369
-
370
- // Other retryable patterns
371
- if (lowerMessage.includes('retry')) return true;
372
397
  if (lowerMessage.includes('overload')) return true;
373
398
  if (lowerMessage.includes('throttl')) return true;
374
399
  if (lowerMessage.includes('429')) return true;
375
400
  if (lowerMessage.includes('529')) return true;
401
+ // A transport-level abort (request-timeout / dropped connection) or a
402
+ // deadline-exceeded is transient and should be retried — even when the provider
403
+ // surfaces it under a misleading 4xx status. A deliberate cancellation is raised
404
+ // as a Temporal CancelledFailure (not an LLM error), so it never reaches here.
405
+ if (lowerMessage.includes('aborted')) return true;
406
+ if (lowerMessage.includes('deadline')) return true;
407
+
408
+ // Explicit auth failures should never be retried even when they arrive without
409
+ // a numeric status code (for example, google-auth-library invalid_grant errors).
410
+ if (lowerMessage.includes('invalid_grant')) return false;
411
+ if (lowerMessage.includes("credential's issuer")) return false;
412
+
413
+ // Numeric status codes
414
+ if (statusCode !== undefined) {
415
+ if (statusCode === 429 || statusCode === 408) return true; // Rate limit, timeout
416
+ if (statusCode === 529) return true; // Overloaded
417
+ if (statusCode >= 500 && statusCode < 600) return true; // Server errors
418
+ return false; // 4xx client errors not retryable
419
+ }
420
+
421
+ // Message-based detection for non-HTTP errors
422
+ if (lowerMessage.includes('retry')) return true;
376
423
 
377
424
  // Unknown errors - let consumer decide retry strategy
378
425
  return undefined;
@@ -380,10 +427,13 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
380
427
 
381
428
  abstract requestTextCompletion(prompt: PromptT, options: ExecutionOptions): Promise<Completion>;
382
429
 
383
- abstract requestTextCompletionStream(prompt: PromptT, options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>>;
430
+ abstract requestTextCompletionStream(
431
+ prompt: PromptT,
432
+ options: ExecutionOptions,
433
+ ): Promise<AsyncIterable<CompletionChunkObject>>;
384
434
 
385
435
  async requestImageGeneration(_prompt: PromptT, _options: ExecutionOptions): Promise<Completion> {
386
- throw new Error("Image generation not implemented.");
436
+ throw new Error('Image generation not implemented.');
387
437
  //Cannot be made abstract, as abstract methods are required in the derived class
388
438
  }
389
439
 
@@ -398,9 +448,15 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
398
448
 
399
449
  /**
400
450
  * Cleanup method called when the driver is evicted from the cache.
401
- * Override this in driver implementations that need to release resources.
451
+ * Releases the lazily-created HTTP agent socket pool. Override this
452
+ * in driver implementations that need to release additional resources
453
+ * — MUST call `super.destroy()` to avoid leaking sockets.
402
454
  */
403
455
  destroy(): void {
404
- // No-op by default
456
+ this._httpAgent?.close().catch(() => {
457
+ /* shutdown best-effort */
458
+ });
459
+ this._httpAgent = undefined;
460
+ this._driverFetch = undefined;
405
461
  }
406
462
  }
package/src/async.ts CHANGED
@@ -1,50 +1,52 @@
1
- import type { ServerSentEvent } from "@vertesia/api-fetch-client"
2
- import { CompletionChunkObject } from "@llumiverse/common";
1
+ import type { CompletionChunkObject } from '@llumiverse/common';
2
+ import type { ServerSentEvent } from '@vertesia/api-fetch-client';
3
3
 
4
4
  export async function* asyncMap<T, R>(asyncIterable: AsyncIterable<T>, callback: (value: T, index: number) => R) {
5
5
  let i = 0;
6
- for await (const val of asyncIterable)
7
- yield callback(val, i++);
6
+ for await (const val of asyncIterable) yield callback(val, i++);
8
7
  }
9
8
 
10
9
  export function oneAsyncIterator<T>(value: T): AsyncIterable<T> {
11
10
  return {
12
11
  async *[Symbol.asyncIterator]() {
13
- yield value
14
- }
15
- }
12
+ yield value;
13
+ },
14
+ };
16
15
  }
17
16
 
18
17
  /**
19
18
  * Given a ReadableStream of server sent events, tran
20
19
  */
21
- export function transformSSEStream(stream: ReadableStream<ServerSentEvent>, transform: (data: string) => CompletionChunkObject): ReadableStream<CompletionChunkObject> & AsyncIterable<CompletionChunkObject> {
20
+ export function transformSSEStream(
21
+ stream: ReadableStream<ServerSentEvent>,
22
+ transform: (data: string) => CompletionChunkObject,
23
+ ): ReadableStream<CompletionChunkObject> & AsyncIterable<CompletionChunkObject> {
22
24
  // on node and bun the ReadableStream is an async iterable
23
- return stream.pipeThrough(new TransformStream<ServerSentEvent, CompletionChunkObject>({
24
- transform(event: ServerSentEvent, controller) {
25
- if (event.type === 'event' && event.data && event.data !== '[DONE]') {
26
- try {
27
- const result = transform(event.data) ?? ''
28
- controller.enqueue(result);
29
- } catch (err) {
30
- // double check for the last event which is not a JSON - at this time togetherai and mistralai returns the string [DONE]
31
- // do nothing - happens if data is not a JSON - the last event data is the [DONE] string
25
+ return stream.pipeThrough(
26
+ new TransformStream<ServerSentEvent, CompletionChunkObject>({
27
+ transform(event: ServerSentEvent, controller) {
28
+ if (event.type === 'event' && event.data && event.data !== '[DONE]') {
29
+ try {
30
+ const result = transform(event.data) ?? '';
31
+ controller.enqueue(result);
32
+ } catch {
33
+ // double check for the last event which is not a JSON - at this time togetherai and mistralai returns the string [DONE]
34
+ // do nothing - happens if data is not a JSON - the last event data is the [DONE] string
35
+ }
32
36
  }
33
- }
34
- }
35
- })) satisfies ReadableStream<CompletionChunkObject> & AsyncIterable<CompletionChunkObject>;
37
+ },
38
+ }),
39
+ ) satisfies ReadableStream<CompletionChunkObject> & AsyncIterable<CompletionChunkObject>;
36
40
  }
37
41
 
38
- export class EventStream<T, ReturnT = any> implements AsyncIterable<T> {
39
-
42
+ export class EventStream<T, ReturnT = unknown> implements AsyncIterable<T> {
40
43
  private queue: T[] = [];
41
44
  private pending?: {
42
- resolve: (result: IteratorResult<T, ReturnT | undefined>) => void,
43
- reject: (err: any) => void
45
+ resolve: (result: IteratorResult<T, ReturnT | undefined>) => void;
46
+ reject: (err: unknown) => void;
44
47
  };
45
48
  private done = false;
46
49
 
47
-
48
50
  push(event: T) {
49
51
  if (this.done) {
50
52
  throw new Error('Cannot push to a closed stream');
@@ -92,13 +94,11 @@ export class EventStream<T, ReturnT = any> implements AsyncIterable<T> {
92
94
  }
93
95
  const _value = await value;
94
96
  return { done: true, value: _value };
95
- }
96
- }
97
+ },
98
+ };
97
99
  }
98
100
  }
99
101
 
100
-
101
-
102
102
  /**
103
103
  * Transform an async iterator by applying a function to each value.
104
104
  * @param originalGenerator
@@ -107,7 +107,7 @@ export class EventStream<T, ReturnT = any> implements AsyncIterable<T> {
107
107
  export async function* transformAsyncIterator<T, V>(
108
108
  originalGenerator: AsyncIterable<T>,
109
109
  transform: (value: T) => V | Promise<V>,
110
- initCallback?: () => V | Promise<V>
110
+ initCallback?: () => V | Promise<V>,
111
111
  ): AsyncIterable<V> {
112
112
  if (initCallback) {
113
113
  yield initCallback();