@outputai/llm 0.8.2-next.e658cc2.0 → 0.8.2-next.edf06bb.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/llm",
3
- "version": "0.8.2-next.e658cc2.0",
3
+ "version": "0.8.2-next.edf06bb.0",
4
4
  "description": "Framework abstraction to interact with LLM models",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -13,7 +13,7 @@
13
13
  "gray-matter": "4.0.3",
14
14
  "liquidjs": "10.25.7",
15
15
  "undici": "8.5.0",
16
- "@outputai/core": "0.8.2-next.e658cc2.0"
16
+ "@outputai/core": "0.8.2-next.edf06bb.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@ai-sdk/amazon-bedrock": "4.0.111",
@@ -36,8 +36,5 @@
36
36
  "license": "Apache-2.0",
37
37
  "publishConfig": {
38
38
  "access": "public"
39
- },
40
- "scripts": {
41
- "build": "pnpm exec tsc -p ./tsconfig.typecheck.json"
42
39
  }
43
40
  }
@@ -1,5 +1,3 @@
1
- import { Logger } from '@outputai/core';
2
-
3
1
  const costTableUrl = 'https://models.dev/api.json';
4
2
  const cacheTTL = 1000 * 60 * 60 * 24; // 1 day
5
3
 
@@ -28,10 +26,10 @@ export const fetchModelsPricing = async () => {
28
26
  const res = await fetch( costTableUrl );
29
27
  if ( !res.ok ) {
30
28
  if ( cache.content ) {
31
- Logger.warn( `Error ${res.status} when fetching models pricing at ${costTableUrl}, falling back to stale cache`, { namespace: 'LLM' } );
29
+ console.warn( `Error ${res.status} when fetching models pricing at ${costTableUrl}, falling back to stale cache` );
32
30
  return cache.content;
33
31
  }
34
- Logger.error( `Error ${res.status} when fetching models pricing at ${costTableUrl}`, { namespace: 'LLM' } );
32
+ console.error( `Error ${res.status} when fetching models pricing at ${costTableUrl}` );
35
33
  return null;
36
34
  }
37
35
  cache.content = buildModelMap( await res.json() );
@@ -9,29 +9,24 @@ const fixturePath = join( __dirname, 'fixtures', 'models_api_light.json' );
9
9
  const fixture = JSON.parse( readFileSync( fixturePath, 'utf8' ) );
10
10
 
11
11
  const costTableUrl = 'https://models.dev/api.json';
12
- const okResponse = data => ( {
13
- ok: true,
14
- json: () => Promise.resolve( data )
15
- } );
16
- const stubFetch = response => {
17
- const fetchMock = vi.fn().mockResolvedValue( response );
18
- vi.stubGlobal( 'fetch', fetchMock );
19
- return fetchMock;
20
- };
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`;
21
14
 
22
15
  describe( 'fetchModelsPricing', () => {
23
16
  beforeEach( () => {
24
17
  cache.content = null;
25
18
  cache.expiresAt = 0;
26
- vi.unstubAllGlobals();
19
+ vi.restoreAllMocks();
27
20
  } );
28
21
 
29
22
  it( 'returns a Map of model costs when fetch succeeds', async () => {
30
- const fetchMock = stubFetch( okResponse( fixture ) );
23
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
24
+ ok: true,
25
+ json: () => Promise.resolve( fixture )
26
+ } ) );
31
27
 
32
28
  const result = await fetchModelsPricing();
33
29
 
34
- expect( fetchMock ).toHaveBeenCalledWith( costTableUrl );
35
30
  expect( result ).toBeInstanceOf( Map );
36
31
  expect( result.size ).toBeGreaterThan( 0 );
37
32
  const firstModel = Object.values( fixture )[0];
@@ -42,7 +37,10 @@ describe( 'fetchModelsPricing', () => {
42
37
  } );
43
38
 
44
39
  it( 'includes main providers from fixture (openai, anthropic, google, nvidia, perplexity)', async () => {
45
- stubFetch( okResponse( fixture ) );
40
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
41
+ ok: true,
42
+ json: () => Promise.resolve( fixture )
43
+ } ) );
46
44
 
47
45
  const result = await fetchModelsPricing();
48
46
 
@@ -57,35 +55,47 @@ describe( 'fetchModelsPricing', () => {
57
55
 
58
56
  it( 'returns null when response is not ok and no cache', async () => {
59
57
  const status = 500;
60
- stubFetch( { ok: false, status } );
58
+ const err = vi.spyOn( console, 'error' ).mockImplementation( () => {} );
59
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( { ok: false, status } ) );
61
60
 
62
61
  const result = await fetchModelsPricing();
63
62
 
64
63
  expect( result ).toBeNull();
64
+ expect( err ).toHaveBeenCalledWith( errNoCache( status ) );
65
+ err.mockRestore();
65
66
  } );
66
67
 
67
68
  it( 'returns stale cache when response is not ok but cache exists', async () => {
68
- stubFetch( okResponse( fixture ) );
69
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
70
+ ok: true,
71
+ json: () => Promise.resolve( fixture )
72
+ } ) );
69
73
  await fetchModelsPricing();
70
74
  cache.expiresAt = 0; // force refetch so we hit the !res.ok path
71
75
 
72
76
  const status = 404;
73
- stubFetch( { ok: false, status } );
77
+ const warn = vi.spyOn( console, 'warn' ).mockImplementation( () => {} );
78
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( { ok: false, status } ) );
74
79
 
75
80
  const result = await fetchModelsPricing();
76
81
 
77
82
  expect( result ).toBeInstanceOf( Map );
78
83
  expect( result.size ).toBeGreaterThan( 0 );
84
+ expect( warn ).toHaveBeenCalledWith( warnStaleCache( status ) );
85
+ warn.mockRestore();
79
86
  } );
80
87
 
81
88
  it( 'returns cached Map when cache is still valid', async () => {
82
- const fetchMock = stubFetch( okResponse( fixture ) );
89
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
90
+ ok: true,
91
+ json: () => Promise.resolve( fixture )
92
+ } ) );
83
93
 
84
94
  const first = await fetchModelsPricing();
85
95
  const second = await fetchModelsPricing();
86
96
 
87
97
  expect( first ).toBe( second );
88
- expect( fetchMock ).toHaveBeenCalledTimes( 1 );
98
+ expect( fetch ).toHaveBeenCalledTimes( 1 );
89
99
  } );
90
100
 
91
101
  it( 'only stores models that have a cost object', async () => {
@@ -98,7 +108,10 @@ describe( 'fetchModelsPricing', () => {
98
108
  }
99
109
  }
100
110
  };
101
- stubFetch( okResponse( dataWithMissingCost ) );
111
+ vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( {
112
+ ok: true,
113
+ json: () => Promise.resolve( dataWithMissingCost )
114
+ } ) );
102
115
 
103
116
  const result = await fetchModelsPricing();
104
117
 
package/src/cost/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { fetchModelsPricing } from './fetch_models_pricing.js';
2
2
  import { Tracing } from '@outputai/core/sdk/runtime';
3
- import { Logger } from '@outputai/core';
4
3
 
5
4
  /**
6
5
  * Calculates the cost of an llm call based on the model and usage.
@@ -14,13 +13,13 @@ export const calculateLLMCallCost = async ( { modelId, usage } ) => {
14
13
  const models = await fetchModelsPricing();
15
14
 
16
15
  if ( !models ) {
17
- Logger.warn( 'Failed to fetch models pricing', { namespace: 'LLM' } );
16
+ console.warn( 'Failed to fetch models pricing' );
18
17
  return null;
19
18
  }
20
19
 
21
20
  const pricing = models.get( modelId );
22
21
  if ( !pricing ) {
23
- Logger.warn( 'Missing cost reference for model', { namespace: 'LLM' } );
22
+ console.warn( 'Missing cost reference for model' );
24
23
  return null;
25
24
  }
26
25
 
@@ -51,7 +50,7 @@ export const calculateLLMCallCost = async ( { modelId, usage } ) => {
51
50
 
52
51
  return llmUsage;
53
52
  } catch ( error ) {
54
- Logger.error( 'Error calculating LLM call costs', { error: error.message, namespace: 'LLM' } );
53
+ console.error( 'Error calculating LLM call costs', error );
55
54
  return null;
56
55
  }
57
56
  };
@@ -61,6 +61,8 @@ const expectLLMUsage = ( result, { modelId, usage, total, tokensUsed } ) => {
61
61
  describe( 'calculateLLMCallCost', () => {
62
62
  beforeEach( () => {
63
63
  vi.clearAllMocks();
64
+ vi.spyOn( console, 'warn' ).mockImplementation( () => {} );
65
+ vi.spyOn( console, 'error' ).mockImplementation( () => {} );
64
66
  } );
65
67
 
66
68
  afterEach( () => {
@@ -76,6 +78,7 @@ describe( 'calculateLLMCallCost', () => {
76
78
  } );
77
79
 
78
80
  expect( result ).toBeNull();
81
+ expect( console.warn ).toHaveBeenCalledWith( 'Failed to fetch models pricing' );
79
82
  } );
80
83
 
81
84
  it( 'returns null when model is missing from cost table', async () => {
@@ -87,6 +90,7 @@ describe( 'calculateLLMCallCost', () => {
87
90
  } );
88
91
 
89
92
  expect( result ).toBeNull();
93
+ expect( console.warn ).toHaveBeenCalledWith( 'Missing cost reference for model' );
90
94
  } );
91
95
 
92
96
  it( 'calculates input and output usage from model pricing', async () => {
@@ -279,5 +283,6 @@ describe( 'calculateLLMCallCost', () => {
279
283
  } );
280
284
 
281
285
  expect( result ).toBeNull();
286
+ expect( console.error ).toHaveBeenCalledWith( 'Error calculating LLM call costs', error );
282
287
  } );
283
288
  } );
@@ -1,4 +1,4 @@
1
- import { Logger, ValidationError, z } from '@outputai/core';
1
+ import { ValidationError, z } from '@outputai/core';
2
2
  import { attributesSchema } from './block_options.js';
3
3
 
4
4
  const toolConfigSchema = z.record( z.string(), z.unknown() );
@@ -70,12 +70,12 @@ const SNAKE_CASE_WARNINGS = {
70
70
  function warnSnakeCaseFields( config ) {
71
71
  for ( const [ snake, camel ] of Object.entries( SNAKE_CASE_WARNINGS ) ) {
72
72
  if ( Object.hasOwn( config, snake ) ) {
73
- Logger.warn( `[output-llm] "${snake}" found in prompt config. Did you mean "${camel}"?`, { namespace: 'LLM' } );
73
+ console.warn( `[output-llm] "${snake}" found in prompt config. Did you mean "${camel}"?` );
74
74
  }
75
75
  }
76
76
  const thinking = config.providerOptions?.thinking;
77
77
  if ( thinking && Object.hasOwn( thinking, 'budget_tokens' ) ) {
78
- Logger.warn( '[output-llm] "budget_tokens" found in providerOptions.thinking. Did you mean "budgetTokens"?', { namespace: 'LLM' } );
78
+ console.warn( '[output-llm] "budget_tokens" found in providerOptions.thinking. Did you mean "budgetTokens"?' );
79
79
  }
80
80
  }
81
81
 
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, vi } from 'vitest';
2
2
  import { ValidationError } from '@outputai/core';
3
3
  import { validatePrompt } from './validations.js';
4
4
 
@@ -548,6 +548,8 @@ describe( 'validatePrompt', () => {
548
548
  } );
549
549
 
550
550
  it( 'should pass through budget_tokens in thinking and warn about snake_case', () => {
551
+ const warnSpy = vi.spyOn( console, 'warn' ).mockImplementation( () => {} );
552
+
551
553
  const promptWithBudgetTokensSnake = {
552
554
  name: 'thinking-budget-snake',
553
555
  config: {
@@ -569,6 +571,11 @@ describe( 'validatePrompt', () => {
569
571
  };
570
572
 
571
573
  expect( () => validatePrompt( promptWithBudgetTokensSnake ) ).not.toThrow();
574
+ expect( warnSpy ).toHaveBeenCalledWith(
575
+ '[output-llm] "budget_tokens" found in providerOptions.thinking. Did you mean "budgetTokens"?'
576
+ );
577
+
578
+ warnSpy.mockRestore();
572
579
  } );
573
580
 
574
581
  it( 'should allow snake_case fields in config via passthrough (no longer strict)', () => {