@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
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { req1 as anthropicCacheSeed, req2 as anthropicCacheReadWrite } from '../fixtures/text_response_v7_anthropic_cache.js';
|
|
3
|
-
import anthropicStream from '../fixtures/stream_response_v7_anthropic.js';
|
|
4
|
-
import anthropicText from '../fixtures/text_response_v7_anthropic.js';
|
|
5
|
-
import googleVertexImage from '../fixtures/image_response_v7_google_vertex.js';
|
|
6
|
-
import googleVertexStream from '../fixtures/stream_response_v7_google_vertex.js';
|
|
7
|
-
import googleVertexText from '../fixtures/text_response_v7_google_vertex.js';
|
|
8
|
-
import openaiImage from '../fixtures/image_response_v7_openai.js';
|
|
9
|
-
import openaiStream from '../fixtures/stream_response_v7_openai.js';
|
|
10
|
-
import openaiText from '../fixtures/text_response_v7_openai.js';
|
|
11
|
-
import perplexityStream from '../fixtures/stream_response_v7_perplexity.js';
|
|
12
|
-
import perplexityText from '../fixtures/text_response_v7_perplexity.js';
|
|
13
|
-
import pricingTable from '../fixtures/models_api_v7_subset.json' with { type: 'json' };
|
|
14
|
-
|
|
15
|
-
const mockFetchModelsPricing = vi.hoisted( () => vi.fn() );
|
|
16
|
-
|
|
17
|
-
vi.mock( './models_pricing.js', () => ( {
|
|
18
|
-
fetchModelsPricing: ( ...args ) => mockFetchModelsPricing( ...args )
|
|
19
|
-
} ) );
|
|
20
|
-
|
|
21
|
-
import { LLMGenerationCost, LLMGenerationCostItem, calculateCosts } from './cost.js';
|
|
22
|
-
import { LLMGenerationUsageItem, parseLLMUsage } from './usage.js';
|
|
23
|
-
|
|
24
|
-
const INPUT = LLMGenerationUsageItem.Group.INPUT;
|
|
25
|
-
const OUTPUT = LLMGenerationUsageItem.Group.OUTPUT;
|
|
26
|
-
const OK = LLMGenerationCostItem.Status.OK;
|
|
27
|
-
const FALLBACK = LLMGenerationCostItem.Status.FALLBACK;
|
|
28
|
-
const MISSING = LLMGenerationCostItem.Status.MISSING;
|
|
29
|
-
|
|
30
|
-
const models = new Map(
|
|
31
|
-
Object.values( pricingTable ).flatMap( provider => Object.entries( provider.models ?? {} ).flatMap( ( [ modelId, model ] ) => (
|
|
32
|
-
model.cost ? [ [ `${provider.id}/${modelId}`, model.cost ] ] : []
|
|
33
|
-
) ) )
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const cases = [
|
|
37
|
-
{
|
|
38
|
-
name: 'OpenAI text',
|
|
39
|
-
fixture: openaiText,
|
|
40
|
-
providerId: 'openai',
|
|
41
|
-
modelId: 'gpt-4.1-mini',
|
|
42
|
-
input: 0.000414,
|
|
43
|
-
output: 0.0006336,
|
|
44
|
-
total: 0.0010476,
|
|
45
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
46
|
-
items: [
|
|
47
|
-
[ INPUT, 'no_cache', 1035, 0.4, 0.000414, OK ],
|
|
48
|
-
[ INPUT, 'cache_read', 0, 0.1, 0, OK ],
|
|
49
|
-
[ INPUT, 'cache_write', 0, 0.4, 0, FALLBACK ],
|
|
50
|
-
[ OUTPUT, 'text', 396, 1.6, 0.0006336, OK ],
|
|
51
|
-
[ OUTPUT, 'reasoning', 0, 1.6, 0, FALLBACK ]
|
|
52
|
-
]
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: 'OpenAI stream',
|
|
56
|
-
fixture: openaiStream,
|
|
57
|
-
providerId: 'openai',
|
|
58
|
-
modelId: 'gpt-4.1-mini',
|
|
59
|
-
input: 0.000414,
|
|
60
|
-
output: 0.0006176,
|
|
61
|
-
total: 0.0010316,
|
|
62
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
63
|
-
items: [
|
|
64
|
-
[ INPUT, 'no_cache', 1035, 0.4, 0.000414, OK ],
|
|
65
|
-
[ INPUT, 'cache_read', 0, 0.1, 0, OK ],
|
|
66
|
-
[ INPUT, 'cache_write', 0, 0.4, 0, FALLBACK ],
|
|
67
|
-
[ OUTPUT, 'text', 386, 1.6, 0.0006176, OK ],
|
|
68
|
-
[ OUTPUT, 'reasoning', 0, 1.6, 0, FALLBACK ]
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'OpenAI image',
|
|
73
|
-
fixture: openaiImage,
|
|
74
|
-
providerId: 'openai',
|
|
75
|
-
modelId: 'gpt-image-1',
|
|
76
|
-
input: null,
|
|
77
|
-
output: null,
|
|
78
|
-
total: null,
|
|
79
|
-
status: LLMGenerationCost.Status.INCOMPLETE,
|
|
80
|
-
items: [
|
|
81
|
-
[ INPUT, null, 151, null, null, MISSING ],
|
|
82
|
-
[ OUTPUT, null, 4160, null, null, MISSING ]
|
|
83
|
-
]
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: 'Anthropic text',
|
|
87
|
-
fixture: anthropicText,
|
|
88
|
-
providerId: 'anthropic',
|
|
89
|
-
modelId: 'claude-haiku-4-5',
|
|
90
|
-
input: 0.001113,
|
|
91
|
-
output: 0.004225,
|
|
92
|
-
total: 0.005338,
|
|
93
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
94
|
-
items: [
|
|
95
|
-
[ INPUT, 'no_cache', 1113, 1, 0.001113, OK ],
|
|
96
|
-
[ INPUT, 'cache_read', 0, 0.1, 0, OK ],
|
|
97
|
-
[ INPUT, 'cache_write', 0, 1.25, 0, OK ],
|
|
98
|
-
[ OUTPUT, null, 845, 5, 0.004225, OK ]
|
|
99
|
-
]
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: 'Anthropic stream',
|
|
103
|
-
fixture: anthropicStream,
|
|
104
|
-
providerId: 'anthropic',
|
|
105
|
-
modelId: 'claude-haiku-4-5',
|
|
106
|
-
input: 0.001113,
|
|
107
|
-
output: 0.005235,
|
|
108
|
-
total: 0.006348,
|
|
109
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
110
|
-
items: [
|
|
111
|
-
[ INPUT, 'no_cache', 1113, 1, 0.001113, OK ],
|
|
112
|
-
[ INPUT, 'cache_read', 0, 0.1, 0, OK ],
|
|
113
|
-
[ INPUT, 'cache_write', 0, 1.25, 0, OK ],
|
|
114
|
-
[ OUTPUT, null, 1047, 5, 0.005235, OK ]
|
|
115
|
-
]
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
name: 'Anthropic cache seed',
|
|
119
|
-
fixture: anthropicCacheSeed,
|
|
120
|
-
providerId: 'anthropic',
|
|
121
|
-
modelId: 'claude-haiku-4-5',
|
|
122
|
-
input: 0.01876625,
|
|
123
|
-
output: 0.000025,
|
|
124
|
-
total: 0.01879125,
|
|
125
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
126
|
-
items: [
|
|
127
|
-
[ INPUT, 'no_cache', 15, 1, 0.000015, OK ],
|
|
128
|
-
[ INPUT, 'cache_read', 0, 0.1, 0, OK ],
|
|
129
|
-
[ INPUT, 'cache_write', 15001, 1.25, 0.01875125, OK ],
|
|
130
|
-
[ OUTPUT, null, 5, 5, 0.000025, OK ]
|
|
131
|
-
]
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: 'Anthropic cache read and write',
|
|
135
|
-
fixture: anthropicCacheReadWrite,
|
|
136
|
-
providerId: 'anthropic',
|
|
137
|
-
modelId: 'claude-haiku-4-5',
|
|
138
|
-
input: 0.0065166,
|
|
139
|
-
output: 0.00002,
|
|
140
|
-
total: 0.0065366,
|
|
141
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
142
|
-
items: [
|
|
143
|
-
[ INPUT, 'no_cache', 14, 1, 0.000014, OK ],
|
|
144
|
-
[ INPUT, 'cache_read', 15001, 0.1, 0.0015001, OK ],
|
|
145
|
-
[ INPUT, 'cache_write', 4002, 1.25, 0.0050025, OK ],
|
|
146
|
-
[ OUTPUT, null, 4, 5, 0.00002, OK ]
|
|
147
|
-
]
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
name: 'Google Vertex text',
|
|
151
|
-
fixture: googleVertexText,
|
|
152
|
-
providerId: 'google-vertex',
|
|
153
|
-
modelId: 'gemini-2.5-flash',
|
|
154
|
-
input: 0.0003096,
|
|
155
|
-
output: 0.0034525,
|
|
156
|
-
total: 0.0037621,
|
|
157
|
-
status: LLMGenerationCost.Status.IMPRECISE,
|
|
158
|
-
items: [
|
|
159
|
-
[ INPUT, 'no_cache', 1032, 0.3, 0.0003096, OK ],
|
|
160
|
-
[ INPUT, 'cache_read', 0, 0.03, 0, OK ],
|
|
161
|
-
[ OUTPUT, 'text', 793, 2.5, 0.0019825, OK ],
|
|
162
|
-
[ OUTPUT, 'reasoning', 588, 2.5, 0.00147, FALLBACK ]
|
|
163
|
-
]
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
name: 'Google Vertex stream',
|
|
167
|
-
fixture: googleVertexStream,
|
|
168
|
-
providerId: 'google-vertex',
|
|
169
|
-
modelId: 'gemini-2.5-flash',
|
|
170
|
-
input: 0.0003096,
|
|
171
|
-
output: 0.0033675,
|
|
172
|
-
total: 0.0036771,
|
|
173
|
-
status: LLMGenerationCost.Status.IMPRECISE,
|
|
174
|
-
items: [
|
|
175
|
-
[ INPUT, 'no_cache', 1032, 0.3, 0.0003096, OK ],
|
|
176
|
-
[ INPUT, 'cache_read', 0, 0.03, 0, OK ],
|
|
177
|
-
[ OUTPUT, 'text', 858, 2.5, 0.002145, OK ],
|
|
178
|
-
[ OUTPUT, 'reasoning', 489, 2.5, 0.0012225, FALLBACK ]
|
|
179
|
-
]
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
name: 'Google Vertex image',
|
|
183
|
-
fixture: googleVertexImage,
|
|
184
|
-
providerId: 'google-vertex',
|
|
185
|
-
modelId: 'gemini-2.5-flash-image',
|
|
186
|
-
input: 0.0000471,
|
|
187
|
-
output: 0.0387,
|
|
188
|
-
total: 0.0387471,
|
|
189
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
190
|
-
items: [
|
|
191
|
-
[ INPUT, null, 157, 0.3, 0.0000471, OK ],
|
|
192
|
-
[ OUTPUT, null, 1290, 30, 0.0387, OK ]
|
|
193
|
-
]
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
name: 'Perplexity text',
|
|
197
|
-
fixture: perplexityText,
|
|
198
|
-
providerId: 'perplexity',
|
|
199
|
-
modelId: 'sonar',
|
|
200
|
-
input: 0.001025,
|
|
201
|
-
output: 0.000437,
|
|
202
|
-
total: 0.001462,
|
|
203
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
204
|
-
items: [
|
|
205
|
-
[ INPUT, 'no_cache', 1025, 1, 0.001025, OK ],
|
|
206
|
-
[ OUTPUT, 'text', 437, 1, 0.000437, OK ],
|
|
207
|
-
[ OUTPUT, 'reasoning', 0, 1, 0, FALLBACK ]
|
|
208
|
-
]
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
name: 'Perplexity stream',
|
|
212
|
-
fixture: perplexityStream,
|
|
213
|
-
providerId: 'perplexity',
|
|
214
|
-
modelId: 'sonar',
|
|
215
|
-
input: 0.001025,
|
|
216
|
-
output: 0.000448,
|
|
217
|
-
total: 0.001473,
|
|
218
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
219
|
-
items: [
|
|
220
|
-
[ INPUT, 'no_cache', 1025, 1, 0.001025, OK ],
|
|
221
|
-
[ OUTPUT, 'text', 448, 1, 0.000448, OK ],
|
|
222
|
-
[ OUTPUT, 'reasoning', 0, 1, 0, FALLBACK ]
|
|
223
|
-
]
|
|
224
|
-
}
|
|
225
|
-
];
|
|
226
|
-
|
|
227
|
-
describe( 'calculateCosts with AI SDK response fixtures', () => {
|
|
228
|
-
beforeEach( () => {
|
|
229
|
-
mockFetchModelsPricing.mockReset();
|
|
230
|
-
mockFetchModelsPricing.mockResolvedValue( models );
|
|
231
|
-
} );
|
|
232
|
-
|
|
233
|
-
it.each( cases )( 'calculates $name cost', async ( {
|
|
234
|
-
fixture,
|
|
235
|
-
providerId,
|
|
236
|
-
modelId,
|
|
237
|
-
input,
|
|
238
|
-
output,
|
|
239
|
-
total,
|
|
240
|
-
status,
|
|
241
|
-
items
|
|
242
|
-
} ) => {
|
|
243
|
-
const usage = parseLLMUsage( {
|
|
244
|
-
prompt: {
|
|
245
|
-
config: {
|
|
246
|
-
provider: providerId,
|
|
247
|
-
model: modelId
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
usage: fixture.usage
|
|
251
|
-
} );
|
|
252
|
-
const result = await calculateCosts( usage );
|
|
253
|
-
|
|
254
|
-
expect( JSON.parse( JSON.stringify( result ) ) ).toEqual( {
|
|
255
|
-
type: LLMGenerationCost.TYPE,
|
|
256
|
-
providerId,
|
|
257
|
-
modelId,
|
|
258
|
-
input,
|
|
259
|
-
output,
|
|
260
|
-
total,
|
|
261
|
-
status,
|
|
262
|
-
items: items.map( ( [ group, label, amount, ppm, itemTotal, itemStatus ] ) => ( {
|
|
263
|
-
group,
|
|
264
|
-
label,
|
|
265
|
-
amount,
|
|
266
|
-
ppm,
|
|
267
|
-
total: itemTotal,
|
|
268
|
-
status: itemStatus
|
|
269
|
-
} ) )
|
|
270
|
-
} );
|
|
271
|
-
} );
|
|
272
|
-
} );
|
package/src/utils/cost.spec.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const mockFetchModelsPricing = vi.hoisted( () => vi.fn() );
|
|
4
|
-
|
|
5
|
-
vi.mock( './models_pricing.js', () => ( {
|
|
6
|
-
fetchModelsPricing: ( ...args ) => mockFetchModelsPricing( ...args )
|
|
7
|
-
} ) );
|
|
8
|
-
|
|
9
|
-
import { Logger } from '@outputai/core';
|
|
10
|
-
import { LLMGenerationCost, LLMGenerationCostItem, calculateCosts } from './cost.js';
|
|
11
|
-
import { LLMGenerationUsage, LLMGenerationUsageItem } from './usage.js';
|
|
12
|
-
|
|
13
|
-
const INPUT = LLMGenerationUsageItem.Group.INPUT;
|
|
14
|
-
const OUTPUT = LLMGenerationUsageItem.Group.OUTPUT;
|
|
15
|
-
const MODEL_ID = 'test-model';
|
|
16
|
-
const PROVIDER_ID = 'test-provider';
|
|
17
|
-
|
|
18
|
-
const item = ( group, label, amount ) => new LLMGenerationUsageItem( group, label, amount );
|
|
19
|
-
const usage = items => new LLMGenerationUsage( MODEL_ID, PROVIDER_ID, items );
|
|
20
|
-
const pricing = value => new Map( [ [ `${PROVIDER_ID}/${MODEL_ID}`, value ] ] );
|
|
21
|
-
const serialize = value => JSON.parse( JSON.stringify( value ) );
|
|
22
|
-
|
|
23
|
-
describe( 'calculateCosts', () => {
|
|
24
|
-
beforeEach( () => {
|
|
25
|
-
mockFetchModelsPricing.mockReset();
|
|
26
|
-
vi.spyOn( Logger, 'warn' ).mockImplementation( () => {} );
|
|
27
|
-
} );
|
|
28
|
-
|
|
29
|
-
afterEach( () => {
|
|
30
|
-
vi.restoreAllMocks();
|
|
31
|
-
} );
|
|
32
|
-
|
|
33
|
-
it( 'returns null when model pricing cannot be fetched', async () => {
|
|
34
|
-
mockFetchModelsPricing.mockResolvedValue( null );
|
|
35
|
-
|
|
36
|
-
const result = await calculateCosts( usage( [
|
|
37
|
-
item( INPUT, null, 100 ),
|
|
38
|
-
item( OUTPUT, null, 50 )
|
|
39
|
-
] ) );
|
|
40
|
-
|
|
41
|
-
expect( result ).toBeNull();
|
|
42
|
-
expect( Logger.warn ).toHaveBeenCalledWith( 'Failed to fetch models pricing', { namespace: 'LLM' } );
|
|
43
|
-
} );
|
|
44
|
-
|
|
45
|
-
it( 'calculates precise aggregate input and output costs', async () => {
|
|
46
|
-
mockFetchModelsPricing.mockResolvedValue( pricing( {
|
|
47
|
-
input: 2,
|
|
48
|
-
output: 10
|
|
49
|
-
} ) );
|
|
50
|
-
|
|
51
|
-
const result = await calculateCosts( usage( [
|
|
52
|
-
item( INPUT, null, 1_000_000 ),
|
|
53
|
-
item( OUTPUT, null, 500_000 )
|
|
54
|
-
] ) );
|
|
55
|
-
|
|
56
|
-
expect( result ).toBeInstanceOf( LLMGenerationCost );
|
|
57
|
-
expect( serialize( result ) ).toEqual( {
|
|
58
|
-
type: LLMGenerationCost.TYPE,
|
|
59
|
-
providerId: PROVIDER_ID,
|
|
60
|
-
modelId: MODEL_ID,
|
|
61
|
-
input: 2,
|
|
62
|
-
output: 5,
|
|
63
|
-
total: 7,
|
|
64
|
-
status: LLMGenerationCost.Status.PRECISE,
|
|
65
|
-
items: [
|
|
66
|
-
{
|
|
67
|
-
group: INPUT,
|
|
68
|
-
label: null,
|
|
69
|
-
amount: 1_000_000,
|
|
70
|
-
ppm: 2,
|
|
71
|
-
total: 2,
|
|
72
|
-
status: LLMGenerationCostItem.Status.OK
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
group: OUTPUT,
|
|
76
|
-
label: null,
|
|
77
|
-
amount: 500_000,
|
|
78
|
-
ppm: 10,
|
|
79
|
-
total: 5,
|
|
80
|
-
status: LLMGenerationCostItem.Status.OK
|
|
81
|
-
}
|
|
82
|
-
]
|
|
83
|
-
} );
|
|
84
|
-
} );
|
|
85
|
-
|
|
86
|
-
it( 'uses specialized cache and reasoning prices when available', async () => {
|
|
87
|
-
mockFetchModelsPricing.mockResolvedValue( pricing( {
|
|
88
|
-
input: 4,
|
|
89
|
-
cache_read: 1,
|
|
90
|
-
cache_write: 5,
|
|
91
|
-
output: 10,
|
|
92
|
-
reasoning: 20
|
|
93
|
-
} ) );
|
|
94
|
-
|
|
95
|
-
const result = await calculateCosts( usage( [
|
|
96
|
-
item( INPUT, 'no_cache', 500_000 ),
|
|
97
|
-
item( INPUT, 'cache_read', 400_000 ),
|
|
98
|
-
item( INPUT, 'cache_write', 100_000 ),
|
|
99
|
-
item( OUTPUT, 'text', 200_000 ),
|
|
100
|
-
item( OUTPUT, 'reasoning', 50_000 )
|
|
101
|
-
] ) );
|
|
102
|
-
|
|
103
|
-
expect( result ).toMatchObject( {
|
|
104
|
-
input: 2.9,
|
|
105
|
-
output: 3,
|
|
106
|
-
total: 5.9,
|
|
107
|
-
status: LLMGenerationCost.Status.PRECISE
|
|
108
|
-
} );
|
|
109
|
-
expect( result.items ).toEqual( [
|
|
110
|
-
new LLMGenerationCostItem( INPUT, 'no_cache', 500_000, 4, 2, LLMGenerationCostItem.Status.OK ),
|
|
111
|
-
new LLMGenerationCostItem( INPUT, 'cache_read', 400_000, 1, 0.4, LLMGenerationCostItem.Status.OK ),
|
|
112
|
-
new LLMGenerationCostItem( INPUT, 'cache_write', 100_000, 5, 0.5, LLMGenerationCostItem.Status.OK ),
|
|
113
|
-
new LLMGenerationCostItem( OUTPUT, 'text', 200_000, 10, 2, LLMGenerationCostItem.Status.OK ),
|
|
114
|
-
new LLMGenerationCostItem( OUTPUT, 'reasoning', 50_000, 20, 1, LLMGenerationCostItem.Status.OK )
|
|
115
|
-
] );
|
|
116
|
-
} );
|
|
117
|
-
|
|
118
|
-
it( 'falls back to aggregate prices for unpriced specialized items', async () => {
|
|
119
|
-
mockFetchModelsPricing.mockResolvedValue( pricing( {
|
|
120
|
-
input: 2,
|
|
121
|
-
output: 10
|
|
122
|
-
} ) );
|
|
123
|
-
|
|
124
|
-
const result = await calculateCosts( usage( [
|
|
125
|
-
item( INPUT, 'cache_read', 400_000 ),
|
|
126
|
-
item( INPUT, 'cache_write', 100_000 ),
|
|
127
|
-
item( OUTPUT, 'reasoning', 50_000 )
|
|
128
|
-
] ) );
|
|
129
|
-
|
|
130
|
-
expect( result ).toMatchObject( {
|
|
131
|
-
input: 1,
|
|
132
|
-
output: 0.5,
|
|
133
|
-
total: 1.5,
|
|
134
|
-
status: LLMGenerationCost.Status.IMPRECISE
|
|
135
|
-
} );
|
|
136
|
-
expect( result.items.map( value => value.status ) ).toEqual( [
|
|
137
|
-
LLMGenerationCostItem.Status.FALLBACK,
|
|
138
|
-
LLMGenerationCostItem.Status.FALLBACK,
|
|
139
|
-
LLMGenerationCostItem.Status.FALLBACK
|
|
140
|
-
] );
|
|
141
|
-
} );
|
|
142
|
-
|
|
143
|
-
it( 'marks cost incomplete while preserving totals for priced items', async () => {
|
|
144
|
-
mockFetchModelsPricing.mockResolvedValue( pricing( {
|
|
145
|
-
input: 2
|
|
146
|
-
} ) );
|
|
147
|
-
|
|
148
|
-
const result = await calculateCosts( usage( [
|
|
149
|
-
item( INPUT, null, 100 ),
|
|
150
|
-
item( OUTPUT, null, 50 )
|
|
151
|
-
] ) );
|
|
152
|
-
|
|
153
|
-
expect( result ).toMatchObject( {
|
|
154
|
-
input: 0.0002,
|
|
155
|
-
output: null,
|
|
156
|
-
total: 0.0002,
|
|
157
|
-
status: LLMGenerationCost.Status.INCOMPLETE
|
|
158
|
-
} );
|
|
159
|
-
expect( result.items ).toEqual( [
|
|
160
|
-
new LLMGenerationCostItem( INPUT, null, 100, 2, 0.0002, LLMGenerationCostItem.Status.OK ),
|
|
161
|
-
new LLMGenerationCostItem( OUTPUT, null, 50, null, null, LLMGenerationCostItem.Status.MISSING )
|
|
162
|
-
] );
|
|
163
|
-
} );
|
|
164
|
-
|
|
165
|
-
it( 'returns an incomplete cost when the model has no pricing reference', async () => {
|
|
166
|
-
mockFetchModelsPricing.mockResolvedValue( new Map() );
|
|
167
|
-
|
|
168
|
-
const result = await calculateCosts( usage( [
|
|
169
|
-
item( INPUT, null, 100 ),
|
|
170
|
-
item( OUTPUT, null, 50 )
|
|
171
|
-
] ) );
|
|
172
|
-
|
|
173
|
-
expect( result ).toMatchObject( {
|
|
174
|
-
input: null,
|
|
175
|
-
output: null,
|
|
176
|
-
total: null,
|
|
177
|
-
status: LLMGenerationCost.Status.INCOMPLETE
|
|
178
|
-
} );
|
|
179
|
-
expect( result.items.every( value => value.status === LLMGenerationCostItem.Status.MISSING ) ).toBe( true );
|
|
180
|
-
expect( Logger.warn ).toHaveBeenCalledWith(
|
|
181
|
-
'Missing pricing reference for model',
|
|
182
|
-
{ namespace: 'LLM', modelId: MODEL_ID, providerId: PROVIDER_ID }
|
|
183
|
-
);
|
|
184
|
-
} );
|
|
185
|
-
|
|
186
|
-
it( 'marks cost incomplete when usage is incomplete', async () => {
|
|
187
|
-
mockFetchModelsPricing.mockResolvedValue( pricing( {
|
|
188
|
-
input: 2,
|
|
189
|
-
output: 10
|
|
190
|
-
} ) );
|
|
191
|
-
|
|
192
|
-
const result = await calculateCosts( usage( [
|
|
193
|
-
item( INPUT, null, 100 )
|
|
194
|
-
] ) );
|
|
195
|
-
|
|
196
|
-
expect( result ).toMatchObject( {
|
|
197
|
-
input: 0.0002,
|
|
198
|
-
output: null,
|
|
199
|
-
total: 0.0002,
|
|
200
|
-
status: LLMGenerationCost.Status.INCOMPLETE
|
|
201
|
-
} );
|
|
202
|
-
expect( result.items[0].status ).toBe( LLMGenerationCostItem.Status.OK );
|
|
203
|
-
} );
|
|
204
|
-
|
|
205
|
-
it( 'accepts zero-valued prices as precise', async () => {
|
|
206
|
-
mockFetchModelsPricing.mockResolvedValue( pricing( {
|
|
207
|
-
input: 0,
|
|
208
|
-
output: 0
|
|
209
|
-
} ) );
|
|
210
|
-
|
|
211
|
-
const result = await calculateCosts( usage( [
|
|
212
|
-
item( INPUT, null, 100 ),
|
|
213
|
-
item( OUTPUT, null, 50 )
|
|
214
|
-
] ) );
|
|
215
|
-
|
|
216
|
-
expect( result ).toMatchObject( {
|
|
217
|
-
input: 0,
|
|
218
|
-
output: 0,
|
|
219
|
-
total: 0,
|
|
220
|
-
status: LLMGenerationCost.Status.PRECISE
|
|
221
|
-
} );
|
|
222
|
-
expect( result.items.every( value => value.status === LLMGenerationCostItem.Status.OK ) ).toBe( true );
|
|
223
|
-
} );
|
|
224
|
-
|
|
225
|
-
it( 'uses decimal arithmetic for fractional prices', async () => {
|
|
226
|
-
mockFetchModelsPricing.mockResolvedValue( pricing( {
|
|
227
|
-
input: 0.1,
|
|
228
|
-
output: 0.2
|
|
229
|
-
} ) );
|
|
230
|
-
|
|
231
|
-
const result = await calculateCosts( usage( [
|
|
232
|
-
item( INPUT, null, 1 ),
|
|
233
|
-
item( INPUT, null, 2 ),
|
|
234
|
-
item( OUTPUT, null, 3 )
|
|
235
|
-
] ) );
|
|
236
|
-
|
|
237
|
-
expect( result.input ).toBe( 0.0000003 );
|
|
238
|
-
expect( result.output ).toBe( 0.0000006 );
|
|
239
|
-
expect( result.total ).toBe( 0.0000009 );
|
|
240
|
-
} );
|
|
241
|
-
} );
|
|
242
|
-
|
|
243
|
-
describe( 'LLMGenerationCost', () => {
|
|
244
|
-
it.each( [
|
|
245
|
-
{
|
|
246
|
-
name: 'precise',
|
|
247
|
-
usageStatus: LLMGenerationUsage.Status.COMPLETE,
|
|
248
|
-
itemStatus: LLMGenerationCostItem.Status.OK,
|
|
249
|
-
expected: LLMGenerationCost.Status.PRECISE
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
name: 'imprecise',
|
|
253
|
-
usageStatus: LLMGenerationUsage.Status.COMPLETE,
|
|
254
|
-
itemStatus: LLMGenerationCostItem.Status.FALLBACK,
|
|
255
|
-
expected: LLMGenerationCost.Status.IMPRECISE
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
name: 'incomplete from missing pricing',
|
|
259
|
-
usageStatus: LLMGenerationUsage.Status.COMPLETE,
|
|
260
|
-
itemStatus: LLMGenerationCostItem.Status.MISSING,
|
|
261
|
-
expected: LLMGenerationCost.Status.INCOMPLETE
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
name: 'incomplete from usage',
|
|
265
|
-
usageStatus: LLMGenerationUsage.Status.INCOMPLETE,
|
|
266
|
-
itemStatus: LLMGenerationCostItem.Status.OK,
|
|
267
|
-
expected: LLMGenerationCost.Status.INCOMPLETE
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
name: 'precise with zero-value fallback pricing',
|
|
271
|
-
usageStatus: LLMGenerationUsage.Status.COMPLETE,
|
|
272
|
-
itemStatus: LLMGenerationCostItem.Status.FALLBACK,
|
|
273
|
-
amount: 0,
|
|
274
|
-
expected: LLMGenerationCost.Status.PRECISE
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
name: 'precise with zero-value missing pricing',
|
|
278
|
-
usageStatus: LLMGenerationUsage.Status.COMPLETE,
|
|
279
|
-
itemStatus: LLMGenerationCostItem.Status.MISSING,
|
|
280
|
-
amount: 0,
|
|
281
|
-
expected: LLMGenerationCost.Status.PRECISE
|
|
282
|
-
}
|
|
283
|
-
] )( 'sets $name status', ( { usageStatus, itemStatus, amount = 100, expected } ) => {
|
|
284
|
-
const cost = new LLMGenerationCost( MODEL_ID, PROVIDER_ID, [
|
|
285
|
-
new LLMGenerationCostItem( INPUT, null, amount, 2, 0.0002, itemStatus )
|
|
286
|
-
], usageStatus );
|
|
287
|
-
|
|
288
|
-
expect( cost.status ).toBe( expected );
|
|
289
|
-
} );
|
|
290
|
-
} );
|