Skip to content

Commit 78b4058

Browse files
Fix async for coroutine error in retry client streaming path
The parent OpenAIChatClient._inner_get_response is a regular def that returns ResponseStream (async iterable) when stream=True, or Awaitable when stream=False. The override was async def, which always returned a coroutine, breaking 'async for event in workflow.run(stream=True)'. Refactored to: - Regular def _inner_get_response dispatching stream vs non-stream - _non_streaming_with_retry: async coroutine with retry + context-trim - _streaming_with_retry: async generator with pre-first-chunk retry - _maybe_trim_messages: shared context-trim helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 32e3aa4 commit 78b4058

1 file changed

Lines changed: 149 additions & 34 deletions

File tree

src/processor/src/libs/agent_framework/azure_openai_response_retry.py

Lines changed: 149 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import os
1111
import random
1212
from dataclasses import dataclass
13-
from typing import Any, MutableSequence
13+
from typing import Any, AsyncIterable, MutableSequence
1414

1515
from agent_framework.openai import OpenAIChatClient
1616
from 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

Comments
 (0)