@outputai/llm 0.11.1-next.da26845.0 → 0.12.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/package.json +7 -3
- package/src/agent.js +7 -2
- package/src/generate.js +1 -0
- package/src/index.d.ts +14 -6
- package/src/utils/wrap.js +35 -2
- package/src/agent.spec.js +0 -595
- package/src/ai_provider.spec.js +0 -217
- package/src/ai_sdk_options.spec.js +0 -390
- package/src/fixtures/image_response_v7_google_vertex.js +0 -72
- package/src/fixtures/image_response_v7_openai.js +0 -77
- package/src/fixtures/models_api_light.json +0 -675
- package/src/fixtures/models_api_v7_subset.json +0 -259
- package/src/fixtures/stream_response_v7_anthropic.js +0 -389
- package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
- package/src/fixtures/stream_response_v7_openai.js +0 -353
- package/src/fixtures/stream_response_v7_perplexity.js +0 -671
- package/src/fixtures/text_response_v7_anthropic.js +0 -359
- package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
- package/src/fixtures/text_response_v7_google_vertex.js +0 -435
- package/src/fixtures/text_response_v7_openai.js +0 -347
- package/src/fixtures/text_response_v7_perplexity.js +0 -764
- package/src/generate.spec.js +0 -525
- package/src/prompt/content.spec.js +0 -208
- package/src/prompt/interpolations.spec.js +0 -109
- package/src/prompt/loader.full.spec.js +0 -309
- package/src/prompt/loader.spec.js +0 -258
- package/src/prompt/markup/attributes.spec.js +0 -132
- package/src/prompt/markup/nodes.spec.js +0 -333
- package/src/prompt/markup/tokenizer.spec.js +0 -149
- package/src/prompt/markup/tokens.spec.js +0 -250
- package/src/prompt/validations.spec.js +0 -812
- package/src/utils/cost.full.spec.js +0 -272
- package/src/utils/cost.spec.js +0 -290
- package/src/utils/error_handler.spec.js +0 -230
- package/src/utils/file.spec.js +0 -89
- package/src/utils/image.spec.js +0 -20
- package/src/utils/legacy_cost_attribute.spec.js +0 -278
- package/src/utils/models.spec.js +0 -119
- package/src/utils/models_pricing.spec.js +0 -162
- package/src/utils/skills.spec.js +0 -168
- package/src/utils/sources.spec.js +0 -122
- package/src/utils/stream.spec.js +0 -55
- package/src/utils/tools.spec.js +0 -167
- package/src/utils/usage.full.spec.js +0 -215
- package/src/utils/usage.spec.js +0 -246
- package/src/utils/wrap.spec.js +0 -467
- package/src/validations.spec.js +0 -631
package/src/utils/wrap.spec.js
DELETED
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import imageResponseFixture from '../fixtures/image_response_v7_openai.js';
|
|
3
|
-
import streamResponseFixture from '../fixtures/stream_response_v7_openai.js';
|
|
4
|
-
import textResponseFixture from '../fixtures/text_response_v7_openai.js';
|
|
5
|
-
|
|
6
|
-
const mocks = vi.hoisted( () => ( {
|
|
7
|
-
extractSources: vi.fn(),
|
|
8
|
-
parseLLMUsage: vi.fn(),
|
|
9
|
-
calculateCosts: vi.fn(),
|
|
10
|
-
convertCostToLegacy: vi.fn(),
|
|
11
|
-
calculateBase64FileSize: vi.fn(),
|
|
12
|
-
mapAiError: vi.fn()
|
|
13
|
-
} ) );
|
|
14
|
-
|
|
15
|
-
vi.mock( './sources.js', () => ( {
|
|
16
|
-
extractSources: mocks.extractSources
|
|
17
|
-
} ) );
|
|
18
|
-
|
|
19
|
-
vi.mock( './usage.js', () => ( {
|
|
20
|
-
parseLLMUsage: mocks.parseLLMUsage
|
|
21
|
-
} ) );
|
|
22
|
-
|
|
23
|
-
vi.mock( './cost.js', () => ( {
|
|
24
|
-
calculateCosts: mocks.calculateCosts
|
|
25
|
-
} ) );
|
|
26
|
-
|
|
27
|
-
vi.mock( './legacy_cost_attribute.js', () => ( {
|
|
28
|
-
convertCostToLegacy: mocks.convertCostToLegacy
|
|
29
|
-
} ) );
|
|
30
|
-
|
|
31
|
-
vi.mock( './image.js', () => ( {
|
|
32
|
-
calculateBase64FileSize: mocks.calculateBase64FileSize
|
|
33
|
-
} ) );
|
|
34
|
-
|
|
35
|
-
vi.mock( './error_handler.js', () => ( {
|
|
36
|
-
mapAiError: mocks.mapAiError
|
|
37
|
-
} ) );
|
|
38
|
-
|
|
39
|
-
vi.mock( '@outputai/core/sdk/runtime', () => ( {
|
|
40
|
-
Tracing: {
|
|
41
|
-
addEventStart: vi.fn(),
|
|
42
|
-
addEventError: vi.fn(),
|
|
43
|
-
addEventAttribute: vi.fn(),
|
|
44
|
-
addEventEnd: vi.fn()
|
|
45
|
-
},
|
|
46
|
-
Event: {
|
|
47
|
-
emit: vi.fn()
|
|
48
|
-
}
|
|
49
|
-
} ) );
|
|
50
|
-
|
|
51
|
-
import { Tracing, Event } from '@outputai/core/sdk/runtime';
|
|
52
|
-
import { wrapGeneration, wrapStream } from './wrap.js';
|
|
53
|
-
|
|
54
|
-
const tracing = vi.mocked( Tracing, true );
|
|
55
|
-
const event = vi.mocked( Event, true );
|
|
56
|
-
|
|
57
|
-
const clone = value => structuredClone( value );
|
|
58
|
-
const textResponse = () => clone( textResponseFixture );
|
|
59
|
-
const streamResponse = () => clone( streamResponseFixture );
|
|
60
|
-
const imageResponse = () => clone( imageResponseFixture );
|
|
61
|
-
|
|
62
|
-
const prompt = {
|
|
63
|
-
name: 'writer@v1',
|
|
64
|
-
config: { provider: 'openai', model: 'test-model' }
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const mockUsage = {
|
|
68
|
-
type: 'llm:generation:usage',
|
|
69
|
-
providerId: 'openai',
|
|
70
|
-
modelId: 'test-model',
|
|
71
|
-
status: 'complete',
|
|
72
|
-
input: 2,
|
|
73
|
-
output: 1,
|
|
74
|
-
total: 3,
|
|
75
|
-
items: []
|
|
76
|
-
};
|
|
77
|
-
const mockCost = { type: 'llm:generation:cost', total: 0.001, items: [] };
|
|
78
|
-
const mockLegacyCost = { type: 'llm:usage', modelId: 'test-model', usage: [], total: 0.001, tokensUsed: 0 };
|
|
79
|
-
const mappedError = new Error( 'mapped' );
|
|
80
|
-
|
|
81
|
-
describe( 'wrapGeneration / wrapStream', () => {
|
|
82
|
-
beforeEach( () => {
|
|
83
|
-
vi.clearAllMocks();
|
|
84
|
-
vi.spyOn( Date, 'now' ).mockReturnValue( 9_000_000_000 );
|
|
85
|
-
mocks.extractSources.mockReturnValue( [] );
|
|
86
|
-
mocks.parseLLMUsage.mockReturnValue( mockUsage );
|
|
87
|
-
mocks.calculateCosts.mockResolvedValue( mockCost );
|
|
88
|
-
mocks.convertCostToLegacy.mockReturnValue( mockLegacyCost );
|
|
89
|
-
mocks.calculateBase64FileSize.mockReturnValue( 1234 );
|
|
90
|
-
mocks.mapAiError.mockReturnValue( mappedError );
|
|
91
|
-
} );
|
|
92
|
-
|
|
93
|
-
afterEach( () => {
|
|
94
|
-
vi.restoreAllMocks();
|
|
95
|
-
} );
|
|
96
|
-
|
|
97
|
-
describe( 'wrapGeneration', () => {
|
|
98
|
-
it( 'starts an llm trace, wraps a text response, and ends with raw usage and sources', async () => {
|
|
99
|
-
const structuredCloneSpy = vi.spyOn( globalThis, 'structuredClone' );
|
|
100
|
-
const response = textResponse();
|
|
101
|
-
const mergedSources = [ { url: 'https://merged.test' } ];
|
|
102
|
-
mocks.extractSources.mockReturnValue( mergedSources );
|
|
103
|
-
|
|
104
|
-
const wrapped = await wrapGeneration( {
|
|
105
|
-
name: 'generateText',
|
|
106
|
-
prompt,
|
|
107
|
-
fn: async () => response
|
|
108
|
-
} );
|
|
109
|
-
|
|
110
|
-
expect( tracing.addEventStart ).toHaveBeenCalledWith( {
|
|
111
|
-
kind: 'llm',
|
|
112
|
-
id: 'generateText-9000000000',
|
|
113
|
-
name: 'generateText',
|
|
114
|
-
details: { prompt }
|
|
115
|
-
} );
|
|
116
|
-
expect( mocks.parseLLMUsage ).toHaveBeenCalledWith( {
|
|
117
|
-
usage: response.usage,
|
|
118
|
-
prompt
|
|
119
|
-
} );
|
|
120
|
-
expect( mocks.calculateCosts ).toHaveBeenCalledWith( mockUsage );
|
|
121
|
-
expect( tracing.addEventAttribute ).toHaveBeenNthCalledWith( 1, {
|
|
122
|
-
eventId: 'generateText-9000000000',
|
|
123
|
-
attribute: mockCost
|
|
124
|
-
} );
|
|
125
|
-
expect( tracing.addEventAttribute ).toHaveBeenNthCalledWith( 2, {
|
|
126
|
-
eventId: 'generateText-9000000000',
|
|
127
|
-
attribute: mockLegacyCost
|
|
128
|
-
} );
|
|
129
|
-
expect( tracing.addEventAttribute ).toHaveBeenNthCalledWith( 3, {
|
|
130
|
-
eventId: 'generateText-9000000000',
|
|
131
|
-
attribute: mockUsage
|
|
132
|
-
} );
|
|
133
|
-
expect( mocks.convertCostToLegacy ).toHaveBeenCalledWith( mockCost );
|
|
134
|
-
expect( event.emit ).toHaveBeenNthCalledWith( 1, 'cost:llm:request', mockLegacyCost );
|
|
135
|
-
expect( event.emit ).toHaveBeenNthCalledWith( 2, 'llm:generation:metering', {
|
|
136
|
-
cost: mockCost,
|
|
137
|
-
usage: mockUsage
|
|
138
|
-
} );
|
|
139
|
-
expect( structuredCloneSpy ).toHaveBeenCalledWith( mockLegacyCost );
|
|
140
|
-
expect( structuredCloneSpy ).toHaveBeenCalledWith( { cost: mockCost, usage: mockUsage } );
|
|
141
|
-
const legacyEventPayload = event.emit.mock.calls[0][1];
|
|
142
|
-
const meteringEventPayload = event.emit.mock.calls[1][1];
|
|
143
|
-
expect( legacyEventPayload ).not.toBe( mockLegacyCost );
|
|
144
|
-
expect( meteringEventPayload.cost ).not.toBe( mockCost );
|
|
145
|
-
expect( meteringEventPayload.usage ).not.toBe( mockUsage );
|
|
146
|
-
expect( mocks.extractSources ).toHaveBeenCalledWith( response );
|
|
147
|
-
expect( tracing.addEventEnd ).toHaveBeenCalledWith( {
|
|
148
|
-
id: 'generateText-9000000000',
|
|
149
|
-
details: {
|
|
150
|
-
result: response.text,
|
|
151
|
-
usage: response.usage,
|
|
152
|
-
providerMetadata: response.finalStep.providerMetadata,
|
|
153
|
-
sources: mergedSources
|
|
154
|
-
}
|
|
155
|
-
} );
|
|
156
|
-
expect( wrapped.result ).toBe( response.text );
|
|
157
|
-
expect( wrapped.cost ).toEqual( mockCost );
|
|
158
|
-
expect( wrapped.sources ).toBe( mergedSources );
|
|
159
|
-
expect( wrapped.text ).toBe( response.text );
|
|
160
|
-
} );
|
|
161
|
-
|
|
162
|
-
it( 'uses usage instead of deprecated totalUsage', async () => {
|
|
163
|
-
const response = {
|
|
164
|
-
text: 'hi',
|
|
165
|
-
usage: { inputTokens: 2 },
|
|
166
|
-
totalUsage: { inputTokens: 99 },
|
|
167
|
-
finalStep: { providerMetadata: undefined },
|
|
168
|
-
sources: []
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
await wrapGeneration( { name: 'generateText', prompt, fn: async () => response } );
|
|
172
|
-
|
|
173
|
-
expect( mocks.parseLLMUsage ).toHaveBeenCalledWith( {
|
|
174
|
-
usage: { inputTokens: 2 },
|
|
175
|
-
prompt
|
|
176
|
-
} );
|
|
177
|
-
expect( mocks.calculateCosts ).toHaveBeenCalledWith( mockUsage );
|
|
178
|
-
} );
|
|
179
|
-
|
|
180
|
-
it( 'wraps an image response using usage and mapped image metadata', async () => {
|
|
181
|
-
const response = imageResponse();
|
|
182
|
-
|
|
183
|
-
const wrapped = await wrapGeneration( {
|
|
184
|
-
name: 'generateImage',
|
|
185
|
-
prompt,
|
|
186
|
-
fn: async () => response
|
|
187
|
-
} );
|
|
188
|
-
|
|
189
|
-
expect( mocks.parseLLMUsage ).toHaveBeenCalledWith( {
|
|
190
|
-
usage: response.usage,
|
|
191
|
-
prompt
|
|
192
|
-
} );
|
|
193
|
-
expect( mocks.calculateCosts ).toHaveBeenCalledWith( mockUsage );
|
|
194
|
-
expect( mocks.calculateBase64FileSize ).toHaveBeenCalledWith( response.images[0].base64Data );
|
|
195
|
-
expect( tracing.addEventEnd ).toHaveBeenCalledWith( {
|
|
196
|
-
id: 'generateImage-9000000000',
|
|
197
|
-
details: {
|
|
198
|
-
result: [ { size: 1234, mediaType: 'image/png' } ],
|
|
199
|
-
usage: response.usage,
|
|
200
|
-
providerMetadata: response.providerMetadata
|
|
201
|
-
}
|
|
202
|
-
} );
|
|
203
|
-
expect( mocks.extractSources ).not.toHaveBeenCalled();
|
|
204
|
-
expect( wrapped.result ).toBe( response.image );
|
|
205
|
-
expect( wrapped.cost ).toEqual( mockCost );
|
|
206
|
-
} );
|
|
207
|
-
|
|
208
|
-
it( 'emits normalized usage when cost is missing', async () => {
|
|
209
|
-
mocks.calculateCosts.mockResolvedValue( null );
|
|
210
|
-
const response = textResponse();
|
|
211
|
-
|
|
212
|
-
const wrapped = await wrapGeneration( {
|
|
213
|
-
name: 'generateText',
|
|
214
|
-
prompt,
|
|
215
|
-
fn: async () => response
|
|
216
|
-
} );
|
|
217
|
-
|
|
218
|
-
expect( tracing.addEventAttribute ).toHaveBeenCalledWith( {
|
|
219
|
-
eventId: 'generateText-9000000000',
|
|
220
|
-
attribute: mockUsage
|
|
221
|
-
} );
|
|
222
|
-
expect( event.emit ).toHaveBeenCalledOnce();
|
|
223
|
-
expect( event.emit ).toHaveBeenCalledWith( 'llm:generation:metering', {
|
|
224
|
-
cost: null,
|
|
225
|
-
usage: mockUsage
|
|
226
|
-
} );
|
|
227
|
-
expect( tracing.addEventEnd ).toHaveBeenCalledWith( {
|
|
228
|
-
id: 'generateText-9000000000',
|
|
229
|
-
details: {
|
|
230
|
-
result: response.text,
|
|
231
|
-
usage: response.usage,
|
|
232
|
-
providerMetadata: response.finalStep.providerMetadata,
|
|
233
|
-
sources: []
|
|
234
|
-
}
|
|
235
|
-
} );
|
|
236
|
-
expect( wrapped.cost ).toBeNull();
|
|
237
|
-
} );
|
|
238
|
-
|
|
239
|
-
it( 'skips the legacy attribute and event when the cost cannot be converted', async () => {
|
|
240
|
-
mocks.convertCostToLegacy.mockReturnValue( null );
|
|
241
|
-
const response = textResponse();
|
|
242
|
-
|
|
243
|
-
await wrapGeneration( {
|
|
244
|
-
name: 'generateText',
|
|
245
|
-
prompt,
|
|
246
|
-
fn: async () => response
|
|
247
|
-
} );
|
|
248
|
-
|
|
249
|
-
expect( tracing.addEventAttribute ).toHaveBeenNthCalledWith( 1, {
|
|
250
|
-
eventId: 'generateText-9000000000',
|
|
251
|
-
attribute: mockCost
|
|
252
|
-
} );
|
|
253
|
-
expect( tracing.addEventAttribute ).toHaveBeenNthCalledWith( 2, {
|
|
254
|
-
eventId: 'generateText-9000000000',
|
|
255
|
-
attribute: mockUsage
|
|
256
|
-
} );
|
|
257
|
-
expect( tracing.addEventAttribute ).toHaveBeenCalledTimes( 2 );
|
|
258
|
-
expect( event.emit ).toHaveBeenCalledOnce();
|
|
259
|
-
expect( event.emit ).toHaveBeenCalledWith( 'llm:generation:metering', {
|
|
260
|
-
cost: mockCost,
|
|
261
|
-
usage: mockUsage
|
|
262
|
-
} );
|
|
263
|
-
} );
|
|
264
|
-
|
|
265
|
-
it( 'skips cost calculation when usage cannot be parsed', async () => {
|
|
266
|
-
mocks.parseLLMUsage.mockReturnValue( null );
|
|
267
|
-
const response = textResponse();
|
|
268
|
-
|
|
269
|
-
const wrapped = await wrapGeneration( {
|
|
270
|
-
name: 'generateText',
|
|
271
|
-
prompt,
|
|
272
|
-
fn: async () => response
|
|
273
|
-
} );
|
|
274
|
-
|
|
275
|
-
expect( mocks.calculateCosts ).not.toHaveBeenCalled();
|
|
276
|
-
expect( tracing.addEventAttribute ).not.toHaveBeenCalled();
|
|
277
|
-
expect( event.emit ).not.toHaveBeenCalled();
|
|
278
|
-
expect( tracing.addEventEnd ).toHaveBeenCalledWith( {
|
|
279
|
-
id: 'generateText-9000000000',
|
|
280
|
-
details: {
|
|
281
|
-
result: response.text,
|
|
282
|
-
usage: response.usage,
|
|
283
|
-
providerMetadata: response.finalStep.providerMetadata,
|
|
284
|
-
sources: []
|
|
285
|
-
}
|
|
286
|
-
} );
|
|
287
|
-
expect( wrapped.cost ).toBeNull();
|
|
288
|
-
} );
|
|
289
|
-
|
|
290
|
-
it( 'maps fn errors onto the llm trace and rethrows', async () => {
|
|
291
|
-
const original = new Error( 'boom' );
|
|
292
|
-
|
|
293
|
-
await expect( wrapGeneration( {
|
|
294
|
-
name: 'Agent.generate',
|
|
295
|
-
prompt,
|
|
296
|
-
fn: async () => {
|
|
297
|
-
throw original;
|
|
298
|
-
}
|
|
299
|
-
} ) ).rejects.toBe( mappedError );
|
|
300
|
-
|
|
301
|
-
expect( mocks.mapAiError ).toHaveBeenCalledWith( original );
|
|
302
|
-
expect( tracing.addEventError ).toHaveBeenCalledWith( {
|
|
303
|
-
id: 'Agent.generate-9000000000',
|
|
304
|
-
details: mappedError
|
|
305
|
-
} );
|
|
306
|
-
expect( tracing.addEventEnd ).not.toHaveBeenCalled();
|
|
307
|
-
} );
|
|
308
|
-
} );
|
|
309
|
-
|
|
310
|
-
describe( 'wrapStream', () => {
|
|
311
|
-
const hooksFrom = name => {
|
|
312
|
-
const fn = vi.fn( () => ( {} ) );
|
|
313
|
-
wrapStream( { name, prompt, fn } );
|
|
314
|
-
return fn.mock.calls[0][0];
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
it( 'starts an llm trace, returns fn result, and passes hooks', () => {
|
|
318
|
-
const stream = { kind: 'stream' };
|
|
319
|
-
const fn = vi.fn( () => stream );
|
|
320
|
-
|
|
321
|
-
const result = wrapStream( { name: 'streamText', prompt, fn } );
|
|
322
|
-
|
|
323
|
-
expect( result ).toBe( stream );
|
|
324
|
-
expect( tracing.addEventStart ).toHaveBeenCalledWith( {
|
|
325
|
-
kind: 'llm',
|
|
326
|
-
id: 'streamText-9000000000',
|
|
327
|
-
name: 'streamText',
|
|
328
|
-
details: { prompt }
|
|
329
|
-
} );
|
|
330
|
-
expect( fn ).toHaveBeenCalledWith( {
|
|
331
|
-
onEndHook: expect.any( Function ),
|
|
332
|
-
onErrorHook: expect.any( Function )
|
|
333
|
-
} );
|
|
334
|
-
expect( tracing.addEventEnd ).not.toHaveBeenCalled();
|
|
335
|
-
} );
|
|
336
|
-
|
|
337
|
-
it( 'wraps onEnd, ends the trace, then awaits the callback', async () => {
|
|
338
|
-
const response = streamResponse();
|
|
339
|
-
const mergedSources = [ { url: 'https://s.test' } ];
|
|
340
|
-
mocks.extractSources.mockReturnValue( mergedSources );
|
|
341
|
-
const callback = vi.fn();
|
|
342
|
-
const { onEndHook } = hooksFrom( 'Agent.stream' );
|
|
343
|
-
|
|
344
|
-
await onEndHook( response, callback );
|
|
345
|
-
|
|
346
|
-
expect( mocks.parseLLMUsage ).toHaveBeenCalledWith( {
|
|
347
|
-
usage: response.usage,
|
|
348
|
-
prompt
|
|
349
|
-
} );
|
|
350
|
-
expect( mocks.calculateCosts ).toHaveBeenCalledWith( mockUsage );
|
|
351
|
-
expect( mocks.extractSources ).toHaveBeenCalledWith( response );
|
|
352
|
-
expect( callback ).toHaveBeenCalledOnce();
|
|
353
|
-
const proxied = callback.mock.calls[0][0];
|
|
354
|
-
expect( proxied.result ).toBe( response.text );
|
|
355
|
-
expect( proxied.cost ).toEqual( mockCost );
|
|
356
|
-
expect( proxied.sources ).toBe( mergedSources );
|
|
357
|
-
expect( tracing.addEventEnd ).toHaveBeenCalledWith( {
|
|
358
|
-
id: 'Agent.stream-9000000000',
|
|
359
|
-
details: {
|
|
360
|
-
result: response.text,
|
|
361
|
-
usage: response.usage,
|
|
362
|
-
providerMetadata: response.finalStep.providerMetadata,
|
|
363
|
-
sources: mergedSources
|
|
364
|
-
}
|
|
365
|
-
} );
|
|
366
|
-
expect( tracing.addEventEnd.mock.invocationCallOrder[0] ).toBeLessThan( callback.mock.invocationCallOrder[0] );
|
|
367
|
-
} );
|
|
368
|
-
|
|
369
|
-
it( 'ends the trace and swallows throws from the onEnd callback', async () => {
|
|
370
|
-
const response = textResponse();
|
|
371
|
-
const { onEndHook } = hooksFrom( 'Agent.stream' );
|
|
372
|
-
|
|
373
|
-
await expect( onEndHook( response, async () => {
|
|
374
|
-
throw new Error( 'user onEnd' );
|
|
375
|
-
} ) ).resolves.toBeUndefined();
|
|
376
|
-
|
|
377
|
-
expect( tracing.addEventEnd ).toHaveBeenCalledOnce();
|
|
378
|
-
expect( tracing.addEventError ).not.toHaveBeenCalled();
|
|
379
|
-
expect( mocks.mapAiError ).not.toHaveBeenCalled();
|
|
380
|
-
} );
|
|
381
|
-
|
|
382
|
-
it( 'maps onError events and forwards the mapped error to the callback', () => {
|
|
383
|
-
const original = new Error( 'stream failed' );
|
|
384
|
-
const callback = vi.fn();
|
|
385
|
-
const { onErrorHook } = hooksFrom( 'streamText' );
|
|
386
|
-
|
|
387
|
-
onErrorHook( { error: original }, callback );
|
|
388
|
-
|
|
389
|
-
expect( mocks.mapAiError ).toHaveBeenCalledWith( original );
|
|
390
|
-
expect( tracing.addEventError ).toHaveBeenCalledWith( {
|
|
391
|
-
id: 'streamText-9000000000',
|
|
392
|
-
details: mappedError
|
|
393
|
-
} );
|
|
394
|
-
expect( callback ).toHaveBeenCalledWith( mappedError );
|
|
395
|
-
} );
|
|
396
|
-
|
|
397
|
-
it( 'swallows throws from the onError callback', async () => {
|
|
398
|
-
const { onErrorHook } = hooksFrom( 'streamText' );
|
|
399
|
-
|
|
400
|
-
await expect( onErrorHook( { error: new Error( 'x' ) }, () => {
|
|
401
|
-
throw new Error( 'user onError' );
|
|
402
|
-
} ) ).resolves.toBeUndefined();
|
|
403
|
-
} );
|
|
404
|
-
|
|
405
|
-
it( 'awaits and swallows rejected promises from the onError callback', async () => {
|
|
406
|
-
const state = { rejection: null };
|
|
407
|
-
const callback = vi.fn( () => new Promise( ( _, reject ) => {
|
|
408
|
-
state.rejection = reject;
|
|
409
|
-
} ) );
|
|
410
|
-
const { onErrorHook } = hooksFrom( 'streamText' );
|
|
411
|
-
|
|
412
|
-
const hookPromise = onErrorHook( { error: new Error( 'x' ) }, callback );
|
|
413
|
-
|
|
414
|
-
expect( hookPromise ).toBeInstanceOf( Promise );
|
|
415
|
-
state.rejection( new Error( 'async user onError' ) );
|
|
416
|
-
await expect( hookPromise ).resolves.toBeUndefined();
|
|
417
|
-
} );
|
|
418
|
-
|
|
419
|
-
it( 'maps a throw from fn onto the llm trace and rethrows', () => {
|
|
420
|
-
const original = new Error( 'create stream' );
|
|
421
|
-
|
|
422
|
-
expect( () => wrapStream( {
|
|
423
|
-
name: 'streamText',
|
|
424
|
-
prompt,
|
|
425
|
-
fn: () => {
|
|
426
|
-
throw original;
|
|
427
|
-
}
|
|
428
|
-
} ) ).toThrow( mappedError );
|
|
429
|
-
|
|
430
|
-
expect( mocks.mapAiError ).toHaveBeenCalledWith( original );
|
|
431
|
-
expect( tracing.addEventError ).toHaveBeenCalledWith( {
|
|
432
|
-
id: 'streamText-9000000000',
|
|
433
|
-
details: mappedError
|
|
434
|
-
} );
|
|
435
|
-
} );
|
|
436
|
-
|
|
437
|
-
it( 'maps a rejected promise from fn onto the llm trace and rethrows', async () => {
|
|
438
|
-
const original = new Error( 'prepareCall failed' );
|
|
439
|
-
|
|
440
|
-
await expect( wrapStream( {
|
|
441
|
-
name: 'Agent.stream',
|
|
442
|
-
prompt,
|
|
443
|
-
fn: () => Promise.reject( original )
|
|
444
|
-
} ) ).rejects.toBe( mappedError );
|
|
445
|
-
|
|
446
|
-
expect( mocks.mapAiError ).toHaveBeenCalledWith( original );
|
|
447
|
-
expect( tracing.addEventError ).toHaveBeenCalledWith( {
|
|
448
|
-
id: 'Agent.stream-9000000000',
|
|
449
|
-
details: mappedError
|
|
450
|
-
} );
|
|
451
|
-
expect( tracing.addEventEnd ).not.toHaveBeenCalled();
|
|
452
|
-
} );
|
|
453
|
-
|
|
454
|
-
it( 'returns a fulfilled promise from fn without mapping', async () => {
|
|
455
|
-
const stream = { kind: 'stream' };
|
|
456
|
-
|
|
457
|
-
await expect( wrapStream( {
|
|
458
|
-
name: 'Agent.stream',
|
|
459
|
-
prompt,
|
|
460
|
-
fn: () => Promise.resolve( stream )
|
|
461
|
-
} ) ).resolves.toBe( stream );
|
|
462
|
-
|
|
463
|
-
expect( mocks.mapAiError ).not.toHaveBeenCalled();
|
|
464
|
-
expect( tracing.addEventError ).not.toHaveBeenCalled();
|
|
465
|
-
} );
|
|
466
|
-
} );
|
|
467
|
-
} );
|