@@ -133,11 +133,11 @@ def create_chat_completion_message_event(
133133 span_id ,
134134 trace_id ,
135135 response_model ,
136- request_model ,
137136 response_id ,
138137 request_id ,
139138 llm_metadata ,
140139 output_message_list ,
140+ all_token_counts ,
141141 request_timestamp = None ,
142142):
143143 settings = transaction .settings if transaction .settings is not None else global_settings ()
@@ -158,20 +158,20 @@ def create_chat_completion_message_event(
158158 "request_id" : request_id ,
159159 "span_id" : span_id ,
160160 "trace_id" : trace_id ,
161- "token_count" : (
162- settings .ai_monitoring .llm_token_count_callback (request_model , message_content )
163- if settings .ai_monitoring .llm_token_count_callback
164- else None
165- ),
166161 "role" : message .get ("role" ),
167162 "completion_id" : chat_completion_id ,
168163 "sequence" : index ,
169164 "response.model" : response_model ,
170165 "vendor" : "openai" ,
171166 "ingest_source" : "Python" ,
172167 }
168+
173169 if settings .ai_monitoring .record_content .enabled and message_content :
174170 chat_completion_input_message_dict ["content" ] = message_content
171+
172+ if all_token_counts :
173+ chat_completion_input_message_dict ["token_count" ] = 0
174+
175175 if request_timestamp :
176176 chat_completion_input_message_dict ["timestamp" ] = request_timestamp
177177
@@ -199,11 +199,6 @@ def create_chat_completion_message_event(
199199 "request_id" : request_id ,
200200 "span_id" : span_id ,
201201 "trace_id" : trace_id ,
202- "token_count" : (
203- settings .ai_monitoring .llm_token_count_callback (response_model , message_content )
204- if settings .ai_monitoring .llm_token_count_callback
205- else None
206- ),
207202 "role" : message .get ("role" ),
208203 "completion_id" : chat_completion_id ,
209204 "sequence" : index ,
@@ -216,6 +211,9 @@ def create_chat_completion_message_event(
216211 if settings .ai_monitoring .record_content .enabled and message_content :
217212 chat_completion_output_message_dict ["content" ] = message_content
218213
214+ if all_token_counts :
215+ chat_completion_output_message_dict ["token_count" ] = 0
216+
219217 chat_completion_output_message_dict .update (llm_metadata )
220218
221219 transaction .record_custom_event ("LlmChatCompletionMessage" , chat_completion_output_message_dict )
@@ -286,15 +284,18 @@ def _record_embedding_success(transaction, embedding_id, linking_metadata, kwarg
286284 else getattr (attribute_response , "organization" , None )
287285 )
288286
287+ response_total_tokens = attribute_response .get ("usage" , {}).get ("total_tokens" ) if response else None
288+
289+ total_tokens = (
290+ settings .ai_monitoring .llm_token_count_callback (response_model , input_ )
291+ if settings .ai_monitoring .llm_token_count_callback and input_
292+ else response_total_tokens
293+ )
294+
289295 full_embedding_response_dict = {
290296 "id" : embedding_id ,
291297 "span_id" : span_id ,
292298 "trace_id" : trace_id ,
293- "token_count" : (
294- settings .ai_monitoring .llm_token_count_callback (response_model , input_ )
295- if settings .ai_monitoring .llm_token_count_callback
296- else None
297- ),
298299 "request.model" : kwargs .get ("model" ) or kwargs .get ("engine" ),
299300 "request_id" : request_id ,
300301 "duration" : ft .duration * 1000 ,
@@ -319,6 +320,7 @@ def _record_embedding_success(transaction, embedding_id, linking_metadata, kwarg
319320 "response.headers.ratelimitRemainingRequests" : check_rate_limit_header (
320321 response_headers , "x-ratelimit-remaining-requests" , True
321322 ),
323+ "response.usage.total_tokens" : total_tokens ,
322324 "vendor" : "openai" ,
323325 "ingest_source" : "Python" ,
324326 }
@@ -489,13 +491,15 @@ def _handle_completion_success(
489491def _record_completion_success (
490492 transaction , linking_metadata , completion_id , kwargs , ft , response_headers , response , request_timestamp = None
491493):
494+ settings = transaction .settings if transaction .settings is not None else global_settings ()
492495 span_id = linking_metadata .get ("span.id" )
493496 trace_id = linking_metadata .get ("trace.id" )
494497
495498 try :
496499 if response :
497500 response_model = response .get ("model" )
498501 response_id = response .get ("id" )
502+ token_usage = response .get ("usage" ) or {}
499503 output_message_list = []
500504 finish_reason = None
501505 choices = response .get ("choices" ) or []
@@ -509,6 +513,7 @@ def _record_completion_success(
509513 else :
510514 response_model = kwargs .get ("response.model" )
511515 response_id = kwargs .get ("id" )
516+ token_usage = kwargs .get ("response.usage" ) or {}
512517 finish_reason = kwargs .get ("finish_reason" )
513518 content = kwargs .get ("content" )
514519 # Tool-call responses may carry an empty content string; in that case the
@@ -519,12 +524,39 @@ def _record_completion_success(
519524 output_message_list = [{"content" : content , "role" : kwargs .get ("role" )}]
520525 else :
521526 output_message_list = []
527+
522528 request_model = kwargs .get ("model" ) or kwargs .get ("engine" )
523529
524- request_id = response_headers .get ("x-request-id" )
525- organization = response_headers .get ("openai-organization" ) or getattr (response , "organization" , None )
526530 messages = kwargs .get ("messages" ) or [{"content" : kwargs .get ("prompt" ), "role" : "user" }]
527531 input_message_list = list (messages )
532+
533+ # Token counts default to those reported in the response object if available,
534+ # but the user registered callback below may override them.
535+ response_prompt_tokens = token_usage .get ("prompt_tokens" )
536+ response_completion_tokens = token_usage .get ("completion_tokens" )
537+ response_total_tokens = token_usage .get ("total_tokens" )
538+
539+ # If the user has registered a callback to compute token counts it should always be preferred.
540+ token_count_callback = settings .ai_monitoring .llm_token_count_callback
541+ if token_count_callback :
542+ input_message_content = " " .join (content for msg in input_message_list if (content := msg .get ("content" )))
543+ if input_message_content :
544+ response_prompt_tokens = token_count_callback (request_model , input_message_content )
545+ output_message_content = " " .join (content for msg in output_message_list if (content := msg .get ("content" )))
546+ if output_message_content :
547+ response_completion_tokens = token_count_callback (response_model , output_message_content )
548+
549+ # Prefer the sum of individual counts as the total whenever both are available.
550+ # This ensures consistency in the event that the token counting callback has reported
551+ # different values for prompt or completion tokens.
552+ if response_prompt_tokens and response_completion_tokens :
553+ response_total_tokens = response_prompt_tokens + response_completion_tokens
554+
555+ all_token_counts = bool (response_prompt_tokens and response_completion_tokens and response_total_tokens )
556+
557+ request_id = response_headers .get ("x-request-id" )
558+ organization = response_headers .get ("openai-organization" ) or getattr (response , "organization" , None )
559+
528560 full_chat_completion_summary_dict = {
529561 "id" : completion_id ,
530562 "span_id" : span_id ,
@@ -571,6 +603,12 @@ def _record_completion_success(
571603 "response.number_of_messages" : len (input_message_list ) + len (output_message_list ),
572604 "timestamp" : request_timestamp ,
573605 }
606+
607+ if all_token_counts :
608+ full_chat_completion_summary_dict ["response.usage.prompt_tokens" ] = response_prompt_tokens
609+ full_chat_completion_summary_dict ["response.usage.completion_tokens" ] = response_completion_tokens
610+ full_chat_completion_summary_dict ["response.usage.total_tokens" ] = response_total_tokens
611+
574612 llm_metadata = _get_llm_attributes (transaction )
575613
576614 if "time_to_first_token" in kwargs :
@@ -586,11 +624,11 @@ def _record_completion_success(
586624 span_id ,
587625 trace_id ,
588626 response_model ,
589- request_model ,
590627 response_id ,
591628 request_id ,
592629 llm_metadata ,
593630 output_message_list ,
631+ all_token_counts ,
594632 request_timestamp ,
595633 )
596634 except Exception :
@@ -602,6 +640,7 @@ def _record_completion_error(transaction, linking_metadata, completion_id, kwarg
602640 trace_id = linking_metadata .get ("trace.id" )
603641 request_message_list = kwargs .get ("messages" , None ) or []
604642 notice_error_attributes = {}
643+
605644 try :
606645 if OPENAI_V1 :
607646 response = getattr (exc , "response" , None )
@@ -667,18 +706,20 @@ def _record_completion_error(transaction, linking_metadata, completion_id, kwarg
667706 output_message_list = []
668707 if "content" in kwargs :
669708 output_message_list = [{"content" : kwargs .get ("content" ), "role" : kwargs .get ("role" )}]
709+
670710 create_chat_completion_message_event (
671711 transaction ,
672712 request_message_list ,
673713 completion_id ,
674714 span_id ,
675715 trace_id ,
676716 kwargs .get ("response.model" ),
677- request_model ,
678717 response_id ,
679718 request_id ,
680719 llm_metadata ,
681720 output_message_list ,
721+ # We do not record token counts in error cases, so set all_token_counts to True so the pipeline tokenizer does not run
722+ True ,
682723 request_timestamp ,
683724 )
684725 except Exception :
@@ -787,6 +828,7 @@ def _record_stream_chunk(self, return_val):
787828 self ._nr_openai_attrs ["response.model" ] = return_val .get ("model" )
788829 self ._nr_openai_attrs ["id" ] = return_val .get ("id" )
789830 self ._nr_openai_attrs ["response.organization" ] = return_val .get ("organization" )
831+ self ._nr_openai_attrs ["response.usage" ] = return_val .get ("usage" )
790832 if choices :
791833 delta = choices [0 ].get ("delta" ) or {}
792834 if delta :
0 commit comments