@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,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
|
-
} );
|
package/src/utils/file.spec.js
DELETED
|
@@ -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
|
-
} );
|
package/src/utils/image.spec.js
DELETED
|
@@ -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
|
-
} );
|