@outputai/llm 0.12.0 → 0.12.1-next.69255d7.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 +2 -2
- package/src/index.d.ts +4 -3
- package/src/utils/cost.js +21 -10
- package/src/utils/grounding.js +49 -0
- package/src/utils/usage.js +16 -2
- package/src/utils/wrap.js +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/llm",
|
|
3
|
-
"version": "0.12.0",
|
|
3
|
+
"version": "0.12.1-next.69255d7.0",
|
|
4
4
|
"description": "Framework abstraction to interact with LLM models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"gray-matter": "4.0.3",
|
|
22
22
|
"liquidjs": "10.27.2",
|
|
23
23
|
"undici": "8.9.0",
|
|
24
|
-
"@outputai/core": "0.12.0"
|
|
24
|
+
"@outputai/core": "0.12.1-next.69255d7.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@ai-sdk/amazon-bedrock": "5.0.57",
|
package/src/index.d.ts
CHANGED
|
@@ -341,9 +341,9 @@ export type ExtractedSource =
|
|
|
341
341
|
|
|
342
342
|
export type LLMGenerationUsageStatus = 'complete' | 'incomplete';
|
|
343
343
|
|
|
344
|
-
/** Token usage reported for one normalized LLM usage component. */
|
|
344
|
+
/** Token usage reported for one normalized LLM usage component. `request` counts billable requests, not tokens. */
|
|
345
345
|
export interface LLMGenerationUsageItem {
|
|
346
|
-
group: 'input' | 'output';
|
|
346
|
+
group: 'input' | 'output' | 'request';
|
|
347
347
|
label: string | null;
|
|
348
348
|
amount: number;
|
|
349
349
|
}
|
|
@@ -386,7 +386,7 @@ export type LLMGenerationCostStatus = 'precise' | 'imprecise' | 'incomplete';
|
|
|
386
386
|
|
|
387
387
|
/** Cost calculated for one normalized LLM usage item. */
|
|
388
388
|
export interface LLMGenerationCostItem {
|
|
389
|
-
group: 'input' | 'output';
|
|
389
|
+
group: 'input' | 'output' | 'request';
|
|
390
390
|
label: string | null;
|
|
391
391
|
amount: number;
|
|
392
392
|
ppm: number | null;
|
|
@@ -401,6 +401,7 @@ export interface LLMGenerationCost extends BaseAttribute {
|
|
|
401
401
|
modelId: string;
|
|
402
402
|
input: number | null;
|
|
403
403
|
output: number | null;
|
|
404
|
+
request: number | null;
|
|
404
405
|
total: number | null;
|
|
405
406
|
status: LLMGenerationCostStatus;
|
|
406
407
|
items: LLMGenerationCostItem[];
|
package/src/utils/cost.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchModelsPricing } from './models_pricing.js';
|
|
2
2
|
import { Tracing } from '@outputai/core/sdk/runtime';
|
|
3
3
|
import { LLMGenerationUsage, LLMGenerationUsageItem } from './usage.js';
|
|
4
|
+
import { GroundingPpmMap } from './grounding.js';
|
|
4
5
|
import { Logger } from '@outputai/core';
|
|
5
6
|
import Decimal from 'decimal.js';
|
|
6
7
|
|
|
@@ -41,6 +42,7 @@ export class LLMGenerationCost extends Tracing.Attribute.BaseAttribute {
|
|
|
41
42
|
modelId;
|
|
42
43
|
input = null;
|
|
43
44
|
output = null;
|
|
45
|
+
request = null;
|
|
44
46
|
total = null;
|
|
45
47
|
status = LLMGenerationCost.Status.INCOMPLETE;
|
|
46
48
|
items = [];
|
|
@@ -60,16 +62,15 @@ export class LLMGenerationCost extends Tracing.Attribute.BaseAttribute {
|
|
|
60
62
|
} else {
|
|
61
63
|
this.status = LLMGenerationCost.Status.PRECISE;
|
|
62
64
|
}
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.total = Decimal( this.input ?? 0 ).add( this.output ?? 0 ).toNumber();
|
|
65
|
+
const sumGroup = group => {
|
|
66
|
+
const groupItems = items.filter( p => p.group === group && exists( p.total ) );
|
|
67
|
+
return groupItems.length > 0 ? groupItems.reduce( ( s, p ) => s.add( p.total ), Decimal( 0 ) ).toNumber() : null;
|
|
68
|
+
};
|
|
69
|
+
this.input = sumGroup( LLMGenerationUsageItem.Group.INPUT );
|
|
70
|
+
this.output = sumGroup( LLMGenerationUsageItem.Group.OUTPUT );
|
|
71
|
+
this.request = sumGroup( LLMGenerationUsageItem.Group.REQUEST );
|
|
72
|
+
if ( exists( this.input ) || exists( this.output ) || exists( this.request ) ) {
|
|
73
|
+
this.total = Decimal( this.input ?? 0 ).add( this.output ?? 0 ).add( this.request ?? 0 ).toNumber();
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
}
|
|
@@ -86,6 +87,10 @@ const resolveValue = values => {
|
|
|
86
87
|
};
|
|
87
88
|
|
|
88
89
|
const resolvePrice = ( { group, label, pricing } ) => {
|
|
90
|
+
// Per-request charges are never token-priced: models.dev has no rate for them.
|
|
91
|
+
if ( group === LLMGenerationUsageItem.Group.REQUEST ) {
|
|
92
|
+
return resolveValue( [ GroundingPpmMap.get( label ) ] );
|
|
93
|
+
}
|
|
89
94
|
if ( !pricing ) {
|
|
90
95
|
return resolveValue( [] );
|
|
91
96
|
}
|
|
@@ -133,5 +138,11 @@ export const calculateCosts = async usage => {
|
|
|
133
138
|
return new LLMGenerationCostItem( group, label, amount, ppm, total, status );
|
|
134
139
|
} );
|
|
135
140
|
|
|
141
|
+
const unrated = items.some( v =>
|
|
142
|
+
v.group === LLMGenerationUsageItem.Group.REQUEST && v.status === LLMGenerationCostItem.Status.MISSING );
|
|
143
|
+
if ( unrated ) {
|
|
144
|
+
Logger.warn( 'Grounded call with no grounding rate for model', { namespace: 'LLM', modelId, providerId } );
|
|
145
|
+
}
|
|
146
|
+
|
|
136
147
|
return new LLMGenerationCost( modelId, providerId, items, usage.status );
|
|
137
148
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grounding with Google Search is billed per request, not per token, and models.dev carries no
|
|
3
|
+
* per-request rate, so the table lives here. Gemini 3 bills per Grounding Query; Gemini 2.x bills
|
|
4
|
+
* per Grounding Prompt, once, regardless of query count. Different units, so the family decides
|
|
5
|
+
* both the label and the amount. Rates are price-per-million to match `ppm` semantics.
|
|
6
|
+
* Verified 2026-09-01: https://cloud.google.com/vertex-ai/generative-ai/pricing
|
|
7
|
+
*/
|
|
8
|
+
const UNITS = [
|
|
9
|
+
{ match: /^gemini-3/, label: 'grounding_query', ppm: 14_000, perQuery: true },
|
|
10
|
+
{ match: /^gemini-2/, label: 'grounding_prompt', ppm: 35_000, perQuery: false }
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Unknown family: still record the quantity so the call is visibly unpriced rather than silently
|
|
15
|
+
* token-only. No rate resolves for this label, so the item lands MISSING and the cost attribute
|
|
16
|
+
* goes INCOMPLETE.
|
|
17
|
+
*/
|
|
18
|
+
export const GROUNDING_UNKNOWN_LABEL = 'grounding';
|
|
19
|
+
|
|
20
|
+
export const GroundingPpmMap = new Map( UNITS.map( u => [ u.label, u.ppm ] ) );
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extracts the billable grounding quantity from provider metadata.
|
|
24
|
+
*
|
|
25
|
+
* Counts web-search grounding only. `groundingMetadata` also carries `imageSearchQueries` and
|
|
26
|
+
* `retrievalQueries`, which bill on their own terms; neither appears in current traffic. Extend
|
|
27
|
+
* here if image or retrieval grounding is enabled.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} modelId - Id of the model that produced the response
|
|
30
|
+
* @param {object} [providerMetadata] - AI SDK provider metadata of a single step
|
|
31
|
+
* @returns {{ label: string, amount: number } | null} Grounding label and amount, or null when ungrounded
|
|
32
|
+
*/
|
|
33
|
+
export const parseGroundingUsage = ( modelId, providerMetadata ) => {
|
|
34
|
+
const meta = providerMetadata?.vertex ?? providerMetadata?.google;
|
|
35
|
+
const queries = meta?.groundingMetadata?.webSearchQueries;
|
|
36
|
+
if ( !Array.isArray( queries ) || queries.length === 0 ) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const unit = UNITS.find( u => u.match.test( modelId ) );
|
|
41
|
+
if ( !unit ) {
|
|
42
|
+
return { label: GROUNDING_UNKNOWN_LABEL, amount: queries.length };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
label: unit.label,
|
|
47
|
+
amount: unit.perQuery ? queries.length : 1
|
|
48
|
+
};
|
|
49
|
+
};
|
package/src/utils/usage.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import Decimal from 'decimal.js';
|
|
2
2
|
import { Tracing } from '@outputai/core/sdk/runtime';
|
|
3
|
+
import { parseGroundingUsage } from './grounding.js';
|
|
3
4
|
|
|
4
5
|
const exists = v => Number.isSafeInteger( v ) && v >= 0;
|
|
5
6
|
|
|
6
7
|
const safeSum = ( ...values ) => values.filter( exists ).reduce( ( t, v ) => t + v, 0 );
|
|
7
8
|
|
|
8
9
|
export class LLMGenerationUsageItem {
|
|
9
|
-
static Group = { INPUT: 'input', OUTPUT: 'output' };
|
|
10
|
+
static Group = { INPUT: 'input', OUTPUT: 'output', REQUEST: 'request' };
|
|
10
11
|
|
|
11
12
|
group;
|
|
12
13
|
label;
|
|
@@ -65,10 +66,11 @@ export class LLMGenerationUsage extends Tracing.Attribute.BaseAttribute {
|
|
|
65
66
|
* @param {string} args.prompt.config.provider - Id of the provider
|
|
66
67
|
* @param {string} args.prompt.config.model - Id of the model
|
|
67
68
|
* @param {object} args.usage - AI SDK usage with aggregate token counts and optional input/output token details
|
|
69
|
+
* @param {object[]} [args.steps] - AI SDK steps, each with its own `providerMetadata`, used for per-request charges
|
|
68
70
|
*
|
|
69
71
|
* @returns {LLMGenerationUsage | null} LLM generation usage with input, output, total and detailed breakdown
|
|
70
72
|
*/
|
|
71
|
-
export const parseLLMUsage = ( { prompt, usage } ) => {
|
|
73
|
+
export const parseLLMUsage = ( { prompt, usage, steps } ) => {
|
|
72
74
|
const { provider: providerId, model: modelId } = prompt.config;
|
|
73
75
|
const { inputTokens, inputTokenDetails, outputTokens, outputTokenDetails } = usage;
|
|
74
76
|
const { noCacheTokens, cacheReadTokens, cacheWriteTokens } = inputTokenDetails ?? {};
|
|
@@ -113,6 +115,18 @@ export const parseLLMUsage = ( { prompt, usage } ) => {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
// Grounding is billed per step, so aggregate across every step rather than only the final one.
|
|
119
|
+
// Per-query families (Gemini 3) return queries.length per step; per-prompt families (Gemini 2.x)
|
|
120
|
+
// return 1 per grounded step. Summing yields total queries and grounded-step count respectively.
|
|
121
|
+
const grounding = ( steps ?? [] )
|
|
122
|
+
.filter( step => step?.providerMetadata )
|
|
123
|
+
.map( step => parseGroundingUsage( modelId, step.providerMetadata ) )
|
|
124
|
+
.filter( Boolean );
|
|
125
|
+
if ( grounding.length > 0 ) {
|
|
126
|
+
const amount = grounding.reduce( ( sum, g ) => sum + g.amount, 0 );
|
|
127
|
+
items.push( new LLMGenerationUsageItem( LLMGenerationUsageItem.Group.REQUEST, grounding[0].label, amount ) );
|
|
128
|
+
}
|
|
129
|
+
|
|
116
130
|
if ( items.length === 0 ) {
|
|
117
131
|
return null;
|
|
118
132
|
}
|
package/src/utils/wrap.js
CHANGED
|
@@ -31,8 +31,8 @@ const handleError = ( { traceId, error: originalError } ) => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/** Normalize raw AI SDK usage, calculate cost, attach trace attributes, and emit metering events */
|
|
34
|
-
const handleMetering = async ( { traceId, usage: sdkUsage, prompt } ) => {
|
|
35
|
-
const usageAttribute = parseLLMUsage( { usage: sdkUsage, prompt } );
|
|
34
|
+
const handleMetering = async ( { traceId, usage: sdkUsage, prompt, steps } ) => {
|
|
35
|
+
const usageAttribute = parseLLMUsage( { usage: sdkUsage, prompt, steps } );
|
|
36
36
|
if ( !usageAttribute ) {
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
@@ -74,7 +74,7 @@ export const wrapGeneration = async ( { name, prompt, fn } ) => {
|
|
|
74
74
|
try {
|
|
75
75
|
const response = await fn();
|
|
76
76
|
const { usage } = response;
|
|
77
|
-
const cost = await handleMetering( { traceId, usage, prompt } );
|
|
77
|
+
const cost = await handleMetering( { traceId, usage, prompt, steps: response.steps } );
|
|
78
78
|
|
|
79
79
|
if ( Array.isArray( response.images ) ) {
|
|
80
80
|
const { image, images, providerMetadata } = response;
|
|
@@ -149,9 +149,9 @@ export const wrapStream = ( { name, prompt, abortSignal, fn } ) => {
|
|
|
149
149
|
const state = { proxyResponse: null };
|
|
150
150
|
removeAbortListener();
|
|
151
151
|
try {
|
|
152
|
-
const { text: result, finalStep, usage } = response;
|
|
152
|
+
const { text: result, finalStep, usage, steps } = response;
|
|
153
153
|
const { providerMetadata } = finalStep;
|
|
154
|
-
const cost = await handleMetering( { traceId, usage, prompt } );
|
|
154
|
+
const cost = await handleMetering( { traceId, usage, prompt, steps } );
|
|
155
155
|
const sources = extractSources( response );
|
|
156
156
|
Tracing.addEventEnd( { id: traceId, details: { result, usage, providerMetadata, sources } } );
|
|
157
157
|
state.proxyResponse = createResponseProxy( { response, properties: { cost, sources, result } } );
|