@outputai/llm 0.6.1-dev.aab2335.0 → 0.6.1-next.0d08ff5.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 (45) hide show
  1. package/package.json +2 -2
  2. package/src/agent.js +15 -9
  3. package/src/agent.spec.js +295 -214
  4. package/src/ai_model.js +79 -36
  5. package/src/ai_model.spec.js +31 -13
  6. package/src/ai_sdk.js +55 -79
  7. package/src/ai_sdk.spec.js +464 -611
  8. package/src/ai_sdk_options.js +61 -0
  9. package/src/ai_sdk_options.spec.js +164 -0
  10. package/src/cost/index.js +1 -1
  11. package/src/index.d.ts +230 -175
  12. package/src/index.js +2 -2
  13. package/src/prompt/escape.js +65 -0
  14. package/src/prompt/escape.spec.js +159 -0
  15. package/src/{load_content.js → prompt/load_content.js} +1 -22
  16. package/src/{load_content.spec.js → prompt/load_content.spec.js} +6 -6
  17. package/src/prompt/loader.js +49 -0
  18. package/src/prompt/loader.spec.js +274 -0
  19. package/src/{prompt_loader_validation.spec.js → prompt/loader_validation.spec.js} +40 -7
  20. package/src/prompt/parser.js +19 -0
  21. package/src/{parser.spec.js → prompt/parser.spec.js} +74 -29
  22. package/src/prompt/prepare_text.js +27 -0
  23. package/src/prompt/prepare_text.spec.js +141 -0
  24. package/src/{skill.js → prompt/skill.js} +19 -0
  25. package/src/prompt/skill.spec.js +172 -0
  26. package/src/{prompt_validations.js → prompt/validations.js} +32 -6
  27. package/src/{prompt_validations.spec.js → prompt/validations.spec.js} +189 -1
  28. package/src/utils/__fixtures__/image_response.json +38 -0
  29. package/src/utils/__fixtures__/stream_response.json +294 -0
  30. package/src/utils/__fixtures__/text_response.json +201 -0
  31. package/src/utils/error_handler.js +65 -0
  32. package/src/utils/error_handler.spec.js +195 -0
  33. package/src/utils/image.js +10 -0
  34. package/src/utils/image.spec.js +20 -0
  35. package/src/utils/response_wrappers.js +46 -19
  36. package/src/utils/response_wrappers.spec.js +130 -70
  37. package/src/utils/source_extraction.js +17 -27
  38. package/src/utils/trace.js +2 -3
  39. package/src/utils/trace.spec.js +9 -13
  40. package/src/validations.js +54 -2
  41. package/src/validations.spec.js +166 -0
  42. package/src/parser.js +0 -28
  43. package/src/prompt_loader.js +0 -80
  44. package/src/prompt_loader.spec.js +0 -358
  45. package/src/skill.d.ts +0 -49
