@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.
Files changed (47) hide show
  1. package/package.json +7 -3
  2. package/src/agent.js +7 -2
  3. package/src/generate.js +1 -0
  4. package/src/index.d.ts +14 -6
  5. package/src/utils/wrap.js +35 -2
  6. package/src/agent.spec.js +0 -595
  7. package/src/ai_provider.spec.js +0 -217
  8. package/src/ai_sdk_options.spec.js +0 -390
  9. package/src/fixtures/image_response_v7_google_vertex.js +0 -72
  10. package/src/fixtures/image_response_v7_openai.js +0 -77
  11. package/src/fixtures/models_api_light.json +0 -675
  12. package/src/fixtures/models_api_v7_subset.json +0 -259
  13. package/src/fixtures/stream_response_v7_anthropic.js +0 -389
  14. package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
  15. package/src/fixtures/stream_response_v7_openai.js +0 -353
  16. package/src/fixtures/stream_response_v7_perplexity.js +0 -671
  17. package/src/fixtures/text_response_v7_anthropic.js +0 -359
  18. package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
  19. package/src/fixtures/text_response_v7_google_vertex.js +0 -435
  20. package/src/fixtures/text_response_v7_openai.js +0 -347
  21. package/src/fixtures/text_response_v7_perplexity.js +0 -764
  22. package/src/generate.spec.js +0 -525
  23. package/src/prompt/content.spec.js +0 -208
  24. package/src/prompt/interpolations.spec.js +0 -109
  25. package/src/prompt/loader.full.spec.js +0 -309
  26. package/src/prompt/loader.spec.js +0 -258
  27. package/src/prompt/markup/attributes.spec.js +0 -132
  28. package/src/prompt/markup/nodes.spec.js +0 -333
  29. package/src/prompt/markup/tokenizer.spec.js +0 -149
  30. package/src/prompt/markup/tokens.spec.js +0 -250
  31. package/src/prompt/validations.spec.js +0 -812
  32. package/src/utils/cost.full.spec.js +0 -272
  33. package/src/utils/cost.spec.js +0 -290
  34. package/src/utils/error_handler.spec.js +0 -230
  35. package/src/utils/file.spec.js +0 -89
  36. package/src/utils/image.spec.js +0 -20
  37. package/src/utils/legacy_cost_attribute.spec.js +0 -278
  38. package/src/utils/models.spec.js +0 -119
  39. package/src/utils/models_pricing.spec.js +0 -162
  40. package/src/utils/skills.spec.js +0 -168
  41. package/src/utils/sources.spec.js +0 -122
  42. package/src/utils/stream.spec.js +0 -55
  43. package/src/utils/tools.spec.js +0 -167
  44. package/src/utils/usage.full.spec.js +0 -215
  45. package/src/utils/usage.spec.js +0 -246
  46. package/src/utils/wrap.spec.js +0 -467
  47. package/src/validations.spec.js +0 -631
