fix(anthropic): recreate stream on retry#5820
Conversation
|
|
2e0c716 to
e260b5f
Compare
| async def _run(self) -> None: | ||
| retryable = True | ||
| try: | ||
| if not self._anthropic_stream: | ||
| self._anthropic_stream = await self._awaitable_anthropic_stream | ||
|
|
||
| async with self._anthropic_stream as stream: | ||
| async with await self._create_anthropic_stream() as stream: |
There was a problem hiding this comment.
π΄ Instance state not reset between retries in _run(), causing stale _ignoring_cot and _tool_call_id on retry
Now that retries actually work (the core purpose of this PR), _run() can be called multiple times by _main_task() (livekit-agents/livekit/agents/llm/llm.py:215-223). However, _run() does not reset instance state variables (_tool_call_id, _fnc_name, _fnc_raw_arguments, _ignoring_cot, _input_tokens, _output_tokens, etc.) at the start of each attempt. If a first attempt partially processes stream events before failing with a retryable error, the stale state carries into the retry.
Two concrete failure modes:
_ignoring_cotstuck True: If the first attempt receives a<thinking>text delta (line 367-368) before failing,_ignoring_cotstays True on retry, silently dropping all text content until a</thinking>tag appears β which may never happen in the retry's fresh response.- Stale
_tool_call_idcauses bogus tool calls: If the first attempt receives acontent_block_startwith typetool_use(lines 356-359) before failing,_tool_call_idremains set on retry. When the retry's text block hitscontent_block_stop(line 385), theif self._tool_call_id is not Nonecheck is True, emitting a spurious tool call with stale name/arguments.
Both of these are retryable scenarios (no ChatChunk was emitted, so retryable stays True). Other plugins like Mistral (livekit-plugins/livekit-plugins-mistralai/livekit/plugins/mistralai/llm.py:222-224) and OpenAI (livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/responses/llm.py:402-403) reset their per-attempt state at the top of _run().
| async def _run(self) -> None: | |
| retryable = True | |
| try: | |
| if not self._anthropic_stream: | |
| self._anthropic_stream = await self._awaitable_anthropic_stream | |
| async with self._anthropic_stream as stream: | |
| async with await self._create_anthropic_stream() as stream: | |
| async def _run(self) -> None: | |
| # Reset per-attempt state so retries start clean | |
| self._tool_call_id = None | |
| self._fnc_name = None | |
| self._fnc_raw_arguments = None | |
| self._request_id = "" | |
| self._ignoring_cot = False | |
| self._input_tokens = 0 | |
| self._cache_creation_tokens = 0 | |
| self._cache_read_tokens = 0 | |
| self._output_tokens = 0 | |
| retryable = True | |
| try: | |
| async with await self._create_anthropic_stream() as stream: |
Was this helpful? React with π or π to provide feedback.
Summary
Fixes #5805.
Tests
uv run pytest tests\test_plugin_anthropic.py -quv run ruff check livekit-plugins\livekit-plugins-anthropic\livekit\plugins\anthropic\llm.py tests\test_plugin_anthropic.pypython -m py_compile livekit-plugins\livekit-plugins-anthropic\livekit\plugins\anthropic\llm.py tests\test_plugin_anthropic.pygit diff --check