@outputai/llm 0.4.1-next.fb7438a.0 → 0.5.1-dev.45fb889.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/llm",
3
- "version": "0.4.1-next.fb7438a.0",
3
+ "version": "0.5.1-dev.45fb889.0",
4
4
  "description": "Framework abstraction to interact with LLM models",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -19,12 +19,11 @@
19
19
  "@perplexity-ai/ai-sdk": "0.1.3",
20
20
  "@tavily/ai-sdk": "0.4.1",
21
21
  "ai": "6.0.168",
22
- "decimal.js": "10.6.0",
23
22
  "entities": "8.0.0",
24
23
  "gray-matter": "4.0.3",
25
24
  "liquidjs": "10.25.7",
26
25
  "undici": "8.1.0",
27
- "@outputai/core": "0.4.1-next.fb7438a.0"
26
+ "@outputai/core": "0.5.1-dev.45fb889.0"
28
27
  },
29
28
  "license": "Apache-2.0",
30
29
  "publishConfig": {
package/src/cost/index.js CHANGED
@@ -1,8 +1,5 @@
1
1
  import { fetchModelsPricing } from './fetch_models_pricing.js';
2
- import Decimal from 'decimal.js';
3
-
4
- const M = 1_000_000;
5
- const calcCost = ( tokens, ppm ) => Decimal( tokens ?? 0 ).div( M ).mul( ppm ).toNumber();
2
+ import { Tracing } from '@outputai/core/sdk_activity_integration';
6
3
 
7
4
  /**
8
5
  * Calculates the cost of an llm call based on the model and usage.
@@ -14,29 +11,41 @@ const calcCost = ( tokens, ppm ) => Decimal( tokens ?? 0 ).div( M ).mul( ppm ).t
14
11
  export const calculateLLMCallCost = async ( { modelId, usage } ) => {
15
12
  try {
16
13
  const models = await fetchModelsPricing();
14
+
17
15
  if ( !models ) {
18
- return { total: null, message: 'Failed to fetch models pricing' };
16
+ console.warn( 'Failed to fetch models pricing' );
17
+ return null;
19
18
  }
20
19
 
21
- const cost = models.get( modelId );
22
- if ( !cost ) {
23
- return { total: null, message: 'Missing cost reference for model' };
20
+ const pricing = models.get( modelId );
21
+ if ( !pricing ) {
22
+ console.warn( 'Missing cost reference for model' );
23
+ return null;
24
24
  }
25
25
 
26
26
  const { inputTokens, cachedInputTokens, outputTokens, reasoningTokens } = usage;
27
27
 
28
28
  const nonCachedTokens = inputTokens - ( cachedInputTokens ?? 0 );
29
29
 
30
- const components = [
31
- Number.isFinite( cost.input ) ? { name: 'input_tokens', value: calcCost( nonCachedTokens, cost.input ) } : false,
32
- Number.isFinite( cost.cache_read ) ? { name: 'input_cached_tokens', value: calcCost( cachedInputTokens, cost.cache_read ) } : false,
33
- Number.isFinite( cost.output ) ? { name: 'output_tokens', value: calcCost( outputTokens, cost.output ) } : false,
34
- /* When there aren't reasoning costs, the providers doesn't differentiate reasoning vs output, so the price is included in the output */
35
- Number.isFinite( cost.reasoning ) ? { name: 'reasoning_tokens', value: calcCost( reasoningTokens, cost.reasoning ) } : false
36
- ].filter( v => !!v );
37
- return { total: components.reduce( ( v, e ) => v.plus( e.value ), Decimal( 0 ) ).toNumber(), components };
30
+ const llmUsage = new Tracing.Attribute.LLMUsage( modelId );
31
+
32
+ if ( Number.isFinite( pricing.input ) && Number.isFinite( nonCachedTokens ) ) {
33
+ llmUsage.addUsage( { type: 'input', ppm: pricing.input, amount: nonCachedTokens } );
34
+ }
35
+ if ( Number.isFinite( pricing.cache_read ) && Number.isFinite( cachedInputTokens ) ) {
36
+ llmUsage.addUsage( { type: 'input_cached', ppm: pricing.cache_read, amount: cachedInputTokens } );
37
+ }
38
+ if ( Number.isFinite( pricing.output ) && Number.isFinite( outputTokens ) ) {
39
+ llmUsage.addUsage( { type: 'output', ppm: pricing.output, amount: outputTokens } );
40
+ }
41
+ // When there aren't reasoning costs, the providers doesn't differentiate reasoning vs output, so the price is included in the output
42
+ if ( Number.isFinite( pricing.reasoning ) && Number.isFinite( reasoningTokens ) ) {
43
+ llmUsage.addUsage( { type: 'reasoning', ppm: pricing.reasoning, amount: reasoningTokens } );
44
+ }
45
+
46
+ return llmUsage;
38
47
  } catch ( error ) {
39
48
  console.error( 'Error calculating LLM call costs', error );
40
- return { total: null, message: `Error calculating LLM call costs: ${error.constructor.name} - ${error.message}` };
49
+ return null;
41
50
  }
