-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(ai): retry without stop param on 400 unsupported_parameter #1544
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kiet08hogit
wants to merge
2
commits into
rocketride-org:develop
Choose a base branch
from
kiet08hogit:fix/1538-crewai-stop-kwargs
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+217
−3
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| import pytest | ||
|
|
||
| from ai.common.chat import _invoke_with_stop_fallback, _stream_with_stop_fallback | ||
|
|
||
| class MockInvokeLLM: | ||
| def __init__(self, should_fail_on_stop=True): | ||
| self.should_fail_on_stop = should_fail_on_stop | ||
| self.invoked_kwargs = [] | ||
|
|
||
| def invoke(self, prompt, **kwargs): | ||
| self.invoked_kwargs.append(kwargs) | ||
| if self.should_fail_on_stop and 'stop' in kwargs: | ||
| # Simulate the OpenAI 400 Bad Request error for unsupported stop parameter | ||
| raise ValueError("unsupported_parameter: 'stop'") | ||
| return 'success' | ||
|
|
||
| class MockStreamLLM: | ||
| def __init__(self, should_fail_on_stop=True): | ||
| self.should_fail_on_stop = should_fail_on_stop | ||
| self.streamed_kwargs = [] | ||
|
|
||
| def stream(self, prompt, **kwargs): | ||
| self.streamed_kwargs.append(kwargs) | ||
| if self.should_fail_on_stop and 'stop' in kwargs: | ||
| def failing_generator(): | ||
| # The error usually occurs on the first yield of the lazy stream | ||
| raise ValueError("unsupported_parameter: 'stop' is invalid") | ||
| yield 'never' | ||
| return failing_generator() | ||
|
|
||
| def success_generator(): | ||
| yield 'chunk1' | ||
| yield 'chunk2' | ||
| return success_generator() | ||
|
|
||
| def test_invoke_with_stop_fallback_recovers(): | ||
| """Verify that a 400 unsupported parameter error on invoke is caught and retried.""" | ||
| llm = MockInvokeLLM(should_fail_on_stop=True) | ||
| stop_kwargs = {'stop': ['\nObservation:']} | ||
|
|
||
| result = _invoke_with_stop_fallback(llm, 'Hello', stop_kwargs) | ||
|
|
||
| assert result == 'success' | ||
| assert len(llm.invoked_kwargs) == 2 | ||
| assert 'stop' in llm.invoked_kwargs[0] | ||
| assert 'stop' not in llm.invoked_kwargs[1] | ||
|
|
||
| def test_invoke_with_stop_fallback_success(): | ||
| """Verify that if the model supports stop, it works on the first try.""" | ||
| llm = MockInvokeLLM(should_fail_on_stop=False) | ||
| stop_kwargs = {'stop': ['\nObservation:']} | ||
|
|
||
| result = _invoke_with_stop_fallback(llm, 'Hello', stop_kwargs) | ||
|
|
||
| assert result == 'success' | ||
| assert len(llm.invoked_kwargs) == 1 | ||
| assert 'stop' in llm.invoked_kwargs[0] | ||
|
|
||
| def test_stream_with_stop_fallback_recovers(): | ||
| """Verify that a 400 unsupported parameter error on stream is caught and retried.""" | ||
| llm = MockStreamLLM(should_fail_on_stop=True) | ||
| stop_kwargs = {'stop': ['\nObservation:']} | ||
|
|
||
| chunks = list(_stream_with_stop_fallback(llm, 'Hello', stop_kwargs)) | ||
|
|
||
| assert chunks == ['chunk1', 'chunk2'] | ||
| assert len(llm.streamed_kwargs) == 2 | ||
| assert 'stop' in llm.streamed_kwargs[0] | ||
| assert 'stop' not in llm.streamed_kwargs[1] | ||
|
|
||
| def test_stream_with_stop_fallback_success(): | ||
| """Verify that if the model supports stop, streaming works on the first try.""" | ||
| llm = MockStreamLLM(should_fail_on_stop=False) | ||
| stop_kwargs = {'stop': ['\nObservation:']} | ||
|
|
||
| chunks = list(_stream_with_stop_fallback(llm, 'Hello', stop_kwargs)) | ||
|
|
||
| assert chunks == ['chunk1', 'chunk2'] | ||
| assert len(llm.streamed_kwargs) == 1 | ||
| assert 'stop' in llm.streamed_kwargs[0] | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| def test_invoke_with_stop_fallback_empty_stop_kwargs(): | ||
| """Verify that an error does not trigger a retry if stop_kwargs is empty.""" | ||
| llm = MockInvokeLLM() | ||
| def failing_invoke(prompt, **kwargs): | ||
| llm.invoked_kwargs.append(kwargs) | ||
| raise ValueError("unsupported_parameter: 'stop'") | ||
| llm.invoke = failing_invoke | ||
|
|
||
| with pytest.raises(ValueError, match="unsupported_parameter: 'stop'"): | ||
| _invoke_with_stop_fallback(llm, 'Hello', {}) | ||
|
|
||
| assert len(llm.invoked_kwargs) == 1 # No retry occurred | ||
|
|
||
| def test_invoke_with_stop_fallback_unrelated_error(): | ||
| """Verify that an unrelated error does not trigger a retry even with stop_kwargs.""" | ||
| llm = MockInvokeLLM() | ||
| def failing_invoke(prompt, **kwargs): | ||
| llm.invoked_kwargs.append(kwargs) | ||
| raise ValueError('Rate limit exceeded') | ||
| llm.invoke = failing_invoke | ||
|
|
||
| stop_kwargs = {'stop': ['\nObservation:']} | ||
| with pytest.raises(ValueError, match='Rate limit exceeded'): | ||
| _invoke_with_stop_fallback(llm, 'Hello', stop_kwargs) | ||
|
|
||
| assert len(llm.invoked_kwargs) == 1 # No retry occurred | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| def test_stream_with_stop_fallback_empty_stop_kwargs(): | ||
| """Verify that a stream error does not trigger a retry if stop_kwargs is empty.""" | ||
| llm = MockStreamLLM() | ||
| def failing_stream(prompt, **kwargs): | ||
| llm.streamed_kwargs.append(kwargs) | ||
| def gen(): | ||
| raise ValueError("unsupported_parameter: 'stop'") | ||
| yield 'never' | ||
| return gen() | ||
| llm.stream = failing_stream | ||
|
|
||
| with pytest.raises(ValueError, match="unsupported_parameter: 'stop'"): | ||
| list(_stream_with_stop_fallback(llm, 'Hello', {})) | ||
|
|
||
| assert len(llm.streamed_kwargs) == 1 # No retry occurred | ||
|
|
||
|
|
||
| def test_stream_with_stop_fallback_unrelated_error(): | ||
| """Verify that an unrelated stream error does not trigger a retry even with stop_kwargs.""" | ||
| llm = MockStreamLLM() | ||
| def failing_stream(prompt, **kwargs): | ||
| llm.streamed_kwargs.append(kwargs) | ||
| def gen(): | ||
| raise ValueError('Rate limit exceeded') | ||
| yield 'never' | ||
| return gen() | ||
| llm.stream = failing_stream | ||
|
|
||
| stop_kwargs = {'stop': ['\nObservation:']} | ||
| with pytest.raises(ValueError, match='Rate limit exceeded'): | ||
| list(_stream_with_stop_fallback(llm, 'Hello', stop_kwargs)) | ||
|
|
||
| assert len(llm.streamed_kwargs) == 1 # No retry occurred | ||
|
|
||
|
|
||
| def test_stream_with_stop_fallback_mid_stream_error(): | ||
| """Verify that an error raised after the first chunk does not trigger a retry, locking in no-duplicate behavior.""" | ||
| llm = MockStreamLLM() | ||
| def failing_stream(prompt, **kwargs): | ||
| llm.streamed_kwargs.append(kwargs) | ||
| def gen(): | ||
| yield 'chunk1' | ||
| raise ValueError("unsupported_parameter: 'stop'") | ||
| return gen() | ||
| llm.stream = failing_stream | ||
|
|
||
| stop_kwargs = {'stop': ['\nObservation:']} | ||
| with pytest.raises(ValueError, match="unsupported_parameter: 'stop'"): | ||
| # The generator will yield 'chunk1', then raise the error. | ||
| # It should NOT retry because the try/except block does not wrap the main yield loop. | ||
| list(_stream_with_stop_fallback(llm, 'Hello', stop_kwargs)) | ||
|
|
||
| assert len(llm.streamed_kwargs) == 1 # No retry occurred | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 risk: the
exceptwraps the whole body includingyield from it, so an error that matches the string filter after chunks were already yielded restarts the stream from scratch and duplicates everything already sent to the UI. Thefirst = next(it)special-casing suggests the intent was to guard only stream creation + first chunk — restructure so only that part is inside thetry:Same principle as the existing guard at
_chat_string_responses("Only retry non-streaming if nothing has reached the UI").