@outputai/llm 0.1.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.
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { readFileSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import { fetchModelsPricing, cache } from './fetch_models_pricing.js';
6
+
7
+ const __dirname = dirname( fileURLToPath( import.meta.url ) );
8
+ const fixturePath = join( __dirname, 'fixtures', 'models_api_light.json' );
9
+ const fixture = JSON.parse( readFileSync( fixturePath, 'utf8' ) );
10
+
11
+ const costTableUrl = 'https://models.dev/api.json';
12
+ const errNoCache = status => `Error ${status} when fetching models pricing at ${costTableUrl}`;
13
+ const warnStaleCache = status => `Error ${status} when fetching models pricing at ${costTableUrl}, falling back to stale cache`;
14
+
15
+ describe( 'fetchModelsPricing', () => {
16
+ beforeEach( () => {
17
+ cache.content = null;
18
+ cache.expiresAt = 0;
19
+ vi.restoreAllMocks();
20
+ } );
21
+
22
+ it( 'returns a Map of model costs when fetch succeeds', async () => {
23
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
24
+ ok: true,
25
+ json: () => Promise.resolve( fixture )
26
+ } ) );
27
+
28
+ const result = await fetchModelsPricing();
29
+
30
+ expect( result ).toBeInstanceOf( Map );
31
+ expect( result.size ).toBeGreaterThan( 0 );
32
+ const firstModel = Object.values( fixture )[0];
33
+ const firstModelId = Object.keys( firstModel.models )[0];
34
+ const cost = firstModel.models[firstModelId].cost;
35
+ expect( result.get( firstModelId ) ).toEqual( cost );
36
+ expect( result.get( `${firstModel.id}/${firstModelId}` ) ).toEqual( cost );
37
+ } );
38
+
39
+ it( 'includes main providers from fixture (openai, anthropic, google, nvidia, perplexity)', async () => {
40
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
41
+ ok: true,
42
+ json: () => Promise.resolve( fixture )
43
+ } ) );
44
+
45
+ const result = await fetchModelsPricing();
46
+
47
+ const openaiProvider = fixture.openai;
48
+ const openaiModelId = Object.keys( openaiProvider.models )[0];
49
+ expect( result.get( openaiModelId ) ).toBeDefined();
50
+ expect( result.get( `openai/${openaiModelId}` ) ).toEqual( openaiProvider.models[openaiModelId].cost );
51
+
52
+ const anthropicModelId = Object.keys( fixture.anthropic.models )[0];
53
+ expect( result.get( anthropicModelId ) ).toBeDefined();
54
+ } );
55
+
56
+ it( 'returns null when response is not ok and no cache', async () => {
57
+ const status = 500;
58
+ const err = vi.spyOn( console, 'error' ).mockImplementation( () => {} );
59
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( { ok: false, status } ) );
60
+
61
+ const result = await fetchModelsPricing();
62
+
63
+ expect( result ).toBeNull();
64
+ expect( err ).toHaveBeenCalledWith( errNoCache( status ) );
65
+ err.mockRestore();
66
+ } );
67
+
68
+ it( 'returns stale cache when response is not ok but cache exists', async () => {
69
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
70
+ ok: true,
71
+ json: () => Promise.resolve( fixture )
72
+ } ) );
73
+ await fetchModelsPricing();
74
+ cache.expiresAt = 0; // force refetch so we hit the !res.ok path
75
+
76
+ const status = 404;
77
+ const warn = vi.spyOn( console, 'warn' ).mockImplementation( () => {} );
78
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( { ok: false, status } ) );
79
+
80
+ const result = await fetchModelsPricing();
81
+
82
+ expect( result ).toBeInstanceOf( Map );
83
+ expect( result.size ).toBeGreaterThan( 0 );
84
+ expect( warn ).toHaveBeenCalledWith( warnStaleCache( status ) );
85
+ warn.mockRestore();
86
+ } );
87
+
88
+ it( 'returns cached Map when cache is still valid', async () => {
89
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
90
+ ok: true,
91
+ json: () => Promise.resolve( fixture )
92
+ } ) );
93
+
94
+ const first = await fetchModelsPricing();
95
+ const second = await fetchModelsPricing();
96
+
97
+ expect( first ).toBe( second );
98
+ expect( fetch ).toHaveBeenCalledTimes( 1 );
99
+ } );
100
+
101
+ it( 'only stores models that have a cost object', async () => {
102
+ const dataWithMissingCost = {
103
+ p1: {
104
+ id: 'p1',
105
+ models: {
106
+ withCost: { cost: { input: 1, output: 2 } },
107
+ noCost: { name: 'x' }
108
+ }
109
+ }
110
+ };
111
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
112
+ ok: true,
113
+ json: () => Promise.resolve( dataWithMissingCost )
114
+ } ) );
115
+
116
+ const result = await fetchModelsPricing();
117
+
118
+ expect( result.get( 'withCost' ) ).toEqual( { input: 1, output: 2 } );
119
+ expect( result.get( 'noCost' ) ).toBeUndefined();
120
+ } );
121
+ } );