42
51
  };
@@ -1,18 +1,75 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ const mockFetchModelsPricing = vi.hoisted( () => vi.fn() );
2
4
 
3
- const mockFetchModelsPricing = vi.fn();
4
5
  vi.mock( './fetch_models_pricing.js', () => ( {
5
6
  fetchModelsPricing: ( ...args ) => mockFetchModelsPricing( ...args )
6
7
  } ) );
7
8
 
9
+ vi.mock( '@outputai/core/sdk_activity_integration', () => {
10
+ class LLMUsage {
11
+ static TYPE = 'llm:usage';
12
+ type = LLMUsage.TYPE;
13
+ modelId;
14
+ usage = [];
15
+
16
+ constructor( modelId ) {
17
+ this.modelId = modelId;
18
+ }
19
+
20
+ addUsage( { type, ppm, amount } ) {
21
+ this.usage.push( {
22
+ type,
23
+ ppm,
24
+ amount,
25
+ total: ( amount / 1_000_000 ) * ppm
26
+ } );
27
+ }
28
+
29
+ get total() {
30
+ return this.usage.reduce( ( total, current ) => total + current.total, 0 );
31
+ }
32
+
33
+ get tokensUsed() {
34
+ return this.usage.reduce( ( total, current ) => total + current.amount, 0 );
35
+ }
36
+ }
37
+
38
+ return {
39
+ Tracing: {
40
+ Attribute: {
41
+ LLMUsage
42
+ }
43
+ }
44
+ };
45
+ } );
46
+
47
+ import { Tracing } from '@outputai/core/sdk_activity_integration';
8
48
  import { calculateLLMCallCost } from './index.js';
9
49
 
