@@ -42,16 +42,44 @@ def _extract_tokens_from_dict_or_obj(ud: Any) -> tuple[int, int, int]:
4242
4343
4444def _try_emit_token_event (inp : int , out : int , tot : int , source : str ) -> None :
45- """Log token usage found in stream/response for diagnostics.
46-
47- The actual LLM_Token_Usage event is emitted by TokenUsageTracker.record()
48- in the orchestrator, which has full context (agent, step, model, user).
49- This function only logs for debugging to avoid duplicate events.
45+ """Emit token usage found in a stream/response to App Insights.
46+
47+ The chat client retry wrapper is the only layer that reliably sees raw LLM
48+ ``usage_details``; the orchestrator's streaming events and final conversation
49+ do not surface usage Content. We therefore record here, against the active
50+ ``TokenUsageTracker`` (resolved from context), which both aggregates the
51+ usage and emits the per-call ``LLM_*`` Application Insights events. When no
52+ tracker is active in the current context this falls back to a diagnostic log.
5053 """
51- if tot > 0 or inp > 0 or out > 0 :
52- logger .info (
53- "[TOKEN_STREAM] usage found: input=%s output=%s total=%s source=%s" ,
54- inp , out , tot , source ,
54+ if not (tot > 0 or inp > 0 or out > 0 ):
55+ return
56+
57+ logger .info (
58+ "[TOKEN_STREAM] usage found: input=%s output=%s total=%s source=%s" ,
59+ inp , out , tot , source ,
60+ )
61+ try :
62+ from utils .token_usage_tracker import record_usage_from_context
63+
64+ emitted = record_usage_from_context (
65+ input_tokens = int (inp ),
66+ output_tokens = int (out ),
67+ total_tokens = int (tot ),
68+ )
69+ if emitted :
70+ logger .info (
71+ "[TOKEN_STREAM] emitted LLM token event: total=%s source=%s" ,
72+ tot , source ,
73+ )
74+ else :
75+ logger .warning (
76+ "[TOKEN_STREAM] no active TokenUsageTracker in context; "
77+ "token usage NOT emitted (total=%s source=%s)" ,
78+ tot , source ,
79+ )
80+ except Exception as e :
81+ logger .warning (
82+ "[TOKEN_STREAM] failed to emit token usage (source=%s): %s" , source , e
5583 )
5684
5785
@@ -836,8 +864,8 @@ async def _non_streaming_with_retry(
836864
837865 try :
838866 response = await _retry_call (
839- lambda : parent_inner_get_response (
840- messages = effective_messages , chat_options = chat_options , ** kwargs
867+ lambda : parent_inner (
868+ messages = effective_messages , options = options , stream = False , ** kwargs
841869 ),
842870 config = self ._retry_config ,
843871 )
@@ -891,12 +919,15 @@ async def _non_streaming_with_retry(
891919 "[AOAI_CTX_TRIM] sleeping %ss before retry" , round (trim_delay , 1 )
892920 )
893921 await asyncio .sleep (trim_delay )
894- return await _retry_call (
922+ response = await _retry_call (
895923 lambda : parent_inner (
896924 messages = trimmed , options = options , stream = False , ** kwargs
897925 ),
898926 config = self ._retry_config ,
899927 )
928+ # Emit token usage from the retried (context-trimmed) response too.
929+ _emit_usage_from_response (response )
930+ return response
900931
901932
902933class AzureOpenAIChatClientWithRetry (OpenAIChatCompletionClient ):
@@ -1040,123 +1071,29 @@ def _maybe_trim_messages(
10401071 return trimmed
10411072 return messages
10421073
1043- iterator = stream .__aiter__ ()
1044- try :
1045- first = await iterator .__anext__ ()
1046-
1047- async def _tail ():
1048- yield first
1049- async for item in iterator :
1050- yield item
1051-
1052- _item_count = 0
1053- _last_item = None
1054- async for item in _tail ():
1055- _item_count += 1
1056- _last_item = item
1057- _emit_usage_from_stream_item (item )
1058- yield item
1059-
1060- # After stream completes, log diagnostic about the last item
1061- if _last_item is not None :
1062- try :
1063- _attrs = [a for a in dir (_last_item ) if not a .startswith ("_" )]
1064- _contents = getattr (_last_item , "contents" , None )
1065- _content_info = []
1066- if _contents :
1067- for _c in _contents :
1068- _ct = getattr (_c , "type" , "?" )
1069- _ca = [a for a in dir (_c ) if not a .startswith ("_" )]
1070- _content_info .append ({"type" : _ct , "attrs" : _ca })
1071- _usage_attr = getattr (_last_item , "usage" , None )
1072- logger .info (
1073- "[TOKEN_DIAG_FINAL] stream_items=%d last_item_type=%s attrs=%s contents=%s usage_attr=%s" ,
1074- _item_count ,
1075- type (_last_item ).__name__ ,
1076- _attrs ,
1077- _content_info ,
1078- repr (_usage_attr ) if _usage_attr is not None else "None" ,
1079- )
1080- except Exception :
1081- pass
1082- return
1083- except StopAsyncIteration :
1084- return
1085- except Exception as e :
1086- close = getattr (stream , "aclose" , None )
1087- if callable (close ):
1088- try :
1089- await close ()
1090- except Exception :
1091- logger .debug ("Best-effort close of response stream failed" , exc_info = True )
1092-
1093- # Progressive retry for context-length failures.
1094- if (
1095- self ._context_trim_config .enabled
1096- and self ._context_trim_config .retry_on_context_error
1097- and _looks_like_context_length (e )
1098- ):
1099- # Make trimming progressively more aggressive on each retry
1100- # GPT-5.1: 272K input tokens ≈ 800K chars. Scale down from 600K default.
1101- scale = attempt_index + 1
1102- aggressive_cfg = ContextTrimConfig (
1103- enabled = True ,
1104- max_total_chars = max (
1105- 30_000 ,
1106- self ._context_trim_config .max_total_chars - scale * 100_000 ,
1107- ),
1108- max_message_chars = max (
1109- 2_000 ,
1110- self ._context_trim_config .max_message_chars - scale * 8_000 ,
1111- ),
1112- keep_last_messages = max (
1113- 4 ,
1114- self ._context_trim_config .keep_last_messages - scale * 8 ,
1115- ),
1116- keep_head_chars = max (
1117- 500 ,
1118- self ._context_trim_config .keep_head_chars - scale * 3_000 ,
1119- ),
1120- keep_tail_chars = max (
1121- 500 ,
1122- self ._context_trim_config .keep_tail_chars - scale * 1_000 ,
1123- ),
1124- keep_system_messages = True ,
1125- retry_on_context_error = True ,
1126- )
1127- trimmed = _trim_messages (effective_messages , cfg = aggressive_cfg )
1128- logger .warning (
1129- "[AOAI_CTX_TRIM_STREAM] retrying after context-length error (attempt %s); count=%s -> %s, budget=%s" ,
1130- attempt_index + 1 ,
1131- len (effective_messages ),
1132- len (trimmed ),
1133- aggressive_cfg .max_total_chars ,
1134- )
1135- effective_messages = trimmed
1136- if attempt_index >= attempts - 1 :
1137- # No more retries available.
1138- raise
1139-
1140- # Cool down before retrying — immediate retries after trimming
1141- # tend to trigger 429s because the API hasn't recovered yet.
1142- trim_delay = self ._retry_config .base_delay_seconds * (
1143- 2 ** attempt_index
1144- )
1145- trim_delay = min (trim_delay , self ._retry_config .max_delay_seconds )
1146- logger .info (
1147- "[AOAI_CTX_TRIM_STREAM] sleeping %ss before retry" ,
1148- round (trim_delay , 1 ),
1149- )
1150- await asyncio .sleep (trim_delay )
1151- continue
1074+ async def _non_streaming_with_retry (
1075+ self ,
1076+ * ,
1077+ effective_messages : MutableSequence [Any ] | list [Any ],
1078+ original_messages : MutableSequence [Any ],
1079+ options : Any = None ,
1080+ ** kwargs : Any ,
1081+ ) -> Any :
1082+ """Non-streaming path: full retry + context-trim fallback."""
1083+ parent_inner = super (
1084+ AzureOpenAIChatClientWithRetry , self
1085+ )._inner_get_response
11521086
11531087 try :
1154- return await _retry_call (
1088+ response = await _retry_call (
11551089 lambda : parent_inner (
11561090 messages = effective_messages , options = options , stream = False , ** kwargs
11571091 ),
11581092 config = self ._retry_config ,
11591093 )
1094+ # Extract and emit token usage from the non-streaming response.
1095+ _emit_usage_from_response (response )
1096+ return response
11601097 except Exception as e :
11611098 if not (
11621099 self ._context_trim_config .enabled
@@ -1206,9 +1143,12 @@ async def _tail():
12061143 "[AOAI_CTX_TRIM] sleeping %ss before retry" , round (trim_delay , 1 )
12071144 )
12081145 await asyncio .sleep (trim_delay )
1209- return await _retry_call (
1146+ response = await _retry_call (
12101147 lambda : parent_inner (
12111148 messages = trimmed , options = options , stream = False , ** kwargs
12121149 ),
12131150 config = self ._retry_config ,
12141151 )
1152+ # Emit token usage from the retried (context-trimmed) response too.
1153+ _emit_usage_from_response (response )
1154+ return response
0 commit comments