@@ -0,0 +1,201 @@
1
+ {
2
+ "steps": [
3
+ {
4
+ "stepNumber": 0,
5
+ "model": {
6
+ "provider": "anthropic.messages",
7
+ "modelId": "claude-haiku-4-5"
8
+ },
9
+ "content": [
10
+ {
11
+ "type": "text",
12
+ "text": "{\"result\": \"yes\"}"
13
+ }
14
+ ],
15
+ "finishReason": "stop",
16
+ "rawFinishReason": "end_turn",
17
+ "usage": {
18
+ "inputTokens": 217,
19
+ "inputTokenDetails": {
20
+ "noCacheTokens": 217,
21
+ "cacheReadTokens": 0,
22
+ "cacheWriteTokens": 0
23
+ },
24
+ "outputTokens": 9,
25
+ "outputTokenDetails": {},
26
+ "totalTokens": 226,
27
+ "raw": {
28
+ "input_tokens": 217,
29
+ "output_tokens": 9,
30
+ "cache_creation_input_tokens": 0,
31
+ "cache_read_input_tokens": 0,
32
+ "cache_creation": {
33
+ "ephemeral_5m_input_tokens": 0,
34
+ "ephemeral_1h_input_tokens": 0
35
+ },
36
+ "service_tier": "standard",
37
+ "inference_geo": "not_available"
38
+ },
39
+ "cachedInputTokens": 0
40
+ },
41
+ "warnings": [],
42
+ "request": {
43
+ "body": {
44
+ "model": "claude-haiku-4-5",
45
+ "max_tokens": 64000,
46
+ "temperature": 0.7,
47
+ "output_config": {
48
+ "format": {
49
+ "type": "json_schema",
50
+ "schema": {
51
+ "$schema": "http://json-schema.org/draft-07/schema#",
52
+ "type": "object",
53
+ "properties": {
54
+ "result": {
55
+ "type": "string",
56
+ "enum": [
57
+ "yes",
58
+ "no"
59
+ ]
60
+ }
61
+ },
62
+ "required": [
63
+ "result"
64
+ ],
65
+ "additionalProperties": false
66
+ }
67
+ }
68
+ },
69
+ "messages": [
70
+ {
71
+ "role": "assistant",
72
+ "content": [
73
+ {
74
+ "type": "text",
75
+ "text": "You are a concise assistant."
76
+ }
77
+ ]
78
+ },
79
+ {
80
+ "role": "user",
81
+ "content": [
82
+ {
83
+ "type": "text",
84
+ "text": "Do humans like carburetors?"
85
+ }
86
+ ]
87
+ }
88
+ ]
89
+ }
90
+ },
91
+ "response": {
92
+ "id": "msg_01VQv2YKP97yEn4kin5Xvn6v",
93
+ "timestamp": "2026-06-04T15:30:32.333Z",
94
+ "modelId": "claude-haiku-4-5-20251001",
95
+ "headers": {
96
+ "__redacted__": "<<headers were removed>>"
97
+ },
98
+ "body": {
99
+ "model": "claude-haiku-4-5-20251001",
100
+ "id": "msg_01VQv2YKP97yEn4kin5Xvn6v",
101
+ "type": "message",
102
+ "role": "assistant",
103
+ "content": [
104
+ {
105
+ "type": "text",
106
+ "text": "{\"result\": \"yes\"}"
107
+ }
108
+ ],
109
+ "stop_reason": "end_turn",
110
+ "stop_sequence": null,
111
+ "stop_details": null,
112
+ "usage": {
113
+ "input_tokens": 217,
114
+ "cache_creation_input_tokens": 0,
115
+ "cache_read_input_tokens": 0,
116
+ "cache_creation": {
117
+ "ephemeral_5m_input_tokens": 0,
118
+ "ephemeral_1h_input_tokens": 0
119
+ },
120
+ "output_tokens": 9,
121
+ "service_tier": "standard",
122
+ "inference_geo": "not_available"
123
+ }
124
+ },
125
+ "messages": [
126
+ {
127
+ "role": "assistant",
128
+ "content": [
129
+ {
130
+ "type": "text",
131
+ "text": "{\"result\": \"yes\"}"
132
+ }
133
+ ]
134
+ }
135
+ ]
136
+ },
137
+ "providerMetadata": {
138
+ "anthropic": {
139
+ "usage": {
140
+ "input_tokens": 217,
141
+ "output_tokens": 9,
142
+ "cache_creation_input_tokens": 0,
143
+ "cache_read_input_tokens": 0,
144
+ "cache_creation": {
145
+ "ephemeral_5m_input_tokens": 0,
146
+ "ephemeral_1h_input_tokens": 0
147
+ },
148
+ "service_tier": "standard",
149
+ "inference_geo": "not_available"
150
+ },
151
+ "cacheCreationInputTokens": 0,
152
+ "stopSequence": null,
153
+ "iterations": null,
154
+ "container": null,
155
+ "contextManagement": null
156
+ }
157
+ },
158
+ "toolCalls": [
159
+ {
160
+ "type": "tool-call",
161
+ "toolCallId": "call_fixture_web_search",
162
+ "toolName": "webSearch",
163
+ "input": {
164
+ "query": "carburetor reference article"
165
+ }
166
+ }
167
+ ],
168
+ "toolResults": [
169
+ {
170
+ "type": "tool-result",
171
+ "toolCallId": "call_fixture_web_search",
172
+ "toolName": "webSearch",
173
+ "input": {
174
+ "query": "carburetor reference article"
175
+ },
176
+ "output": {
177
+ "results": [
178
+ {
179
+ "title": "Carburetor reference article",
180
+ "url": "https://example.com/carburetor-reference"
181
+ }
182
+ ]
183
+ }
184
+ }
185
+ ]
186
+ }
187
+ ],
188
+ "_output": "yes",
189
+ "totalUsage": {
190
+ "inputTokens": 217,
191
+ "inputTokenDetails": {
192
+ "noCacheTokens": 217,
193
+ "cacheReadTokens": 0,
194
+ "cacheWriteTokens": 0
195
+ },
196
+ "outputTokens": 9,
197
+ "outputTokenDetails": {},
198
+ "totalTokens": 226,
199
+ "cachedInputTokens": 0
200
+ }
201
+ }
@@ -0,0 +1,65 @@
1
+ import {
2
+ APICallError,
3
+ InvalidArgumentError,
4
+ InvalidDataContentError,
5
+ InvalidPromptError,
6
+ LoadAPIKeyError,
7
+ LoadSettingError,
8
+ NoImageGeneratedError,
9
+ NoSuchModelError,
10
+ NoSuchProviderError,
11
+ UnsupportedFunctionalityError
12
+ } from 'ai';
13
+ import { FatalError } from '@outputai/core';
14
+
15
+ const toFatalError = ( error, extraMessage = '' ) => new FatalError(
16
+ `AI-SDK fatal error${extraMessage ? ` (${extraMessage})` : ''}: ${error.message}`,
17
+ { cause: error }
18
+ );
19
+
20
+ export const mapAiError = error => {
21
+ if ( error instanceof FatalError ) {
22
+ return error;
23
+ }
24
+ if ( APICallError.isInstance( error ) && !error.isRetryable ) {
25
+ // Non-retryable API failures are already classified by AI SDK as permanent provider failures.
26
+ return toFatalError( error, error.statusCode ? `HTTP ${error.statusCode}` : '' );
27
+ }
28
+ if ( InvalidArgumentError.isInstance( error ) ) {
29
+ // Invalid call settings are deterministic caller bugs, so retrying the same activity cannot fix them.
30
+ return toFatalError( error );
31
+ }
32
+ if ( InvalidDataContentError.isInstance( error ) ) {
33
+ // Invalid media content has the wrong local shape/encoding and will fail again with the same input.
34
+ return toFatalError( error );
35
+ }
36
+ if ( InvalidPromptError.isInstance( error ) ) {
37
+ // Invalid prompt structure is a deterministic request-construction error.
38
+ return toFatalError( error );
39
+ }
40
+ if ( LoadAPIKeyError.isInstance( error ) ) {
41
+ // Missing or invalid API key configuration will not change during an activity retry.
42
+ return toFatalError( error );
43
+ }
44
+ if ( LoadSettingError.isInstance( error ) ) {
45
+ // Missing or invalid provider settings are deployment/configuration problems.
46
+ return toFatalError( error );
47
+ }
48
+ if ( NoImageGeneratedError.isInstance( error ) ) {
49
+ // Image generation completed provider calls but collected zero images; repeating identical input is not useful.
50
+ return toFatalError( error );
51
+ }
52
+ if ( NoSuchProviderError.isInstance( error ) ) {
53
+ // A missing provider id is a deterministic provider registry/configuration error.
54
+ return toFatalError( error );
55
+ }
56
+ if ( NoSuchModelError.isInstance( error ) ) {
57
+ // A missing model id is a deterministic provider/model configuration error.
58
+ return toFatalError( error );
59
+ }
60
+ if ( UnsupportedFunctionalityError.isInstance( error ) ) {
61
+ // The selected model/output mode does not support the requested feature.
62
+ return toFatalError( error );
63
+ }
64
+ return error;
65
+ };
@@ -0,0 +1,195 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ APICallError,
4
+ InvalidArgumentError,
5
+ InvalidDataContentError,
6
+ InvalidMessageRoleError,
7
+ InvalidPromptError,
8
+ InvalidToolApprovalError,
9
+ InvalidToolInputError,
10
+ LoadAPIKeyError,
11
+ LoadSettingError,
12
+ MessageConversionError,
13
+ NoImageGeneratedError,
14
+ NoOutputGeneratedError,
15
+ NoObjectGeneratedError,
16
+ NoSuchModelError,
17
+ NoSuchProviderError,
18
+ ToolCallNotFoundForApprovalError,
19
+ ToolCallRepairError,
20
+ UnsupportedFunctionalityError
21
+ } from 'ai';
22
+ import { FatalError } from '@outputai/core';
23
+ import { mapAiError } from './error_handler.js';
24
+
25
+ const makeApiCallError = ( input = {} ) => new APICallError( {
26
+ message: 'Provider rejected the request',
27
+ url: 'https://provider.test/v1/generate',
28
+ requestBodyValues: {},
29
+ responseHeaders: {},
30
+ responseBody: '{"error":"bad request"}',
31
+ ...input
32
+ } );
33
+
34
+ const fatalAiSdkErrors = [
35
+ [
36
+ 'InvalidArgumentError',
37
+ () => new InvalidArgumentError( {
38
+ parameter: 'temperature',
39
+ value: 'hot',
40
+ message: 'temperature must be a number'
41
+ } )
42
+ ],
43
+ [
44
+ 'InvalidDataContentError',
45
+ () => new InvalidDataContentError( { content: { bad: true } } )
46
+ ],
47
+ [
48
+ 'InvalidPromptError',
49
+ () => new InvalidPromptError( { prompt: {}, message: 'prompt or messages must be defined' } )
50
+ ],
51
+ [
52
+ 'LoadAPIKeyError',
53
+ () => new LoadAPIKeyError( { message: 'Missing API key' } )
54
+ ],
55
+ [
56
+ 'LoadSettingError',
57
+ () => new LoadSettingError( { message: 'Missing setting' } )
58
+ ],
59
+ [
60
+ 'NoImageGeneratedError',
61
+ () => new NoImageGeneratedError( { responses: [] } )
62
+ ],
63
+ [
64
+ 'NoSuchModelError',
65
+ () => new NoSuchModelError( { modelId: 'missing-model', modelType: 'languageModel' } )
66
+ ],
67
+ [
68
+ 'NoSuchProviderError',
69
+ () => new NoSuchProviderError( {
70
+ modelId: 'missing-provider:model',
71
+ modelType: 'languageModel',
72
+ providerId: 'missing-provider',
73
+ availableProviders: [ 'openai' ]
74
+ } )
75
+ ],
76
+ [
77
+ 'UnsupportedFunctionalityError',
78
+ () => new UnsupportedFunctionalityError( { functionality: 'image masks' } )
79
+ ]
80
+ ];
81
+
82
+ const preservedAiSdkErrors = [
83
+ [
84
+ 'InvalidMessageRoleError',
85
+ () => new InvalidMessageRoleError( { role: 'critic' } )
86
+ ],
87
+ [
88
+ 'InvalidToolApprovalError',
89
+ () => new InvalidToolApprovalError( { approvalId: 'approval-1' } )
90
+ ],
91
+ [
92
+ 'InvalidToolInputError',
93
+ () => new InvalidToolInputError( {
94
+ toolName: 'search',
95
+ toolInput: '{bad json',
96
+ cause: new Error( 'parse failed' )
97
+ } )
98
+ ],
99
+ [
100
+ 'MessageConversionError',
101
+ () => new MessageConversionError( {
102
+ originalMessage: { role: 'critic', content: 'bad role' },
103
+ message: 'Unsupported role'
104
+ } )
105
+ ],
106
+ [
107
+ 'NoObjectGeneratedError',
108
+ () => new NoObjectGeneratedError( {
109
+ text: 'not json',
110
+ cause: new Error( 'parse failed' )
111
+ } )
112
+ ],
113
+ [
114
+ 'NoOutputGeneratedError',
115
+ () => new NoOutputGeneratedError()
116
+ ],
117
+ [
118
+ 'ToolCallNotFoundForApprovalError',
119
+ () => new ToolCallNotFoundForApprovalError( {
120
+ toolCallId: 'tool-call-1',
121
+ approvalId: 'approval-1'
122
+ } )
123
+ ],
124
+ [
125
+ 'ToolCallRepairError',
126
+ () => new ToolCallRepairError( {
127
+ cause: new Error( 'repair failed' ),
128
+ originalError: new Error( 'invalid tool input' )
129
+ } )
130
+ ]
131
+ ];
132
+
133
+ describe( 'mapAiError', () => {
134
+ it( 'preserves existing FatalError instances', () => {
135
+ const error = new FatalError( 'Already fatal' );
136
+
137
+ expect( mapAiError( error ) ).toBe( error );
138
+ } );
139
+
140
+ it( 'maps non-retryable APICallError instances to FatalError', () => {
141
+ const error = makeApiCallError( {
142
+ statusCode: 400,
143
+ isRetryable: false
144
+ } );
145
+
146
+ const result = mapAiError( error );
147
+
148
+ expect( result ).toBeInstanceOf( FatalError );
149
+ expect( result.message ).toBe( 'AI-SDK fatal error (HTTP 400): Provider rejected the request' );
150
+ expect( result.cause ).toBe( error );
151
+ } );
152
+
153
+ it( 'maps non-retryable APICallError instances without status codes to FatalError', () => {
154
+ const error = makeApiCallError( {
155
+ isRetryable: false
156
+ } );
157
+
158
+ const result = mapAiError( error );
159
+
160
+ expect( result ).toBeInstanceOf( FatalError );
161
+ expect( result.message ).toBe( 'AI-SDK fatal error: Provider rejected the request' );
162
+ expect( result.cause ).toBe( error );
163
+ } );
164
+
165
+ it( 'preserves retryable APICallError instances', () => {
166
+ const error = makeApiCallError( {
167
+ statusCode: 429,
168
+ isRetryable: true
169
+ } );
170
+
171
+ expect( mapAiError( error ) ).toBe( error );
172
+ } );
173
+
174
+ it.each( fatalAiSdkErrors )( 'maps %s to FatalError', ( _name, makeError ) => {
175
+ const error = makeError();
176
+
177
+ const result = mapAiError( error );
178
+
179
+ expect( result ).toBeInstanceOf( FatalError );
180
+ expect( result.message ).toBe( `AI-SDK fatal error: ${error.message}` );
181
+ expect( result.cause ).toBe( error );
182
+ } );
183
+
184
+ it.each( preservedAiSdkErrors )( 'preserves %s for now', ( _name, makeError ) => {
185
+ const error = makeError();
186
+
187
+ expect( mapAiError( error ) ).toBe( error );
188
+ } );
189
+
190
+ it( 'preserves ordinary errors', () => {
191
+ const error = new Error( 'Network exploded' );
192
+
193
+ expect( mapAiError( error ) ).toBe( error );
194
+ } );
195
+ } );
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Get the approximate file size from a base64 string.
3
+ * @param {string} b64data
4
+ * @returns {number} Size in bytes
5
+ */
6
+ export const calculateBase64FileSize = b64data => {
7
+ const baseSize = b64data.length * ( 3 / 4 );
8
+ const paddingSize = [ b64data.at( -2 ), b64data.at( -1 ) ].filter( v => v === '=' ).length;
9
+ return baseSize - paddingSize;
10
+ };
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { calculateBase64FileSize } from './image.js';
3
+
4
+ describe( 'calculateBase64FileSize', () => {
5
+ it( 'calculates size for base64 without padding', () => {
6
+ expect( calculateBase64FileSize( 'TWFu' ) ).toBe( 3 );
7
+ } );
8
+
9
+ it( 'calculates size for base64 with one padding character', () => {
10
+ expect( calculateBase64FileSize( 'TWE=' ) ).toBe( 2 );
11
+ } );
12
+
13
+ it( 'calculates size for base64 with two padding characters', () => {
14
+ expect( calculateBase64FileSize( 'TQ==' ) ).toBe( 1 );
15
+ } );
16
+
17
+ it( 'returns zero for an empty string', () => {
18
+ expect( calculateBase64FileSize( '' ) ).toBe( 0 );
19
+ } );
20
+ } );
@@ -1,6 +1,7 @@
1
1
  import { combineSources, extractSourcesFromSteps } from './source_extraction.js';