@@ -1,230 +0,0 @@
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, TransparentFatalError } 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( 'preserves NoObjectGeneratedError with its complete schema issue details', () => {
141
- class ZodError extends Error {
142
- constructor( issues ) {
143
- super( 'schema failed' );
144
- this.issues = issues;
145
- }
146
- }
147
- const zodError = new ZodError( [
148
- {
149
- path: [ 'items', 0, 'title' ],
150
- message: 'Expected string'
151
- }
152
- ] );
153
- const validationError = new Error( 'validation failed', { cause: zodError } );
154
- const error = new NoObjectGeneratedError( {
155
- message: 'No object generated: response did not match schema.',
156
- text: '{"items":[{}]}',
157
- response: { id: 'response-1' },
158
- usage: { totalTokens: 10 },
159
- finishReason: 'stop',
160
- cause: validationError
161
- } );
162
-
163
- expect( mapAiError( error ) ).toBe( error );
164
- expect( error.cause.cause ).toBe( zodError );
165
- expect( error.cause.cause.issues ).toEqual( zodError.issues );
166
- } );
167
-
168
- it( 'maps non-retryable APICallError instances to TransparentFatalError', () => {
169
- const error = makeApiCallError( {
170
- statusCode: 400,
171
- isRetryable: false
172
- } );
173
-
174
- const result = mapAiError( error );
175
-
176
- expect( result ).toBeInstanceOf( TransparentFatalError );
177
- expect( result.cause ).toBe( error );
178
- } );
179
-
180
- it( 'maps non-retryable APICallError instances without status codes to TransparentFatalError', () => {
181
- const error = makeApiCallError( {
182
- isRetryable: false
183
- } );
184
-
185
- const result = mapAiError( error );
186
-
187
- expect( result ).toBeInstanceOf( TransparentFatalError );
188
- expect( result.cause ).toBe( error );
189
- } );
190
-
191
- it( 'preserves retryable APICallError instances', () => {
192
- const error = makeApiCallError( {
193
- statusCode: 429,
194
- isRetryable: true
195
- } );
196
-
197
- expect( mapAiError( error ) ).toBe( error );
198
- } );
199
-
200
- it( 'preserves Anthropic grammar compilation timeouts as retryable activity failures', () => {
201
- const error = makeApiCallError( {
202
- message: 'Grammar compilation timed out.',
203
- statusCode: 400,
204
- isRetryable: false
205
- } );
206
-
207
- expect( mapAiError( error ) ).toBe( error );
208
- } );
209
-
210
- it.each( fatalAiSdkErrors )( 'maps %s to TransparentFatalError', ( _name, makeError ) => {
211
- const error = makeError();
212
-
213
- const result = mapAiError( error );
214
-
215
- expect( result ).toBeInstanceOf( TransparentFatalError );
216
- expect( result.cause ).toBe( error );
217
- } );
218
-
219
- it.each( preservedAiSdkErrors )( 'preserves %s for now', ( _name, makeError ) => {
220
- const error = makeError();
221
-
222
- expect( mapAiError( error ) ).toBe( error );
223
- } );
224
-
225
- it( 'preserves ordinary errors', () => {
226
- const error = new Error( 'Network exploded' );
227
-
228
- expect( mapAiError( error ) ).toBe( error );
229
- } );
230
- } );
@@ -1,89 +0,0 @@
1
- import { afterEach, describe, expect, it } from 'vitest';
2
- import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { FatalError } from '@outputai/core';
6
- import { searchAndReadFile } from './file.js';
7
-
8
- const dirs = [];
9
-
10
- const tempDir = () => {
11
- const dir = mkdtempSync( join( tmpdir(), 'search-and-read-' ) );
12
- dirs.push( dir );
13
- return dir;
14
- };
15
-
16
- afterEach( () => {
17
- for ( const dir of dirs.splice( 0 ) ) {
18
- rmSync( dir, { recursive: true, force: true } );
19
- }
20
- } );
21
-
22
- describe( 'searchAndReadFile', () => {
23
- it( 'reads a file in the search directory', () => {
24
- const dir = tempDir();
25
- writeFileSync( join( dir, 'notes.txt' ), 'hello' );
26
-
27
- expect( searchAndReadFile( dir, 'notes.txt' ) ).toEqual( {
28
- dir,
29
- content: 'hello'
30
- } );
31
- } );
32
-
33
- it( 'reads a file from a nested subdirectory and returns that directory', () => {
34
- const dir = tempDir();
35
- const nested = join( dir, 'prompts' );
36
- mkdirSync( nested );
37
- writeFileSync( join( nested, 'writer@v1.prompt' ), 'Write it.' );
38
-
39
- expect( searchAndReadFile( dir, 'writer@v1.prompt' ) ).toEqual( {
40
- dir: nested,
41
- content: 'Write it.'
42
- } );
43
- } );
44
-
45
- it( 'continues into later siblings after a subdirectory miss', () => {
46
- const dir = tempDir();
47
- mkdirSync( join( dir, 'empty' ) );
48
- const found = join( dir, 'found' );
49
- mkdirSync( found );
50
- writeFileSync( join( found, 'target.txt' ), 'here' );
51
-
52
- expect( searchAndReadFile( dir, 'target.txt' ) ).toEqual( {
53
- dir: found,
54
- content: 'here'
55
- } );
56
- } );
57
-
58
- it( 'skips symbolic-link directories', () => {
59
- const dir = tempDir();
60
- const hidden = join( dir, 'hidden' );
61
- const search = join( dir, 'search' );
62
- mkdirSync( hidden );
63
- mkdirSync( search );
64
- writeFileSync( join( hidden, 'secret.txt' ), 'nope' );
65
- symlinkSync( hidden, join( search, 'link' ) );
66
-
67
- expect( searchAndReadFile( search, 'secret.txt' ) ).toBeNull();
68
- } );
69
-
70
- it( 'returns null when the file is not found', () => {
71
- const dir = tempDir();
72
- writeFileSync( join( dir, 'other.txt' ), 'x' );
73
-
74
- expect( searchAndReadFile( dir, 'missing.txt' ) ).toBeNull();
75
- } );
76
-
77
- it( 'returns null for an empty directory', () => {
78
- expect( searchAndReadFile( tempDir(), 'missing.txt' ) ).toBeNull();
79
- } );
80
-
81
- it( 'throws FatalError when the search directory cannot be scanned', () => {
82
- const missing = join( tempDir(), 'gone' );
83
-
84
- expect( () => searchAndReadFile( missing, 'notes.txt' ) ).toThrow( FatalError );
85
- expect( () => searchAndReadFile( missing, 'notes.txt' ) ).toThrow(
86
- `Error scanning directory "${missing}"`
87
- );
88
- } );
89
- } );
@@ -1,20 +0,0 @@
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,278 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { LLMUsageLegacy, convertCostToLegacy } from './legacy_cost_attribute.js';
3
-
4
- const MODEL_ID = 'test-model';
5
- const INPUT = 'input';
6
- const OUTPUT = 'output';
7
- const OK = 'ok';
8
- const FALLBACK = 'fallback';
9
- const MISSING = 'missing';
10
-
11
- const item = ( group, label, amount, ppm, total, status = OK ) => ( {
12
- group,
13
- label,
14
- amount,
15
- ppm,
16
- total,
17
- status
18
- } );
19
-
20
- const cost = items => ( { modelId: MODEL_ID, items } );
21
-
22
- describe( 'convertCostToLegacy', () => {
23
- it( 'returns a legacy usage attribute instance', () => {
24
- const result = convertCostToLegacy( cost( [
25
- item( INPUT, null, 100, 2, 0.0002 )
26
- ] ) );
27
-
28
- expect( result ).toBeInstanceOf( LLMUsageLegacy );
29
- } );
30
-
31
- it( 'returns null without a cost', () => {
32
- expect( convertCostToLegacy( null ) ).toBeNull();
33
- } );
34
-
35
- it( 'returns null when there are no cost items', () => {
36
- expect( convertCostToLegacy( cost( [] ) ) ).toBeNull();
37
- } );
38
-
39
- it( 'returns null when every cost item is missing', () => {
40
- expect( convertCostToLegacy( cost( [
41
- item( INPUT, null, 100, null, null, MISSING ),
42
- item( INPUT, 'cache_read', 20, null, null, MISSING ),
43
- item( OUTPUT, null, 50, null, null, MISSING )
44
- ] ) ) ).toBeNull();
45
- } );
46
-
47
- it( 'converts aggregate input and output items', () => {
48
- expect( convertCostToLegacy( cost( [
49
- item( INPUT, null, 100, 2, 0.0002 ),
50
- item( OUTPUT, null, 50, 10, 0.0005 )
51
- ] ) ) ).toEqual( {
52
- type: 'llm:usage',
53
- modelId: MODEL_ID,
54
- usage: [
55
- { type: 'input', ppm: 2, amount: 100, total: 0.0002 },
56
- { type: 'output', ppm: 10, amount: 50, total: 0.0005 }
57
- ],
58
- total: 0.0007,
59
- tokensUsed: 150
60
- } );
61
- } );
62
-
63
- it( 'converts every detailed input and output item', () => {
64
- expect( convertCostToLegacy( cost( [
65
- item( INPUT, 'no_cache', 500_000, 4, 2 ),
66
- item( INPUT, 'cache_read', 400_000, 1, 0.4 ),
67
- item( INPUT, 'cache_write', 100_000, 5, 0.5 ),
68
- item( OUTPUT, 'text', 200_000, 10, 2 ),
69
- item( OUTPUT, 'reasoning', 50_000, 20, 1 )
70
- ] ) ) ).toEqual( {
71
- type: 'llm:usage',
72
- modelId: MODEL_ID,
73
- usage: [
74
- { type: 'input', ppm: 4, amount: 600_000, total: 2.4 },
75
- { type: 'input_cached', ppm: 1, amount: 400_000, total: 0.4 },
76
- { type: 'output', ppm: 10, amount: 200_000, total: 2 },
77
- { type: 'reasoning', ppm: 20, amount: 50_000, total: 1 }
78
- ],
79
- total: 5.8,
80
- tokensUsed: 1_250_000
81
- } );
82
- } );
83
-
84
- it( 'converts no-cache input without other input details', () => {
85
- expect( convertCostToLegacy( cost( [
86
- item( INPUT, 'no_cache', 100, 2, 0.0002 )
87
- ] ) ) ).toEqual( {
88
- type: 'llm:usage',
89
- modelId: MODEL_ID,
90
- usage: [
91
- { type: 'input', ppm: 2, amount: 100, total: 0.0002 }
92
- ],
93
- total: 0.0002,
94
- tokensUsed: 100
95
- } );
96
- } );
97
-
98
- it( 'converts cache-write input without a no-cache item', () => {
99
- expect( convertCostToLegacy( cost( [
100
- item( INPUT, 'cache_write', 100, 5, 0.0005 )
101
- ] ) ) ).toEqual( {
102
- type: 'llm:usage',
103
- modelId: MODEL_ID,
104
- usage: [
105
- { type: 'input', ppm: 5, amount: 100, total: 0.0005 }
106
- ],
107
- total: 0.0005,
108
- tokensUsed: 100
109
- } );
110
- } );
111
-
112
- it( 'uses no-cache ppm when folding cache-write input', () => {
113
- expect( convertCostToLegacy( cost( [
114
- item( INPUT, 'no_cache', 0, 2, 0 ),
115
- item( INPUT, 'cache_write', 100, 5, 0.0005 )
116
- ] ) ) ).toEqual( {
117
- type: 'llm:usage',
118
- modelId: MODEL_ID,
119
- usage: [
120
- { type: 'input', ppm: 2, amount: 100, total: 0.0002 }
121
- ],
122
- total: 0.0002,
123
- tokensUsed: 100
124
- } );
125
- } );
126
-
127
- it( 'converts exactly priced cache reads', () => {
128
- expect( convertCostToLegacy( cost( [
129
- item( INPUT, 'cache_read', 200, 1, 0.0002 )
130
- ] ) ) ).toEqual( {
131
- type: 'llm:usage',
132
- modelId: MODEL_ID,
133
- usage: [
134
- { type: 'input_cached', ppm: 1, amount: 200, total: 0.0002 }
135
- ],
136
- total: 0.0002,
137
- tokensUsed: 200
138
- } );
139
- } );
140
-
141
- it.each( [ FALLBACK, MISSING ] )(
142
- 'converts %s cache reads using the legacy zero price',
143
- status => {
144
- expect( convertCostToLegacy( cost( [
145
- item( INPUT, 'cache_read', 200, status === FALLBACK ? 2 : null, status === FALLBACK ? 0.0004 : null, status ),
146
- item( OUTPUT, null, 50, 10, 0.0005 )
147
- ] ) ) ).toEqual( {
148
- type: 'llm:usage',
149
- modelId: MODEL_ID,
150
- usage: [
151
- { type: 'input_cached', ppm: 0, amount: 200, total: 0 },
152
- { type: 'output', ppm: 10, amount: 50, total: 0.0005 }
153
- ],
154
- total: 0.0005,
155
- tokensUsed: 250
156
- } );
157
- }
158
- );
159
-
160
- it( 'gives aggregate input precedence over detailed input items', () => {
161
- expect( convertCostToLegacy( cost( [
162
- item( INPUT, null, 100, 2, 0.0002 ),
163
- item( INPUT, 'no_cache', 60, 2, 0.00012 ),
164
- item( INPUT, 'cache_read', 20, 1, 0.00002 ),
165
- item( INPUT, 'cache_write', 20, 3, 0.00006 )
166
- ] ) ) ).toEqual( {
167
- type: 'llm:usage',
168
- modelId: MODEL_ID,
169
- usage: [
170
- { type: 'input', ppm: 2, amount: 100, total: 0.0002 }
171
- ],
172
- total: 0.0002,
173
- tokensUsed: 100
174
- } );
175
- } );
176
-
177
- it( 'converts text output without reasoning', () => {
178
- expect( convertCostToLegacy( cost( [
179
- item( OUTPUT, 'text', 50, 10, 0.0005 )
180
- ] ) ) ).toEqual( {
181
- type: 'llm:usage',
182
- modelId: MODEL_ID,
183
- usage: [
184
- { type: 'output', ppm: 10, amount: 50, total: 0.0005 }
185
- ],
186
- total: 0.0005,
187
- tokensUsed: 50
188
- } );
189
- } );
190
-
191
- it( 'converts reasoning output without text', () => {
192
- expect( convertCostToLegacy( cost( [
193
- item( OUTPUT, 'reasoning', 50, 20, 0.001 )
194
- ] ) ) ).toEqual( {
195
- type: 'llm:usage',
196
- modelId: MODEL_ID,
197
- usage: [
198
- { type: 'reasoning', ppm: 20, amount: 50, total: 0.001 }
199
- ],
200
- total: 0.001,
201
- tokensUsed: 50
202
- } );
203
- } );
204
-
205
- it( 'gives aggregate output precedence over detailed output items', () => {
206
- expect( convertCostToLegacy( cost( [
207
- item( OUTPUT, null, 70, 10, 0.0007 ),
208
- item( OUTPUT, 'text', 20, 10, 0.0002 ),
209
- item( OUTPUT, 'reasoning', 50, 20, 0.001 )
210
- ] ) ) ).toEqual( {
211
- type: 'llm:usage',
212
- modelId: MODEL_ID,
213
- usage: [
214
- { type: 'output', ppm: 10, amount: 70, total: 0.0007 }
215
- ],
216
- total: 0.0007,
217
- tokensUsed: 70
218
- } );
219
- } );
220
-
221
- it( 'omits unpriced aggregate and detailed items from a partially priced cost', () => {
222
- expect( convertCostToLegacy( cost( [
223
- item( INPUT, null, 100, null, null, MISSING ),
224
- item( INPUT, 'no_cache', 100, null, null, MISSING ),
225
- item( INPUT, 'cache_write', 20, null, null, MISSING ),
226
- item( OUTPUT, null, 50, null, null, MISSING ),
227
- item( OUTPUT, 'text', 40, null, null, MISSING ),
228
- item( OUTPUT, 'reasoning', 10, null, null, MISSING ),
229
- item( INPUT, 'cache_read', 25, null, null, MISSING ),
230
- item( OUTPUT, 'text', 5, 10, 0.00005, OK )
231
- ] ) ) ).toEqual( {
232
- type: 'llm:usage',
233
- modelId: MODEL_ID,
234
- usage: [
235
- { type: 'input_cached', ppm: 0, amount: 25, total: 0 },
236
- { type: 'output', ppm: 10, amount: 5, total: 0.00005 }
237
- ],
238
- total: 0.00005,
239
- tokensUsed: 30
240
- } );
241
- } );
242
-
243
- it( 'preserves zero amounts and prices', () => {
244
- expect( convertCostToLegacy( cost( [
245
- item( INPUT, 'no_cache', 0, 0, 0 ),
246
- item( INPUT, 'cache_read', 0, 0, 0 ),
247
- item( OUTPUT, 'text', 0, 0, 0 ),
248
- item( OUTPUT, 'reasoning', 0, 0, 0 )
249
- ] ) ) ).toEqual( {
250
- type: 'llm:usage',
251
- modelId: MODEL_ID,
252
- usage: [
253
- { type: 'input', ppm: 0, amount: 0, total: 0 },
254
- { type: 'input_cached', ppm: 0, amount: 0, total: 0 },
255
- { type: 'output', ppm: 0, amount: 0, total: 0 },
256
- { type: 'reasoning', ppm: 0, amount: 0, total: 0 }
257
- ],
258
- total: 0,
259
- tokensUsed: 0
260
- } );
261
- } );
262
-
263
- it( 'recalculates line and aggregate totals from amount and ppm', () => {
264
- expect( convertCostToLegacy( cost( [
265
- item( INPUT, null, 333_333, 3, 999 ),
266
- item( OUTPUT, null, 100_000, 7, 999 )
267
- ] ) ) ).toEqual( {
268
- type: 'llm:usage',
269
- modelId: MODEL_ID,
270
- usage: [
271
- { type: 'input', ppm: 3, amount: 333_333, total: 0.999999 },
272
- { type: 'output', ppm: 7, amount: 100_000, total: 0.7 }
273
- ],
274
- total: 1.699999,
275
- tokensUsed: 433_333
276
- } );
277
- } );
278
- } );