Skip to content

Commit 4679825

Browse files
committed
add token cost to usageMetadata
1 parent 293a0e1 commit 4679825

3 files changed

Lines changed: 152 additions & 6 deletions

File tree

packages/components/nodes/agentflow/Agent/Agent.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
updateFlowState
3535
} from '../utils'
3636
import { convertMultiOptionsToStringArray, processTemplateVariables, configureStructuredOutput } from '../../../src/utils'
37+
import { getModelConfigByModelName, MODEL_TYPE } from '../../../src/modelLoader'
3738

3839
interface ITool {
3940
agentSelectedTool: string
@@ -1428,6 +1429,13 @@ class Agent_Agentflow implements INode {
14281429
const reasonContentObj =
14291430
reasonContent !== undefined && reasonContent !== '' ? { thinking: reasonContent, thinkingDuration } : undefined
14301431

1432+
const costMetadata = await this.calculateUsageCost(
1433+
model,
1434+
modelConfig?.modelName as string | undefined,
1435+
response.usage_metadata,
1436+
additionalTokens
1437+
)
1438+
14311439
const output = this.prepareOutputObject(
14321440
response,
14331441
availableTools,
@@ -1442,7 +1450,8 @@ class Agent_Agentflow implements INode {
14421450
isWaitingForHumanInput,
14431451
fileAnnotations,
14441452
isStructuredOutput,
1445-
reasonContentObj
1453+
reasonContentObj,
1454+
costMetadata
14461455
)
14471456

14481457
// End analytics tracking
@@ -1962,6 +1971,49 @@ class Agent_Agentflow implements INode {
19621971
return response
19631972
}
19641973

1974+
/**
1975+
* Calculates input/output and total cost from usage metadata using model pricing from models.json.
1976+
* Also returns the model's base (per-token) input and output costs.
1977+
*/
1978+
private async calculateUsageCost(
1979+
provider: string | undefined,
1980+
modelName: string | undefined,
1981+
usageMetadata: Record<string, any> | undefined,
1982+
additionalTokens: number = 0
1983+
): Promise<
1984+
| {
1985+
input_cost: number
1986+
output_cost: number
1987+
total_cost: number
1988+
base_input_cost: number
1989+
base_output_cost: number
1990+
}
1991+
| undefined
1992+
> {
1993+
if (!provider || !modelName) return undefined
1994+
const inputTokens = (usageMetadata?.input_tokens ?? 0) as number
1995+
const outputTokens = ((usageMetadata?.output_tokens ?? 0) as number) + additionalTokens
1996+
try {
1997+
const modelConfig = await getModelConfigByModelName(MODEL_TYPE.CHAT, provider, modelName)
1998+
if (!modelConfig) return undefined
1999+
const baseInputCost = Number(modelConfig.input_cost) || 0
2000+
const baseOutputCost = Number(modelConfig.output_cost) || 0
2001+
const inputCost = inputTokens * baseInputCost
2002+
const outputCost = outputTokens * baseOutputCost
2003+
const totalCost = inputCost + outputCost
2004+
if (inputCost === 0 && outputCost === 0) return undefined
2005+
return {
2006+
input_cost: inputCost,
2007+
output_cost: outputCost,
2008+
total_cost: totalCost,
2009+
base_input_cost: baseInputCost,
2010+
base_output_cost: baseOutputCost
2011+
}
2012+
} catch {
2013+
return undefined
2014+
}
2015+
}
2016+
19652017
/**
19662018
* Prepares the output object with response and metadata
19672019
*/
@@ -1979,7 +2031,14 @@ class Agent_Agentflow implements INode {
19792031
isWaitingForHumanInput: boolean = false,
19802032
fileAnnotations: any[] = [],
19812033
isStructuredOutput: boolean = false,
1982-
reasonContent?: { thinking: string; thinkingDuration?: number }
2034+
reasonContent?: { thinking: string; thinkingDuration?: number },
2035+
costMetadata?: {
2036+
input_cost: number
2037+
output_cost: number
2038+
total_cost: number
2039+
base_input_cost: number
2040+
base_output_cost: number
2041+
}
19832042
): any {
19842043
const output: any = {
19852044
content: finalResponse,
@@ -2010,6 +2069,14 @@ class Agent_Agentflow implements INode {
20102069
}
20112070
}
20122071

2072+
if (costMetadata && output.usageMetadata) {
2073+
output.usageMetadata.input_cost = costMetadata.input_cost
2074+
output.usageMetadata.output_cost = costMetadata.output_cost
2075+
output.usageMetadata.total_cost = costMetadata.total_cost
2076+
output.usageMetadata.base_input_cost = costMetadata.base_input_cost
2077+
output.usageMetadata.base_output_cost = costMetadata.base_output_cost
2078+
}
2079+
20132080
if (response.response_metadata) {
20142081
output.responseMetadata = response.response_metadata
20152082
}

packages/components/nodes/agentflow/LLM/LLM.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
updateFlowState
1717
} from '../utils'
1818
import { processTemplateVariables, configureStructuredOutput } from '../../../src/utils'
19+
import { getModelConfigByModelName, MODEL_TYPE } from '../../../src/modelLoader'
1920
import { flatten } from 'lodash'
2021

2122
class LLM_Agentflow implements INode {
@@ -597,6 +598,8 @@ class LLM_Agentflow implements INode {
597598
} else {
598599
finalResponse = JSON.stringify(response, null, 2)
599600
}
601+
const costMetadata = await this.calculateUsageCost(model, modelConfig?.modelName as string | undefined, response.usage_metadata)
602+
600603
const output = this.prepareOutputObject(
601604
response,
602605
finalResponse,
@@ -606,7 +609,8 @@ class LLM_Agentflow implements INode {
606609
isStructuredOutput,
607610
artifacts,
608611
fileAnnotations,
609-
reasonContentObj
612+
reasonContentObj,
613+
costMetadata
610614
)
611615

612616
// End analytics tracking
@@ -949,6 +953,48 @@ class LLM_Agentflow implements INode {
949953
return response
950954
}
951955

956+
/**
957+
* Calculates input/output and total cost from usage metadata using model pricing from models.json.
958+
* Also returns the model's base (per-token) input and output costs.
959+
*/
960+
private async calculateUsageCost(
961+
provider: string | undefined,
962+
modelName: string | undefined,
963+
usageMetadata: Record<string, any> | undefined
964+
): Promise<
965+
| {
966+
input_cost: number
967+
output_cost: number
968+
total_cost: number
969+
base_input_cost: number
970+
base_output_cost: number
971+
}
972+
| undefined
973+
> {
974+
if (!provider || !modelName) return undefined
975+
const inputTokens = (usageMetadata?.input_tokens ?? 0) as number
976+
const outputTokens = (usageMetadata?.output_tokens ?? 0) as number
977+
try {
978+
const modelConfig = await getModelConfigByModelName(MODEL_TYPE.CHAT, provider, modelName)
979+
if (!modelConfig) return undefined
980+
const baseInputCost = Number(modelConfig.input_cost) || 0
981+
const baseOutputCost = Number(modelConfig.output_cost) || 0
982+
const inputCost = inputTokens * baseInputCost
983+
const outputCost = outputTokens * baseOutputCost
984+
const totalCost = inputCost + outputCost
985+
if (inputCost === 0 && outputCost === 0) return undefined
986+
return {
987+
input_cost: inputCost,
988+
output_cost: outputCost,
989+
total_cost: totalCost,
990+
base_input_cost: baseInputCost,
991+
base_output_cost: baseOutputCost
992+
}
993+
} catch {
994+
return undefined
995+
}
996+
}
997+
952998
/**
953999
* Prepares the output object with response and metadata
9541000
*/
@@ -961,7 +1007,14 @@ class LLM_Agentflow implements INode {
9611007
isStructuredOutput: boolean,
9621008
artifacts: any[] = [],
9631009
fileAnnotations: any[] = [],
964-
reasonContent?: { thinking: string; thinkingDuration?: number }
1010+
reasonContent?: { thinking: string; thinkingDuration?: number },
1011+
costMetadata?: {
1012+
input_cost: number
1013+
output_cost: number
1014+
total_cost: number
1015+
base_input_cost: number
1016+
base_output_cost: number
1017+
}
9651018
): any {
9661019
const output: any = {
9671020
content: finalResponse,
@@ -977,7 +1030,15 @@ class LLM_Agentflow implements INode {
9771030
}
9781031

9791032
if (response.usage_metadata) {
980-
output.usageMetadata = response.usage_metadata
1033+
output.usageMetadata = { ...response.usage_metadata }
1034+
}
1035+
1036+
if (costMetadata && output.usageMetadata) {
1037+
output.usageMetadata.input_cost = costMetadata.input_cost
1038+
output.usageMetadata.output_cost = costMetadata.output_cost
1039+
output.usageMetadata.total_cost = costMetadata.total_cost
1040+
output.usageMetadata.base_input_cost = costMetadata.base_input_cost
1041+
output.usageMetadata.base_output_cost = costMetadata.base_output_cost
9811042
}
9821043

9831044
if (response.response_metadata) {

packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
} from '@mui/material'
2626
import { useTheme, darken } from '@mui/material/styles'
2727
import { useSnackbar } from 'notistack'
28-
import { IconCoins, IconClock, IconChevronDown, IconDownload, IconTool } from '@tabler/icons-react'
28+
import { IconCoins, IconCoin, IconClock, IconChevronDown, IconDownload, IconTool } from '@tabler/icons-react'
2929
import toolSVG from '@/assets/images/tool.svg'
3030

3131
// Project imports
@@ -326,6 +326,24 @@ export const NodeExecutionDetails = ({ data, label, status, metadata, isPublic,
326326
sx={{ ml: 1, '& .MuiChip-icon': { mr: 0.2, ml: 1 } }}
327327
/>
328328
)}
329+
{data.output?.usageMetadata?.total_cost != null && Number(data.output.usageMetadata.total_cost) >= 0 && (
330+
<Chip
331+
icon={<IconCoin size={17} />}
332+
label={
333+
data.output.usageMetadata.total_cost >= 0.01
334+
? `$${Number(data.output.usageMetadata.total_cost).toFixed(2)}`
335+
: `$${Number(data.output.usageMetadata.total_cost).toFixed(6)}`
336+
}
337+
variant='contained'
338+
size='small'
339+
sx={{
340+
ml: 1,
341+
backgroundColor: '#c49331',
342+
color: 'white',
343+
'& .MuiChip-icon': { color: 'white', mr: 0.2, ml: 1 }
344+
}}
345+
/>
346+
)}
329347
</Box>
330348
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
331349
<ToggleButtonGroup

0 commit comments

Comments
 (0)