2
2
  import { calculateLLMCallCost } from '../cost/index.js';
3
3
  import { endTraceWithSuccess } from './trace.js';
4
+ import { calculateBase64FileSize } from './image.js';
4
5
 
5
6
  /**
6
7
  * Calculates the cost and wraps an AI SDK text response in a Proxy with shortcut for 'result' and 'cost'
@@ -16,10 +17,12 @@ import { endTraceWithSuccess } from './trace.js';
16
17
  * @returns {object} Proxied response
17
18
  */
18
19
  export const wrapTextResponse = async ( { traceId, modelId, response } ) => {
19
- const sourcesFromTools = extractSourcesFromSteps( response.steps );
20
- const cost = await calculateLLMCallCost( { usage: response.totalUsage, modelId } );
20
+ const { totalUsage: usage, providerMetadata, text: result, steps, sources } = response;
21
21
 
22
- endTraceWithSuccess( { traceId, modelId, response, cost, sourcesFromTools } );
22
+ const cost = await calculateLLMCallCost( { usage, modelId } );
23
+ const sourcesFromTools = extractSourcesFromSteps( steps );
24
+
25
+ endTraceWithSuccess( { traceId, usage, cost, result, providerMetadata, sourcesFromTools } );
23
26
 
24
27
  return new Proxy( response, {
25
28
  get( target, prop, receiver ) {
@@ -30,7 +33,7 @@ export const wrapTextResponse = async ( { traceId, modelId, response } ) => {
30
33
  return cost;
31
34
  }
32
35
  if ( prop === 'sources' && sourcesFromTools.length > 0 ) {
33
- return combineSources( { sourcesFromTools, sourcesFromResponse: response.sources } );
36
+ return combineSources( { sourcesFromTools, sourcesFromResponse: sources } );
34
37
  }
35
38
  return Reflect.get( target, prop, receiver );
36
39
  }
@@ -46,25 +49,49 @@ export const wrapTextResponse = async ( { traceId, modelId, response } ) => {
46
49
  * @param {object} args
47
50
  * @param {string} args.traceId - id created by the startTrace
48
51
  * @param {string} args.modelId - id of the model used
49
- * @param {Function} args.onFinish - Original callback to call with the Proxied reponse
52
+ * @param {Function} args.onFinish - Original callback to call with the proxied response
50
53
  * @returns {object} Proxied response
51
54
  */
52
55
  export const wrapStreamOnFinishResponse = ( { traceId, modelId, onFinish: _onFinish } ) => ( {
53
56
  async onFinish( response ) {
54
- const cost = await calculateLLMCallCost( { modelId, usage: response.totalUsage } );
57
+ const proxiedResponse = await wrapTextResponse( { traceId, modelId, response } );
58
+ _onFinish?.( proxiedResponse );
59
+ }
60
+ } );
61
+
62
+ /**
63
+ * Calculates the cost and wraps an AI SDK image response in a Proxy with shortcut for 'result' and 'cost'
64
+ *
65
+ * Emits the `cost:llm:request` event.
66
+ *
67
+ * Also finishes the trace events.
68
+ *
69
+ * @param {object} args
70
+ * @param {string} args.traceId - id created by the startTrace
71
+ * @param {string} args.modelId - id of the model used
72
+ * @param {object} args.response - AI SDK's image response
73
+ * @returns {object} Proxied response
74
+ */
75
+ export const wrapImageResponse = async ( { traceId, modelId, response } ) => {
76
+ const { usage, providerMetadata } = response;
77
+ const cost = await calculateLLMCallCost( { usage, modelId } );
78
+
79
+ const result = response.images.map( ( { mediaType, base64 } ) => ( {
80
+ size: calculateBase64FileSize( base64 ),
81
+ mediaType
82
+ } ) );
55
83
 
56
- endTraceWithSuccess( { traceId, modelId, response, cost } );
84
+ endTraceWithSuccess( { traceId, usage, cost, result, providerMetadata } );
57
85
 
58
- _onFinish?.( new Proxy( response, {
59
- get( target, prop, receiver ) {
60
- if ( prop === 'result' ) {
61
- return target.text;
62
- }
63
- if ( prop === 'cost' ) {
64
- return cost;
65
- }
66
- return Reflect.get( target, prop, receiver );
86
+ return new Proxy( response, {
87
+ get( target, prop, receiver ) {
88
+ if ( prop === 'result' ) {
89
+ return target.image;
67
90
  }
68
- } ) );
69
- }
70
- } );
91
+ if ( prop === 'cost' ) {
92
+ return cost;
93
+ }
94
+ return Reflect.get( target, prop, receiver );
95
+ }
96
+ } );
97
+ };