50
+ const expectLLMUsage = ( result, { modelId, usage, total, tokensUsed } ) => {
51
+ expect( result ).toBeInstanceOf( Tracing.Attribute.LLMUsage );
52
+ expect( result ).toEqual( expect.objectContaining( {
53
+ type: Tracing.Attribute.LLMUsage.TYPE,
54
+ modelId,
55
+ usage
56
+ } ) );
57
+ expect( result.total ).toBeCloseTo( total );
58
+ expect( result.tokensUsed ).toBe( tokensUsed );
59
+ };
60
+
10
61
  describe( 'calculateLLMCallCost', () => {
11
62
  beforeEach( () => {
12
63
  vi.clearAllMocks();
64
+ vi.spyOn( console, 'warn' ).mockImplementation( () => {} );
65
+ vi.spyOn( console, 'error' ).mockImplementation( () => {} );
13
66
  } );
14
67
 
15
- it( 'returns total null and message when fetchModelsPricing returns null', async () => {
68
+ afterEach( () => {
69
+ vi.restoreAllMocks();
70
+ } );
71
+
72
+ it( 'returns null when fetchModelsPricing returns null', async () => {
16
73
  mockFetchModelsPricing.mockResolvedValue( null );
17
74
 
18
75
  const result = await calculateLLMCallCost( {
@@ -20,10 +77,11 @@ describe( 'calculateLLMCallCost', () => {
20
77
  usage: { inputTokens: 100, outputTokens: 50 }
21
78
  } );
22
79
 
23
- expect( result ).toEqual( { total: null, message: 'Failed to fetch models pricing' } );
80
+ expect( result ).toBeNull();
81
+ expect( console.warn ).toHaveBeenCalledWith( 'Failed to fetch models pricing' );
24
82
  } );
25
83
 
26
- it( 'returns total null and message when model is missing from cost table', async () => {
84
+ it( 'returns null when model is missing from cost table', async () => {
27
85
  mockFetchModelsPricing.mockResolvedValue( new Map() );
28
86
 
29
87
  const result = await calculateLLMCallCost( {
@@ -31,47 +89,50 @@ describe( 'calculateLLMCallCost', () => {
31
89
  usage: { inputTokens: 100, outputTokens: 50 }
32
90
  } );
33
91
 
34
- expect( result ).toEqual( {
35
- total: null,
36
- message: 'Missing cost reference for model'
37
- } );
92
+ expect( result ).toBeNull();
93
+ expect( console.warn ).toHaveBeenCalledWith( 'Missing cost reference for model' );
38
94
  } );
39
95
 
40
- it( 'calculates input and output cost from mock model', async () => {
41
- const cost = { input: 2, output: 10, cache_read: 1 };
42
- mockFetchModelsPricing.mockResolvedValue( new Map( [ [ 'gpt-4o', cost ] ] ) );
96
+ it( 'calculates input and output usage from model pricing', async () => {
97
+ mockFetchModelsPricing.mockResolvedValue( new Map( [ [ 'gpt-4o', { input: 2, output: 10, cache_read: 1 } ] ] ) );
43
98
 
44
99
  const result = await calculateLLMCallCost( {
45
100
  modelId: 'gpt-4o',
46
101
  usage: { inputTokens: 1_000_000, outputTokens: 500_000 }
47
102
  } );
48
103
 
49
- expect( result.total ).toBe( 7 );
50
- expect( result.components ).toEqual( [
51
- { name: 'input_tokens', value: 2 },
52
- { name: 'input_cached_tokens', value: 0 },
53
- { name: 'output_tokens', value: 5 }
54
- ] );
104
+ expectLLMUsage( result, {
105
+ modelId: 'gpt-4o',
106
+ usage: [
107
+ { type: 'input', ppm: 2, amount: 1_000_000, total: 2 },
108
+ { type: 'output', ppm: 10, amount: 500_000, total: 5 }
109
+ ],
110
+ total: 7,
111
+ tokensUsed: 1_500_000
112
+ } );
55
113
  } );
56
114
 
57
- it( 'splits input into non-cached and cached at respective rates', async () => {
58
- const cost = { input: 4, cache_read: 1, output: 10 };
59
- mockFetchModelsPricing.mockResolvedValue( new Map( [ [ 'cached-model', cost ] ] ) );
115
+ it( 'splits input into non-cached and cached usage at respective rates', async () => {
116
+ mockFetchModelsPricing.mockResolvedValue( new Map( [ [ 'cached-model', { input: 4, cache_read: 1, output: 10 } ] ] ) );
60
117
 
61
118
  const result = await calculateLLMCallCost( {
62
119
  modelId: 'cached-model',
63
120
  usage: { inputTokens: 1_000_000, cachedInputTokens: 500_000, outputTokens: 100_000 }
64
121
  } );
65
122
 
66
- expect( result.components ).toEqual( [
67
- { name: 'input_tokens', value: 2 },
68
- { name: 'input_cached_tokens', value: 0.5 },
69
- { name: 'output_tokens', value: 1 }
70
- ] );
71
- expect( result.total ).toBeCloseTo( 3.5 );
123
+ expectLLMUsage( result, {
124
+ modelId: 'cached-model',
125
+ usage: [
126
+ { type: 'input', ppm: 4, amount: 500_000, total: 2 },
127
+ { type: 'input_cached', ppm: 1, amount: 500_000, total: 0.5 },
128
+ { type: 'output', ppm: 10, amount: 100_000, total: 1 }
129
+ ],
130
+ total: 3.5,
131
+ tokensUsed: 1_100_000
132
+ } );
72
133
  } );
73
134
 
74
- it( 'omits cached component when model has no cache_read (non-cached rate applies to full input minus cached)', async () => {
135
+ it( 'omits cached usage when model has no cache_read rate', async () => {
75
136
  mockFetchModelsPricing.mockResolvedValue( new Map( [ [ 'no-cache', { input: 2, output: 10 } ] ] ) );
76
137
 
77
138
  const result = await calculateLLMCallCost( {
@@ -79,14 +140,18 @@ describe( 'calculateLLMCallCost', () => {
79
140
  usage: { inputTokens: 1_000_000, cachedInputTokens: 200_000, outputTokens: 0 }
80
141
  } );
81
142
 
82
- expect( result.components ).toEqual( [
83
- { name: 'input_tokens', value: 1.6 },
84
- { name: 'output_tokens', value: 0 }
85
- ] );
86
- expect( result.total ).toBe( 1.6 );
143
+ expectLLMUsage( result, {
144
+ modelId: 'no-cache',
145
+ usage: [
146
+ { type: 'input', ppm: 2, amount: 800_000, total: 1.6 },
147
+ { type: 'output', ppm: 10, amount: 0, total: 0 }
148
+ ],
149
+ total: 1.6,
150
+ tokensUsed: 800_000
151
+ } );
87
152
  } );
88
153
 
89
- it( 'omits input component when pricing has no input rate', async () => {
154
+ it( 'omits input usage when pricing has no input rate', async () => {
90
155
  mockFetchModelsPricing.mockResolvedValue( new Map( [ [ 'out-only', { output: 10 } ] ] ) );
91
156
 
92
157
  const result = await calculateLLMCallCost( {
@@ -94,13 +159,17 @@ describe( 'calculateLLMCallCost', () => {
94
159
  usage: { inputTokens: 100, outputTokens: 50 }
95
160
  } );
96
161
 
97
- expect( result.total ).toBe( 0.0005 );
98
- expect( result.components ).toEqual( [
99
- { name: 'output_tokens', value: 0.0005 }
100
- ] );
162
+ expectLLMUsage( result, {
163
+ modelId: 'out-only',
164
+ usage: [
165
+ { type: 'output', ppm: 10, amount: 50, total: 0.0005 }
166
+ ],
167
+ total: 0.0005,
168
+ tokensUsed: 50
169
+ } );
101
170
  } );
102
171
 
103
- it( 'omits output component when pricing has no output rate', async () => {
172
+ it( 'omits output usage when pricing has no output rate', async () => {
104
173
  mockFetchModelsPricing.mockResolvedValue( new Map( [ [ 'in-only', { input: 1 } ] ] ) );
105
174
 
106
175
  const result = await calculateLLMCallCost( {
@@ -108,13 +177,17 @@ describe( 'calculateLLMCallCost', () => {
108
177
  usage: { inputTokens: 100, outputTokens: 50 }
109
178
  } );
110
179
 
111
- expect( result.total ).toBe( 0.0001 );
112
- expect( result.components ).toEqual( [
113
- { name: 'input_tokens', value: 0.0001 }
114
- ] );
180
+ expectLLMUsage( result, {
181
+ modelId: 'in-only',
182
+ usage: [
183
+ { type: 'input', ppm: 1, amount: 100, total: 0.0001 }
184
+ ],
185
+ total: 0.0001,
186
+ tokensUsed: 100
187
+ } );
115
188
  } );
116
189
 
117
- it( 'uses reasoning cost when present', async () => {
190
+ it( 'includes reasoning usage when present', async () => {
118
191
  mockFetchModelsPricing.mockResolvedValue( new Map( [ [
119
192
  'with-reasoning',
120
193
  { input: 1, output: 10, reasoning: 60 }
@@ -125,15 +198,19 @@ describe( 'calculateLLMCallCost', () => {
125
198
  usage: { inputTokens: 100, outputTokens: 20, reasoningTokens: 50 }
126
199
  } );
127
200
 
128
- expect( result.total ).toBeCloseTo( 0.0033 );
129
- expect( result.components ).toEqual( [
130
- { name: 'input_tokens', value: 0.0001 },
131
- { name: 'output_tokens', value: 0.0002 },
132
- { name: 'reasoning_tokens', value: 0.003 }
133
- ] );
201
+ expectLLMUsage( result, {
202
+ modelId: 'with-reasoning',
203
+ usage: [
204
+ { type: 'input', ppm: 1, amount: 100, total: 0.0001 },
205
+ { type: 'output', ppm: 10, amount: 20, total: 0.0002 },
206
+ { type: 'reasoning', ppm: 60, amount: 50, total: 0.003 }
207
+ ],
208
+ total: 0.0033,
209
+ tokensUsed: 170
210
+ } );
134
211
  } );
135
212
 
136
- it( 'omits reasoning component when reasoning cost missing (included in output)', async () => {
213
+ it( 'omits reasoning usage when reasoning cost is missing', async () => {
137
214
  mockFetchModelsPricing.mockResolvedValue( new Map( [ [ 'no-reasoning', { input: 1, output: 10 } ] ] ) );
138
215
 
139
216
  const result = await calculateLLMCallCost( {
@@ -141,14 +218,18 @@ describe( 'calculateLLMCallCost', () => {
141
218
  usage: { inputTokens: 100, outputTokens: 20, reasoningTokens: 50 }
142
219
  } );
143
220
 
144
- expect( result.total ).toBeCloseTo( 0.0003 );
145
- expect( result.components ).toEqual( [
146
- { name: 'input_tokens', value: 0.0001 },
147
- { name: 'output_tokens', value: 0.0002 }
148
- ] );
221
+ expectLLMUsage( result, {
222
+ modelId: 'no-reasoning',
223
+ usage: [
224
+ { type: 'input', ppm: 1, amount: 100, total: 0.0001 },
225
+ { type: 'output', ppm: 10, amount: 20, total: 0.0002 }
226
+ ],
227
+ total: 0.0003,
228
+ tokensUsed: 120
229
+ } );
149
230
  } );
150
231
 
151
- it( 'includes reasoning component with zero when reasoningTokens is zero', async () => {
232
+ it( 'includes reasoning usage with zero amount when reasoningTokens is zero', async () => {
152
233
  mockFetchModelsPricing.mockResolvedValue( new Map( [ [
153
234
  'full',
154
235
  { input: 2, output: 8, reasoning: 60 }
@@ -159,15 +240,19 @@ describe( 'calculateLLMCallCost', () => {
159
240
  usage: { inputTokens: 100, outputTokens: 50, reasoningTokens: 0 }
160
241
  } );
161
242
 
162
- expect( result.components ).toEqual( [
163
- { name: 'input_tokens', value: 0.0002 },
164
- { name: 'output_tokens', value: 0.0004 },
165
- { name: 'reasoning_tokens', value: 0 }
166
- ] );
167
- expect( result.total ).toBeCloseTo( 0.0006 );
243
+ expectLLMUsage( result, {
244
+ modelId: 'full',
245
+ usage: [
246
+ { type: 'input', ppm: 2, amount: 100, total: 0.0002 },
247
+ { type: 'output', ppm: 8, amount: 50, total: 0.0004 },
248
+ { type: 'reasoning', ppm: 60, amount: 0, total: 0 }
249
+ ],
250
+ total: 0.0006,
251
+ tokensUsed: 150
252
+ } );
168
253
  } );
169
254
 
170
- it( 'treats null/undefined token counts as 0', async () => {
255
+ it( 'omits usage entries for non-finite token counts', async () => {
171
256
  mockFetchModelsPricing.mockResolvedValue( new Map( [ [ 'm', { input: 1, output: 2 } ] ] ) );
172
257
 
173
258
  const result = await calculateLLMCallCost( {
@@ -175,10 +260,26 @@ describe( 'calculateLLMCallCost', () => {
175
260
  usage: { inputTokens: null, outputTokens: undefined }
176
261
  } );
177
262
 
178
- expect( result.total ).toBe( 0 );
179
- expect( result.components ).toEqual( [
180
- { name: 'input_tokens', value: 0 },
181
- { name: 'output_tokens', value: 0 }
182
- ] );
263
+ expectLLMUsage( result, {
264
+ modelId: 'm',
265
+ usage: [
266
+ { type: 'input', ppm: 1, amount: 0, total: 0 }
267
+ ],
268
+ total: 0,
269
+ tokensUsed: 0
270
+ } );
271
+ } );
272
+
273
+ it( 'returns null when pricing lookup throws', async () => {
274
+ const error = new Error( 'boom' );
275
+ mockFetchModelsPricing.mockRejectedValue( error );
276
+
277
+ const result = await calculateLLMCallCost( {
278
+ modelId: 'gpt-4o',
279
+ usage: { inputTokens: 100, outputTokens: 50 }
280
+ } );
281
+
282
+ expect( result ).toBeNull();
283
+ expect( console.error ).toHaveBeenCalledWith( 'Error calculating LLM call costs', error );
183
284
  } );
184
285
  } );
@@ -12,7 +12,9 @@ export const endTraceWithError = ( { traceId, error } ) => {
12
12
 
13
13
  export const endTraceWithSuccess = ( { traceId, modelId, response, cost, ...extra } ) => {
14
14
  const { totalUsage: usage, text: result, providerMetadata } = response;
15
- Tracing.addEventAttribute( { eventId: traceId, name: Tracing.Attribute.COST, value: cost } );
15
+ if ( cost ) {
16
+ Tracing.addEventAttribute( { eventId: traceId, attribute: cost } );
17
+ emitEvent( 'cost:llm:request', cost );
18
+ }
16
19
  Tracing.addEventEnd( { id: traceId, details: { result, usage, providerMetadata, ...extra } } );
17
- emitEvent( 'cost:llm:request', { modelId, cost, usage } );
18
20
  };
@@ -5,10 +5,7 @@ vi.mock( '@outputai/core/sdk_activity_integration', () => ( {
5
5
  addEventStart: vi.fn(),
6
6
  addEventError: vi.fn(),
7
7
  addEventAttribute: vi.fn(),
8
- addEventEnd: vi.fn(),
9
- Attribute: {
10
- COST: 'cost'
11
- }
8
+ addEventEnd: vi.fn()
12
9
  },
13
10
  emitEvent: vi.fn()
14
11
  } ) );
@@ -54,8 +51,8 @@ describe( 'trace utils', () => {
54
51
  } );
55
52
 
56
53
  describe( 'endTraceWithSuccess', () => {
57
- it( 'adds cost attribute, ends the trace with response fields and extra details, and emits cost:llm:request', () => {
58
- const cost = { total: 0.01, components: [] };
54
+ it( 'adds cost attribute, emits cost attribute, and ends the trace with response fields and extra details', () => {
55
+ const cost = { type: 'llm:usage', modelId: 'my-model', total: 0.01, usage: [] };
59
56
  const usage = { inputTokens: 2, outputTokens: 3 };
60
57
  const response = {
61
58
  text: 'hello',
@@ -73,9 +70,9 @@ describe( 'trace utils', () => {
73
70
 
74
71
  expect( tracing.addEventAttribute ).toHaveBeenCalledWith( {
75
72
  eventId: 'trace-a',
76
- name: 'cost',
77
- value: cost
73
+ attribute: cost
78
74
  } );
75
+ expect( emitEvent ).toHaveBeenCalledWith( 'cost:llm:request', cost );
79
76
  expect( tracing.addEventEnd ).toHaveBeenCalledWith( {
80
77
  id: 'trace-a',
81
78
  details: {
@@ -85,10 +82,31 @@ describe( 'trace utils', () => {
85
82
  sourcesFromTools: [ { url: 'https://u.test', title: '' } ]
86
83
  }
87
84
  } );
88
- expect( emitEvent ).toHaveBeenCalledWith( 'cost:llm:request', {
85
+ } );
86
+
87
+ it( 'does not emit or add an attribute when cost is missing', () => {
88
+ const usage = { inputTokens: 2, outputTokens: 3 };
89
+ const response = {
90
+ text: 'hello',
91
+ totalUsage: usage,
92
+ providerMetadata: { provider: 'x' }
93
+ };
94
+
95
+ endTraceWithSuccess( {
96
+ traceId: 'trace-no-cost',
89
97
  modelId: 'my-model',
90
- cost,
91
- usage
98
+ response
99
+ } );
100
+
101
+ expect( tracing.addEventAttribute ).not.toHaveBeenCalled();
102
+ expect( emitEvent ).not.toHaveBeenCalled();
103
+ expect( tracing.addEventEnd ).toHaveBeenCalledWith( {
104
+ id: 'trace-no-cost',
105
+ details: {
106
+ result: 'hello',
107
+ usage,
108
+ providerMetadata: { provider: 'x' }
109
+ }
92
110
  } );
93
111
  } );
94
112
  } );