Skip to content

Commit b8678cc

Browse files
feat: Updated token calculation for openai LLM Events (newrelic#3430)
1 parent 013c4e3 commit b8678cc

15 files changed

+612
-76
lines changed

lib/llm-events/openai/chat-completion-message.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
'use strict'
77
const LlmEvent = require('./event')
8+
const { tokenUsageAttributesExist } = require('./utils')
9+
const { setTokenFromCallback } = require('../utils')
810

911
module.exports = class LlmChatCompletionMessage extends LlmEvent {
1012
constructor({
@@ -38,17 +40,42 @@ module.exports = class LlmChatCompletionMessage extends LlmEvent {
3840
if (agent.config.ai_monitoring.record_content.enabled === true) {
3941
this.content = content
4042
}
43+
this.setTokenCount(agent, request, response)
44+
}
4145

42-
// Calculate token count if the callback is available.
46+
setTokenCount(agent, request, response) {
4347
const tokenCB = agent.llm?.tokenCountCallback
44-
if (typeof tokenCB !== 'function') {
48+
49+
if (tokenCB) {
50+
const messages = request?.input || request?.messages
51+
52+
const promptContent = typeof messages === 'string'
53+
? messages
54+
: messages?.map((msg) => msg.content).join(' ')
55+
56+
const completionContent = response?.output
57+
? response.output.map((resContent) => resContent.content[0].text).join(' ')
58+
: response?.choices?.map((resContent) => resContent.message.content).join(' ')
59+
60+
if (promptContent && completionContent) {
61+
setTokenFromCallback(
62+
{
63+
context: this,
64+
tokenCB,
65+
reqModel: request.model,
66+
resModel: this['response.model'],
67+
promptContent,
68+
completionContent
69+
}
70+
)
71+
}
4572
return
4673
}
4774

48-
if (this.is_response) {
49-
this.token_count = tokenCB(this['response.model'], content)
50-
} else {
51-
this.token_count = tokenCB(request.model || request.engine, content)
75+
// If no token count callback is available, we need to check the response object
76+
// for usage information and set token_count to 0 if all usage attributes are present.
77+
if (tokenUsageAttributesExist(response)) {
78+
this.token_count = 0
5279
}
5380
}
5481
}

lib/llm-events/openai/chat-completion-summary.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
'use strict'
77
const LlmEvent = require('./event')
8+
const { setUsageTokens } = require('./utils')
9+
const { setTokenUsageFromCallback } = require('../utils')
810

911
module.exports = class LlmChatCompletionSummary extends LlmEvent {
1012
constructor({ agent, segment, request = {}, response = {}, withError = false, transaction }) {
@@ -24,5 +26,39 @@ module.exports = class LlmChatCompletionSummary extends LlmEvent {
2426
this['response.number_of_messages'] = request?.messages?.length + response?.choices?.length
2527
this['response.choices.finish_reason'] = response?.choices?.[0]?.finish_reason
2628
}
29+
30+
this.setTokens(agent, request, response)
31+
}
32+
33+
setTokens(agent, request, response) {
34+
const tokenCB = agent.llm?.tokenCountCallback
35+
36+
// Prefer callback for prompt and completion tokens; if unavailable, fall back to response data.
37+
if (tokenCB) {
38+
const messages = request?.input || request?.messages
39+
40+
const promptContent = typeof messages === 'string'
41+
? messages
42+
: messages?.map((msg) => msg.content).join(' ')
43+
44+
const completionContent = response?.output
45+
? response.output.map((resContent) => resContent.content[0].text).join(' ')
46+
: response?.choices?.map((resContent) => resContent.message.content).join(' ')
47+
48+
if (promptContent && completionContent) {
49+
setTokenUsageFromCallback(
50+
{
51+
context: this,
52+
tokenCB,
53+
reqModel: request.model,
54+
resModel: this['response.model'],
55+
promptContent,
56+
completionContent
57+
}
58+
)
59+
}
60+
return
61+
}
62+
setUsageTokens(response, this)
2763
}
2864
}

lib/llm-events/openai/embedding.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
'use strict'
77
const LlmEvent = require('./event')
8+
const { validCallbackTokenCount, calculateCallbackTokens } = require('../utils')
89

910
module.exports = class LlmEmbedding extends LlmEvent {
1011
constructor({ agent, segment, request = {}, response = {}, withError = false, transaction }) {
@@ -14,9 +15,38 @@ module.exports = class LlmEmbedding extends LlmEvent {
1415
if (agent.config.ai_monitoring.record_content.enabled === true) {
1516
this.input = request.input?.toString()
1617
}
17-
this.token_count = agent.llm?.tokenCountCallback?.(
18-
this['request.model'],
19-
request.input?.toString()
20-
)
18+
19+
this.setTotalTokens(agent, request, response)
20+
}
21+
22+
setTotalTokens(agent, request, response) {
23+
const tokenCB = agent.llm?.tokenCountCallback
24+
25+
// For embedding events, only total token count is relevant.
26+
// Prefer callback for total tokens; if unavailable, fall back to response data.
27+
if (tokenCB) {
28+
const content = request.input?.toString()
29+
30+
if (content === undefined) {
31+
return
32+
}
33+
34+
const totalTokenCount = calculateCallbackTokens(tokenCB, this['request.model'], content)
35+
const hasValidCallbackCounts = validCallbackTokenCount(totalTokenCount)
36+
37+
if (hasValidCallbackCounts) {
38+
this['response.usage.total_tokens'] = Number(totalTokenCount)
39+
}
40+
return
41+
}
42+
43+
const totalTokens = this.getTotalTokens(response)
44+
if (totalTokens) {
45+
this['response.usage.total_tokens'] = Number(totalTokens)
46+
}
47+
}
48+
49+
getTotalTokens(response) {
50+
return response?.usage?.total_tokens || response?.usage?.totalTokens
2151
}
2252
}

lib/llm-events/openai/utils.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const { setTokensInResponse } = require('../utils')
9+
10+
function setUsageTokens(response, context) {
11+
// input and output token counts must available in order to add all usage attributes to response
12+
// if total tokens is not available, we can manually add it up (from input and output token count)
13+
if (tokenUsageAttributesExist(response) === false) {
14+
return
15+
}
16+
17+
const promptTokens = Number(response?.usage?.prompt_tokens || response?.usage?.input_tokens)
18+
const completionTokens = Number(response?.usage?.completion_tokens || response?.usage?.output_tokens)
19+
const totalTokens = Number(response?.usage?.total_tokens || response?.usage?.totalTokens)
20+
21+
setTokensInResponse(context, { promptTokens, completionTokens, totalTokens })
22+
}
23+
24+
function tokenUsageAttributesExist(response) {
25+
const tokensA = response?.usage?.prompt_tokens && response?.usage?.completion_tokens
26+
const tokensB = response?.usage?.input_tokens && response?.usage?.output_tokens
27+
28+
return tokensA !== undefined || tokensB !== undefined
29+
}
30+
31+
module.exports = {
32+
tokenUsageAttributesExist,
33+
setUsageTokens
34+
}

lib/llm-events/utils.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
/**
9+
* Determines if the provided token count is valid.
10+
* A valid token count is greater than 0 and not null.
11+
* @param {number} tokenCount The token count obtained from the token callback
12+
* @returns {boolean} Whether the token count is valid
13+
*/
14+
function validCallbackTokenCount(tokenCount) {
15+
return tokenCount !== null && tokenCount > 0
16+
}
17+
18+
/**
19+
* Calculates the total token count from the prompt tokens and completion tokens
20+
* set in the context.
21+
* @param {LlmEvent} context The context object containing token counts
22+
* @returns {number} The total token count
23+
*/
24+
function getTotalTokenCount(context) {
25+
return Number(context['response.usage.prompt_tokens']) + Number(context['response.usage.completion_tokens'])
26+
}
27+
28+
/**
29+
* Sets the provided tokens counts on the LLM event.
30+
* @param {LlmChatCompletionMessage} context The context object to set token usage counts on.
31+
* @param {object} tokens The object contains the token prompt, completion and total counts.
32+
*/
33+
function setTokensInResponse(context, tokens) {
34+
context['response.usage.prompt_tokens'] = tokens.promptTokens
35+
context['response.usage.completion_tokens'] = tokens.completionTokens
36+
context['response.usage.total_tokens'] = tokens.totalTokens || getTotalTokenCount(context)
37+
}
38+
39+
/**
40+
* Calculates prompt and completion token counts using the provided callback and models.
41+
* If both counts are valid, sets context.token_count to 0.
42+
*
43+
* @param {object} options - The params object.
44+
* @param {LlmChatCompletionMessage} options.context - The context object to set token count on.
45+
* @param {Function} options.tokenCB - The token counting callback function.
46+
* @param {string} options.reqModel - The model used for the prompt.
47+
* @param {string} options.resModel - The model used for the completion.
48+
* @param {string} options.promptContent - The prompt content to count tokens for.
49+
* @param {string} options.completionContent - The completion content to count tokens for.
50+
* @returns {void}
51+
*/
52+
function setTokenFromCallback({ context, tokenCB, reqModel, resModel, promptContent, completionContent }) {
53+
const promptToken = calculateCallbackTokens(tokenCB, reqModel, promptContent)
54+
const completionToken = calculateCallbackTokens(tokenCB, resModel, completionContent)
55+
56+
const hasValidCallbackCounts =
57+
validCallbackTokenCount(promptToken) && validCallbackTokenCount(completionToken)
58+
59+
if (hasValidCallbackCounts) {
60+
context.token_count = 0
61+
}
62+
}
63+
64+
/**
65+
* Calculates prompt and completion token counts using the provided callback and models.
66+
* If both counts are valid, sets token prompt, completion and total counts on the context.
67+
*
68+
* @param {object} options - The params object.
69+
* @param {LlmEvent} options.context - The context object (llm summary or llm embedding) to set token count on.
70+
* @param {Function} options.tokenCB - The token counting callback function.
71+
* @param {string} options.reqModel - The model used for the prompt.
72+
* @param {string} options.resModel - The model used for the completion.
73+
* @param {string} options.promptContent - The prompt content to count tokens for.
74+
* @param {string} options.completionContent - The completion content to count tokens for.
75+
* @returns {void}
76+
*/
77+
function setTokenUsageFromCallback({ context, tokenCB, reqModel, resModel, promptContent, completionContent }) {
78+
const promptTokens = calculateCallbackTokens(tokenCB, reqModel, promptContent)
79+
const completionTokens = calculateCallbackTokens(tokenCB, resModel, completionContent)
80+
81+
const hasValidCallbackCounts =
82+
validCallbackTokenCount(promptTokens) && validCallbackTokenCount(completionTokens)
83+
84+
if (hasValidCallbackCounts) {
85+
setTokensInResponse(context, { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens })
86+
}
87+
}
88+
89+
/**
90+
* Calculate the token counts using the provided callback.
91+
* @param {Function} tokenCB - The token count callback function.
92+
* @param {string} model - The model.
93+
* @param {string} content - The content to calculate tokens for, such as prompt or completion response.
94+
* @returns {number|undefined} - The calculated token count or undefined if callback is not a function.
95+
*/
96+
function calculateCallbackTokens(tokenCB, model, content) {
97+
if (typeof tokenCB === 'function') {
98+
return tokenCB(model, content)
99+
}
100+
return undefined
101+
}
102+
103+
module.exports = {
104+
validCallbackTokenCount,
105+
getTotalTokenCount,
106+
setTokensInResponse,
107+
setTokenFromCallback,
108+
setTokenUsageFromCallback,
109+
calculateCallbackTokens
110+
}

0 commit comments

Comments
 (0)