@llumiverse/core 1.0.0-dev.20260224.234313Z → 1.0.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/lib/cjs/CompletionStream.js +31 -9
- package/lib/cjs/CompletionStream.js.map +1 -1
- package/lib/cjs/Driver.js +105 -6
- package/lib/cjs/Driver.js.map +1 -1
- package/lib/cjs/conversation-utils.js +59 -0
- package/lib/cjs/conversation-utils.js.map +1 -1
- package/lib/cjs/index.js +2 -2
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/validation.js +1 -1
- package/lib/cjs/validation.js.map +1 -1
- package/lib/esm/CompletionStream.js +31 -9
- package/lib/esm/CompletionStream.js.map +1 -1
- package/lib/esm/Driver.js +105 -6
- package/lib/esm/Driver.js.map +1 -1
- package/lib/esm/conversation-utils.js +58 -0
- package/lib/esm/conversation-utils.js.map +1 -1
- package/lib/esm/index.js +2 -2
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/validation.js +1 -1
- package/lib/esm/validation.js.map +1 -1
- package/lib/types/CompletionStream.d.ts.map +1 -1
- package/lib/types/Driver.d.ts +27 -2
- package/lib/types/Driver.d.ts.map +1 -1
- package/lib/types/conversation-utils.d.ts +15 -0
- package/lib/types/conversation-utils.d.ts.map +1 -1
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/validation.d.ts +1 -1
- package/package.json +3 -3
- package/src/CompletionStream.ts +39 -10
- package/src/Driver.error.test.ts +261 -0
- package/src/Driver.ts +121 -10
- package/src/conversation-utils.ts +66 -0
- package/src/index.ts +2 -2
- package/src/validation.ts +2 -2
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AIModel,
|
|
3
|
+
Completion,
|
|
4
|
+
CompletionChunkObject,
|
|
5
|
+
DriverOptions,
|
|
6
|
+
EmbeddingsOptions,
|
|
7
|
+
EmbeddingsResult,
|
|
8
|
+
ExecutionOptions,
|
|
9
|
+
LlumiverseErrorContext,
|
|
10
|
+
ModelSearchPayload,
|
|
11
|
+
LlumiverseError,
|
|
12
|
+
} from '@llumiverse/common';
|
|
13
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
14
|
+
import { AbstractDriver } from './Driver.js';
|
|
15
|
+
|
|
16
|
+
// Simple test driver implementation
|
|
17
|
+
class TestDriver extends AbstractDriver<DriverOptions, string> {
|
|
18
|
+
provider = 'test-provider';
|
|
19
|
+
|
|
20
|
+
async requestTextCompletion(_prompt: string, _options: ExecutionOptions): Promise<Completion> {
|
|
21
|
+
throw new Error('Not implemented');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async requestTextCompletionStream(_prompt: string, _options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>> {
|
|
25
|
+
throw new Error('Not implemented');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async listModels(_params?: ModelSearchPayload): Promise<AIModel[]> {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async validateConnection(): Promise<boolean> {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async generateEmbeddings(_options: EmbeddingsOptions): Promise<EmbeddingsResult> {
|
|
37
|
+
throw new Error('Not implemented');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('AbstractDriver Error Formatting', () => {
|
|
42
|
+
let driver: TestDriver;
|
|
43
|
+
const mockContext: LlumiverseErrorContext = {
|
|
44
|
+
provider: 'test-provider',
|
|
45
|
+
model: 'test-model',
|
|
46
|
+
operation: 'execute',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
driver = new TestDriver({});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('isRetryableError', () => {
|
|
54
|
+
describe('HTTP status codes', () => {
|
|
55
|
+
it('should mark 429 as retryable (rate limit)', () => {
|
|
56
|
+
expect(driver['isRetryableError'](429, 'Rate limit exceeded')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should mark 408 as retryable (timeout)', () => {
|
|
60
|
+
expect(driver['isRetryableError'](408, 'Request timeout')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should mark 529 as retryable (overloaded)', () => {
|
|
64
|
+
expect(driver['isRetryableError'](529, 'Service overloaded')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should mark 5xx as retryable (server errors)', () => {
|
|
68
|
+
expect(driver['isRetryableError'](500, 'Internal server error')).toBe(true);
|
|
69
|
+
expect(driver['isRetryableError'](502, 'Bad gateway')).toBe(true);
|
|
70
|
+
expect(driver['isRetryableError'](503, 'Service unavailable')).toBe(true);
|
|
71
|
+
expect(driver['isRetryableError'](504, 'Gateway timeout')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should mark 4xx as not retryable (except 429, 408)', () => {
|
|
75
|
+
expect(driver['isRetryableError'](400, 'Bad request')).toBe(false);
|
|
76
|
+
expect(driver['isRetryableError'](401, 'Unauthorized')).toBe(false);
|
|
77
|
+
expect(driver['isRetryableError'](403, 'Forbidden')).toBe(false);
|
|
78
|
+
expect(driver['isRetryableError'](404, 'Not found')).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should mark 2xx and 3xx as not retryable', () => {
|
|
82
|
+
expect(driver['isRetryableError'](200, 'OK')).toBe(false);
|
|
83
|
+
expect(driver['isRetryableError'](301, 'Moved permanently')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('message-based detection', () => {
|
|
88
|
+
it('should detect rate limit in message', () => {
|
|
89
|
+
expect(driver['isRetryableError'](undefined, 'Rate limit exceeded')).toBe(true);
|
|
90
|
+
expect(driver['isRetryableError'](undefined, 'You have hit the rate limit')).toBe(true);
|
|
91
|
+
expect(driver['isRetryableError'](undefined, 'RATE_LIMIT_EXCEEDED')).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should detect timeout in message', () => {
|
|
95
|
+
expect(driver['isRetryableError'](undefined, 'Request timeout')).toBe(true);
|
|
96
|
+
expect(driver['isRetryableError'](undefined, 'Connection timed out')).toBe(true);
|
|
97
|
+
expect(driver['isRetryableError'](undefined, 'TIMEOUT_ERROR')).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should detect retry in message', () => {
|
|
101
|
+
expect(driver['isRetryableError'](undefined, 'Please retry later')).toBe(true);
|
|
102
|
+
expect(driver['isRetryableError'](undefined, 'Retry the request')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should detect overload in message', () => {
|
|
106
|
+
expect(driver['isRetryableError'](undefined, 'Service overloaded')).toBe(true);
|
|
107
|
+
expect(driver['isRetryableError'](undefined, 'Server is overload')).toBe(true);
|
|
108
|
+
expect(driver['isRetryableError'](undefined, 'System overloaded')).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should detect resource exhausted in message', () => {
|
|
112
|
+
expect(driver['isRetryableError'](undefined, 'Resource exhausted')).toBe(true);
|
|
113
|
+
expect(driver['isRetryableError'](undefined, 'Resources exhausted')).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should detect throttle in message', () => {
|
|
117
|
+
expect(driver['isRetryableError'](undefined, 'Request throttled')).toBe(true);
|
|
118
|
+
expect(driver['isRetryableError'](undefined, 'Throttling exception')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should detect status codes in message', () => {
|
|
122
|
+
expect(driver['isRetryableError'](undefined, 'Error 429: Too many requests')).toBe(true);
|
|
123
|
+
expect(driver['isRetryableError'](undefined, 'HTTP 529 error')).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should mark unknown messages as undefined (let consumer decide)', () => {
|
|
127
|
+
expect(driver['isRetryableError'](undefined, 'Invalid API key')).toBeUndefined();
|
|
128
|
+
expect(driver['isRetryableError'](undefined, 'Bad request')).toBeUndefined();
|
|
129
|
+
expect(driver['isRetryableError'](undefined, 'Model not found')).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('formatLlumiverseError', () => {
|
|
135
|
+
it('should format error with status code', () => {
|
|
136
|
+
const originalError = new Error('Rate limit exceeded');
|
|
137
|
+
(originalError as any).status = 429;
|
|
138
|
+
|
|
139
|
+
const formatted = driver['formatLlumiverseError'](originalError, mockContext);
|
|
140
|
+
|
|
141
|
+
expect(formatted).toBeInstanceOf(LlumiverseError);
|
|
142
|
+
expect(formatted.code).toBe(429);
|
|
143
|
+
expect(formatted.retryable).toBe(true);
|
|
144
|
+
expect(formatted.message).toContain('[test-provider]');
|
|
145
|
+
expect(formatted.message).toContain('Rate limit exceeded');
|
|
146
|
+
expect(formatted.context).toEqual(mockContext);
|
|
147
|
+
expect(formatted.originalError).toBe(originalError);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should extract status from statusCode property', () => {
|
|
151
|
+
const originalError = new Error('Server error');
|
|
152
|
+
(originalError as any).statusCode = 500;
|
|
153
|
+
|
|
154
|
+
const formatted = driver['formatLlumiverseError'](originalError, mockContext);
|
|
155
|
+
|
|
156
|
+
expect(formatted.code).toBe(500);
|
|
157
|
+
expect(formatted.retryable).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should extract status from code property', () => {
|
|
161
|
+
const originalError = new Error('Timeout');
|
|
162
|
+
(originalError as any).code = 408;
|
|
163
|
+
|
|
164
|
+
const formatted = driver['formatLlumiverseError'](originalError, mockContext);
|
|
165
|
+
|
|
166
|
+
expect(formatted.code).toBe(408);
|
|
167
|
+
expect(formatted.retryable).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should use undefined when no status code found', () => {
|
|
171
|
+
const originalError = new Error('Generic error');
|
|
172
|
+
|
|
173
|
+
const formatted = driver['formatLlumiverseError'](originalError, mockContext);
|
|
174
|
+
|
|
175
|
+
expect(formatted.code).toBeUndefined();
|
|
176
|
+
expect(formatted.retryable).toBeUndefined(); // Unknown retryability
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle non-Error objects', () => {
|
|
180
|
+
const originalError = 'String error message';
|
|
181
|
+
|
|
182
|
+
const formatted = driver['formatLlumiverseError'](originalError, mockContext);
|
|
183
|
+
|
|
184
|
+
expect(formatted.message).toContain('String error message');
|
|
185
|
+
expect(formatted.originalError).toBe(originalError);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should preserve provider in message', () => {
|
|
189
|
+
const error = new Error('Test error');
|
|
190
|
+
|
|
191
|
+
const formatted = driver['formatLlumiverseError'](error, mockContext);
|
|
192
|
+
|
|
193
|
+
expect(formatted.message).toMatch(/^\[test-provider\]/);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should determine retryability based on status and message', () => {
|
|
197
|
+
// Retryable by status
|
|
198
|
+
const retryableError = new Error('Error');
|
|
199
|
+
(retryableError as any).status = 429;
|
|
200
|
+
const formatted1 = driver['formatLlumiverseError'](retryableError, mockContext);
|
|
201
|
+
expect(formatted1.retryable).toBe(true);
|
|
202
|
+
|
|
203
|
+
// Not retryable by status
|
|
204
|
+
const nonRetryableError = new Error('Error');
|
|
205
|
+
(nonRetryableError as any).status = 400;
|
|
206
|
+
const formatted2 = driver['formatLlumiverseError'](nonRetryableError, mockContext);
|
|
207
|
+
expect(formatted2.retryable).toBe(false);
|
|
208
|
+
|
|
209
|
+
// Retryable by message
|
|
210
|
+
const messageRetryable = new Error('Rate limit exceeded');
|
|
211
|
+
const formatted3 = driver['formatLlumiverseError'](messageRetryable, mockContext);
|
|
212
|
+
expect(formatted3.retryable).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('driver override capability', () => {
|
|
217
|
+
class CustomDriver extends TestDriver {
|
|
218
|
+
public formatLlumiverseError(
|
|
219
|
+
error: unknown,
|
|
220
|
+
context: LlumiverseErrorContext
|
|
221
|
+
): LlumiverseError {
|
|
222
|
+
// Custom logic: check for specific error type
|
|
223
|
+
if ((error as any).type === 'custom_retryable') {
|
|
224
|
+
return new LlumiverseError(
|
|
225
|
+
`[${this.provider}] Custom retryable error`,
|
|
226
|
+
true,
|
|
227
|
+
context,
|
|
228
|
+
error,
|
|
229
|
+
undefined,
|
|
230
|
+
'CUSTOM_ERROR'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
// Fall back to default
|
|
234
|
+
return super.formatLlumiverseError(error, context);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
it('should allow drivers to override error formatting', () => {
|
|
239
|
+
const customDriver = new CustomDriver({});
|
|
240
|
+
const customError = { type: 'custom_retryable', message: 'Custom error' };
|
|
241
|
+
|
|
242
|
+
const formatted = customDriver['formatLlumiverseError'](customError, mockContext);
|
|
243
|
+
|
|
244
|
+
expect(formatted.name).toBe('CUSTOM_ERROR');
|
|
245
|
+
expect(formatted.code).toBeUndefined();
|
|
246
|
+
expect(formatted.retryable).toBe(true);
|
|
247
|
+
expect(formatted.message).toContain('Custom retryable error');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should fall back to default for non-custom errors', () => {
|
|
251
|
+
const customDriver = new CustomDriver({});
|
|
252
|
+
const regularError = new Error('Regular error');
|
|
253
|
+
(regularError as any).status = 500;
|
|
254
|
+
|
|
255
|
+
const formatted = customDriver['formatLlumiverseError'](regularError, mockContext);
|
|
256
|
+
|
|
257
|
+
expect(formatted.code).toBe(500);
|
|
258
|
+
expect(formatted.retryable).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
package/src/Driver.ts
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
* (eg: OpenAI, HuggingFace, etc.)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { DefaultCompletionStream, FallbackCompletionStream } from "./CompletionStream.js";
|
|
8
|
-
import { formatTextPrompt } from "./formatters/index.js";
|
|
9
7
|
import {
|
|
10
8
|
AIModel,
|
|
11
9
|
Completion,
|
|
@@ -17,15 +15,19 @@ import {
|
|
|
17
15
|
EmbeddingsResult,
|
|
18
16
|
ExecutionOptions,
|
|
19
17
|
ExecutionResponse,
|
|
18
|
+
LlumiverseErrorContext,
|
|
20
19
|
Logger,
|
|
21
|
-
Modalities,
|
|
22
20
|
ModelSearchPayload,
|
|
23
21
|
PromptOptions,
|
|
24
22
|
PromptSegment,
|
|
23
|
+
Providers,
|
|
25
24
|
TrainingJob,
|
|
26
25
|
TrainingOptions,
|
|
27
|
-
TrainingPromptOptions
|
|
26
|
+
TrainingPromptOptions,
|
|
27
|
+
LlumiverseError
|
|
28
28
|
} from "@llumiverse/common";
|
|
29
|
+
import { DefaultCompletionStream, FallbackCompletionStream } from "./CompletionStream.js";
|
|
30
|
+
import { formatTextPrompt } from "./formatters/index.js";
|
|
29
31
|
import { validateResult } from "./validation.js";
|
|
30
32
|
|
|
31
33
|
// Helper to create logger methods that support both message-only and object-first signatures
|
|
@@ -119,7 +121,7 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
|
|
|
119
121
|
options: OptionsT;
|
|
120
122
|
logger: Logger;
|
|
121
123
|
|
|
122
|
-
abstract provider: string; // the provider name
|
|
124
|
+
abstract provider: Providers | string; // the provider name
|
|
123
125
|
|
|
124
126
|
constructor(opts: OptionsT) {
|
|
125
127
|
this.options = opts;
|
|
@@ -165,8 +167,15 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
|
|
|
165
167
|
async execute(segments: PromptSegment[], options: ExecutionOptions): Promise<ExecutionResponse<PromptT>> {
|
|
166
168
|
const prompt = await this.createPrompt(segments, options);
|
|
167
169
|
return this._execute(prompt, options).catch((error: any) => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
// Don't wrap if already a LlumiverseError
|
|
171
|
+
if (LlumiverseError.isLlumiverseError(error)) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
throw this.formatLlumiverseError(error, {
|
|
175
|
+
provider: this.provider,
|
|
176
|
+
model: options.model,
|
|
177
|
+
operation: 'execute',
|
|
178
|
+
});
|
|
170
179
|
});
|
|
171
180
|
}
|
|
172
181
|
|
|
@@ -189,8 +198,17 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
|
|
|
189
198
|
const execution_time = Date.now() - start;
|
|
190
199
|
return { ...result, prompt, execution_time };
|
|
191
200
|
} catch (error) {
|
|
192
|
-
|
|
193
|
-
|
|
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',
|
|
211
|
+
});
|
|
194
212
|
}
|
|
195
213
|
}
|
|
196
214
|
|
|
@@ -200,9 +218,10 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
|
|
|
200
218
|
|
|
201
219
|
// by default no stream is supported. we block and we return all at once
|
|
202
220
|
async stream(segments: PromptSegment[], options: ExecutionOptions): Promise<CompletionStream<PromptT>> {
|
|
221
|
+
this.logger.info(options, `Executing prompt with provider ${this.provider} with options: ${JSON.stringify(options)}`);
|
|
203
222
|
const prompt = await this.createPrompt(segments, options);
|
|
204
223
|
const canStream = await this.canStream(options);
|
|
205
|
-
if (
|
|
224
|
+
if (canStream) {
|
|
206
225
|
return new DefaultCompletionStream(this, prompt, options);
|
|
207
226
|
} else if (this.isImageModel(options.model)) {
|
|
208
227
|
return new FallbackCompletionStream(this, prompt, options);
|
|
@@ -267,6 +286,98 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
|
|
|
267
286
|
return undefined;
|
|
268
287
|
}
|
|
269
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Format an error into LlumiverseError. Override in driver implementations
|
|
291
|
+
* to provide provider-specific error parsing.
|
|
292
|
+
*
|
|
293
|
+
* The default implementation uses common patterns:
|
|
294
|
+
* - Status 429, 408: retryable (rate limit, timeout)
|
|
295
|
+
* - Status 529: retryable (overloaded)
|
|
296
|
+
* - Status 5xx: retryable (server errors)
|
|
297
|
+
* - Status 4xx (except above): not retryable (client errors)
|
|
298
|
+
* - Error messages containing "rate limit", "timeout", etc.: retryable
|
|
299
|
+
*
|
|
300
|
+
* @param error - The error to format
|
|
301
|
+
* @param context - Context about where the error occurred
|
|
302
|
+
* @returns A standardized LlumiverseError
|
|
303
|
+
*/
|
|
304
|
+
public formatLlumiverseError(
|
|
305
|
+
error: unknown,
|
|
306
|
+
context: LlumiverseErrorContext
|
|
307
|
+
): LlumiverseError {
|
|
308
|
+
// Extract status code from common locations (only if numeric)
|
|
309
|
+
let code: number | undefined;
|
|
310
|
+
const rawCode = (error as any)?.status
|
|
311
|
+
|| (error as any)?.statusCode
|
|
312
|
+
|| (error as any)?.code;
|
|
313
|
+
|
|
314
|
+
if (typeof rawCode === 'number') {
|
|
315
|
+
code = rawCode;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Extract error name if available
|
|
319
|
+
const errorName = (error as any)?.name;
|
|
320
|
+
|
|
321
|
+
// Extract message
|
|
322
|
+
const message = error instanceof Error
|
|
323
|
+
? error.message
|
|
324
|
+
: String(error);
|
|
325
|
+
|
|
326
|
+
// Determine retryability
|
|
327
|
+
const retryable = this.isRetryableError(code, message);
|
|
328
|
+
|
|
329
|
+
return new LlumiverseError(
|
|
330
|
+
`[${this.provider}] ${message}`,
|
|
331
|
+
retryable,
|
|
332
|
+
context,
|
|
333
|
+
error,
|
|
334
|
+
code,
|
|
335
|
+
errorName
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Determine if an error is retryable based on status code and message.
|
|
341
|
+
* Can be overridden by drivers for provider-specific logic.
|
|
342
|
+
*
|
|
343
|
+
* @param statusCode - The HTTP status code (if available)
|
|
344
|
+
* @param message - The error message
|
|
345
|
+
* @returns True if retryable, false if not retryable, undefined if unknown
|
|
346
|
+
*/
|
|
347
|
+
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
|
+
const lowerMessage = message.toLowerCase();
|
|
358
|
+
|
|
359
|
+
// Rate limit variations
|
|
360
|
+
if (lowerMessage.includes('rate') && lowerMessage.includes('limit')) return true;
|
|
361
|
+
|
|
362
|
+
// Timeout variations (timeout, timed out, time out)
|
|
363
|
+
if (lowerMessage.includes('timeout')) return true;
|
|
364
|
+
if (lowerMessage.includes('timed') && lowerMessage.includes('out')) return true;
|
|
365
|
+
if (lowerMessage.includes('time') && lowerMessage.includes('out')) return true;
|
|
366
|
+
|
|
367
|
+
// Resource exhausted variations
|
|
368
|
+
if (lowerMessage.includes('resource') && lowerMessage.includes('exhaust')) return true;
|
|
369
|
+
|
|
370
|
+
// Other retryable patterns
|
|
371
|
+
if (lowerMessage.includes('retry')) return true;
|
|
372
|
+
if (lowerMessage.includes('overload')) return true;
|
|
373
|
+
if (lowerMessage.includes('throttl')) return true;
|
|
374
|
+
if (lowerMessage.includes('429')) return true;
|
|
375
|
+
if (lowerMessage.includes('529')) return true;
|
|
376
|
+
|
|
377
|
+
// Unknown errors - let consumer decide retry strategy
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
270
381
|
abstract requestTextCompletion(prompt: PromptT, options: ExecutionOptions): Promise<Completion>;
|
|
271
382
|
|
|
272
383
|
abstract requestTextCompletionStream(prompt: PromptT, options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>>;
|
|
@@ -529,3 +529,69 @@ function truncateLargeTextInternal(obj: unknown, maxChars: number): unknown {
|
|
|
529
529
|
|
|
530
530
|
return obj;
|
|
531
531
|
}
|
|
532
|
+
|
|
533
|
+
const HEARTBEAT_OPEN_TAG = '<heartbeat>';
|
|
534
|
+
const HEARTBEAT_CLOSE_TAG = '</heartbeat>';
|
|
535
|
+
const HEARTBEAT_PLACEHOLDER = '[Heartbeat removed from conversation history]';
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Strip heartbeat status messages from conversation to reduce context bloat.
|
|
539
|
+
*
|
|
540
|
+
* Heartbeat messages are periodic workstream status updates injected by the
|
|
541
|
+
* workstream management system. They are wrapped with `<heartbeat>...</heartbeat>`
|
|
542
|
+
* tags at the point of injection.
|
|
543
|
+
*
|
|
544
|
+
* This function recursively walks the conversation and replaces any string
|
|
545
|
+
* wrapped in heartbeat tags with a short placeholder.
|
|
546
|
+
*
|
|
547
|
+
* @param obj The conversation object to strip heartbeats from
|
|
548
|
+
* @param options Optional settings for turn-based stripping (default keepForTurns: 1)
|
|
549
|
+
* @returns A new object with old heartbeat messages replaced
|
|
550
|
+
*/
|
|
551
|
+
export function stripHeartbeatsFromConversation(obj: unknown, options?: StripOptions): unknown {
|
|
552
|
+
const { keepForTurns = 1 } = options ?? {};
|
|
553
|
+
const currentTurn = options?.currentTurn ?? getConversationMeta(obj).turnNumber;
|
|
554
|
+
|
|
555
|
+
// If keepForTurns is Infinity, never strip
|
|
556
|
+
if (keepForTurns === Infinity) {
|
|
557
|
+
return obj;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Keep heartbeats if we haven't exceeded the turn threshold
|
|
561
|
+
if (keepForTurns > 0 && currentTurn < keepForTurns) {
|
|
562
|
+
return obj;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return stripHeartbeatsInternal(obj);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function stripHeartbeatsInternal(obj: unknown): unknown {
|
|
569
|
+
if (obj === null || obj === undefined) return obj;
|
|
570
|
+
|
|
571
|
+
// Replace heartbeat-tagged strings with placeholder
|
|
572
|
+
if (typeof obj === 'string') {
|
|
573
|
+
if (obj.startsWith(HEARTBEAT_OPEN_TAG) && obj.endsWith(HEARTBEAT_CLOSE_TAG)) {
|
|
574
|
+
return HEARTBEAT_PLACEHOLDER;
|
|
575
|
+
}
|
|
576
|
+
return obj;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (Array.isArray(obj)) {
|
|
580
|
+
return obj.map(item => stripHeartbeatsInternal(item));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (typeof obj === 'object') {
|
|
584
|
+
const result: Record<string, unknown> = {};
|
|
585
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
586
|
+
// Preserve metadata
|
|
587
|
+
if (key === META_KEY) {
|
|
588
|
+
result[key] = value;
|
|
589
|
+
} else {
|
|
590
|
+
result[key] = stripHeartbeatsInternal(value);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return obj;
|
|
597
|
+
}
|
package/src/index.ts
CHANGED
package/src/validation.ts
CHANGED
|
@@ -47,7 +47,7 @@ function parseCompletionAsJson(data: CompletionResult[]) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
export function validateResult(data: CompletionResult[], schema:
|
|
50
|
+
export function validateResult(data: CompletionResult[], schema: object): CompletionResult[] {
|
|
51
51
|
let json;
|
|
52
52
|
if (Array.isArray(data)) {
|
|
53
53
|
const jsonResults = data.filter(r => r.type === "json");
|
|
@@ -68,7 +68,7 @@ export function validateResult(data: CompletionResult[], schema: Object): Comple
|
|
|
68
68
|
const valid = validate(json);
|
|
69
69
|
|
|
70
70
|
if (!valid && validate.errors) {
|
|
71
|
-
|
|
71
|
+
const errors = [];
|
|
72
72
|
|
|
73
73
|
for (const e of validate.errors) {
|
|
74
74
|
const path = e.instancePath.split("/").slice(1);
|