1010import os
1111import random
1212from dataclasses import dataclass
13- from typing import Any , MutableSequence
13+ from typing import Any , AsyncIterable , MutableSequence
1414
1515from agent_framework .openai import OpenAIChatClient
1616from tenacity import (
@@ -530,35 +530,72 @@ def __init__(
530530 self ._retry_config = retry_config or RateLimitRetryConfig .from_env ()
531531 self ._context_trim_config = ContextTrimConfig .from_env ()
532532
533- async def _inner_get_response (
534- self , * , messages : MutableSequence [Any ], options : Any = None , ** kwargs : Any
533+ def _inner_get_response (
534+ self , * , messages : MutableSequence [Any ], options : Any = None , stream : bool = False , ** kwargs : Any
535535 ) -> Any :
536- parent_inner_get_response = super (
536+ """Override that adds retry + context-trimming around the parent call.
537+
538+ Must remain a regular ``def`` (not ``async def``) because the parent
539+ returns different types depending on *stream*:
540+ - stream=False → Awaitable[ChatResponse]
541+ - stream=True → ResponseStream (AsyncIterable)
542+ """
543+ effective_messages = self ._maybe_trim_messages (messages )
544+
545+ if stream :
546+ return self ._streaming_with_retry (
547+ effective_messages = effective_messages ,
548+ original_messages = messages ,
549+ options = options ,
550+ ** kwargs ,
551+ )
552+ else :
553+ return self ._non_streaming_with_retry (
554+ effective_messages = effective_messages ,
555+ original_messages = messages ,
556+ options = options ,
557+ ** kwargs ,
558+ )
559+
560+ def _maybe_trim_messages (
561+ self , messages : MutableSequence [Any ]
562+ ) -> MutableSequence [Any ] | list [Any ]:
563+ """Apply pre-call context trimming if enabled and over budget."""
564+ if not self ._context_trim_config .enabled :
565+ return messages
566+ approx_chars = sum (len (_estimate_message_text (m )) for m in messages )
567+ if (
568+ self ._context_trim_config .max_total_chars > 0
569+ and approx_chars > self ._context_trim_config .max_total_chars
570+ ):
571+ trimmed = _trim_messages (messages , cfg = self ._context_trim_config )
572+ logger .warning (
573+ "[AOAI_CTX_TRIM] pre-trimmed request messages: approx_chars=%s -> %s; count=%s -> %s" ,
574+ approx_chars ,
575+ sum (len (_estimate_message_text (m )) for m in trimmed ),
576+ len (messages ),
577+ len (trimmed ),
578+ )
579+ return trimmed
580+ return messages
581+
582+ async def _non_streaming_with_retry (
583+ self ,
584+ * ,
585+ effective_messages : MutableSequence [Any ] | list [Any ],
586+ original_messages : MutableSequence [Any ],
587+ options : Any = None ,
588+ ** kwargs : Any ,
589+ ) -> Any :
590+ """Non-streaming path: full retry + context-trim fallback."""
591+ parent_inner = super (
537592 AzureOpenAIResponseClientWithRetry , self
538593 )._inner_get_response
539594
540- effective_messages : MutableSequence [Any ] | list [Any ] = messages
541- if self ._context_trim_config .enabled :
542- approx_chars = sum (len (_estimate_message_text (m )) for m in messages )
543- if (
544- self ._context_trim_config .max_total_chars > 0
545- and approx_chars > self ._context_trim_config .max_total_chars
546- ):
547- effective_messages = _trim_messages (
548- messages , cfg = self ._context_trim_config
549- )
550- logger .warning (
551- "[AOAI_CTX_TRIM] pre-trimmed request messages: approx_chars=%s -> %s; count=%s -> %s" ,
552- approx_chars ,
553- sum (len (_estimate_message_text (m )) for m in effective_messages ),
554- len (messages ),
555- len (effective_messages ),
556- )
557-
558595 try :
559596 return await _retry_call (
560- lambda : parent_inner_get_response (
561- messages = effective_messages , options = options , ** kwargs
597+ lambda : parent_inner (
598+ messages = effective_messages , options = options , stream = False , ** kwargs
562599 ),
563600 config = self ._retry_config ,
564601 )
@@ -571,7 +608,7 @@ async def _inner_get_response(
571608 raise
572609
573610 trimmed = _trim_messages (
574- messages ,
611+ original_messages ,
575612 cfg = ContextTrimConfig (
576613 enabled = True ,
577614 max_total_chars = max (
@@ -593,20 +630,98 @@ async def _inner_get_response(
593630 )
594631 logger .warning (
595632 "[AOAI_CTX_TRIM] retrying after context-length error; count=%s -> %s" ,
596- len (messages ),
633+ len (original_messages ),
597634 len (trimmed ),
598635 )
599- # Cool down before retrying to avoid triggering 429s immediately.
600- trim_delay = self ._retry_config .base_delay_seconds
601- trim_delay = min (trim_delay , self ._retry_config .max_delay_seconds )
636+ trim_delay = min (
637+ self ._retry_config .base_delay_seconds ,
638+ self ._retry_config .max_delay_seconds ,
639+ )
602640 logger .info (
603- "[AOAI_CTX_TRIM] sleeping %ss before retry" ,
604- round (trim_delay , 1 ),
641+ "[AOAI_CTX_TRIM] sleeping %ss before retry" , round (trim_delay , 1 )
605642 )
606643 await asyncio .sleep (trim_delay )
607644 return await _retry_call (
608- lambda : parent_inner_get_response (
609- messages = trimmed , options = options , ** kwargs
645+ lambda : parent_inner (
646+ messages = trimmed , options = options , stream = False , ** kwargs
610647 ),
611648 config = self ._retry_config ,
612- )
649+ )
650+
651+ async def _streaming_with_retry (
652+ self ,
653+ * ,
654+ effective_messages : MutableSequence [Any ] | list [Any ],
655+ original_messages : MutableSequence [Any ],
656+ options : Any = None ,
657+ ** kwargs : Any ,
658+ ) -> AsyncIterable [Any ]:
659+ """Streaming path: retry only before the first chunk is yielded."""
660+ parent_inner = super (
661+ AzureOpenAIResponseClientWithRetry , self
662+ )._inner_get_response
663+
664+ attempts = self ._retry_config .max_retries + 1
665+
666+ for attempt_index in range (attempts ):
667+ response_stream = parent_inner (
668+ messages = effective_messages , options = options , stream = True , ** kwargs
669+ )
670+
671+ iterator = response_stream .__aiter__ ()
672+ try :
673+ first = await iterator .__anext__ ()
674+
675+ async def _tail ():
676+ yield first
677+ async for item in iterator :
678+ yield item
679+
680+ async for item in _tail ():
681+ yield item
682+ return
683+ except StopAsyncIteration :
684+ return
685+ except Exception as e :
686+ close = getattr (response_stream , "aclose" , None )
687+ if callable (close ):
688+ try :
689+ await close ()
690+ except Exception :
691+ logger .debug (
692+ "Best-effort close of response stream failed" ,
693+ exc_info = True ,
694+ )
695+
696+ if not _looks_like_rate_limit (e ) or attempt_index >= attempts - 1 :
697+ if _looks_like_rate_limit (e ):
698+ logger .warning (
699+ "[AOAI_RETRY_STREAM] giving up after %s/%s attempts; error=%s" ,
700+ attempt_index + 1 ,
701+ attempts ,
702+ _format_exc_brief (e ),
703+ )
704+ raise
705+
706+ retry_after = _try_get_retry_after_seconds (e )
707+ if retry_after is not None and retry_after >= 0 :
708+ delay = retry_after
709+ else :
710+ delay = self ._retry_config .base_delay_seconds * (
711+ 2 ** attempt_index
712+ )
713+ delay = min (delay , self ._retry_config .max_delay_seconds )
714+ delay = delay + random .uniform (0.0 , 0.25 * max (delay , 0.1 ))
715+
716+ status = getattr (e , "status_code" , None ) or getattr (
717+ e , "status" , None
718+ )
719+ logger .warning (
720+ "[AOAI_RETRY_STREAM] attempt %s/%s; sleeping=%ss; retry_after=%s; status=%s; error=%s" ,
721+ attempt_index + 1 ,
722+ attempts ,
723+ round (float (delay ), 3 ),
724+ None if retry_after is None else round (float (retry_after ), 3 ),
725+ status ,
726+ _format_exc_brief (e ),
727+ )
0 commit comments