fix(llm): don't swallow response_model conversion errors on streaming (#6735) - #6738
fix(llm): don't swallow response_model conversion errors on streaming (#6735)#6738Anai-Guo wants to merge 1 commit into
Conversation
…crewAIInc#6735) When streaming with a response_model, _handle_streaming_response ran the InternalInstructor.to_pydantic() / model_dump_json() conversion inside the try that wraps chunk consumption. Its except Exception salvage branch is meant to return whatever text arrived before a stream broke mid-flight, so a conversion failure was routed through it: the caller got the raw prose back with no exception and no signal that conversion was attempted. A conversion failure is not a broken stream. The stream completed; the output just didn't match the schema. Wrap the conversion in its own try/except that raises a dedicated StructuredOutputConversionError, and re-raise it before the salvage branch (mirroring the existing LLMContextLengthExceededError handling).
📝 WalkthroughWalkthroughStreaming response-model conversion failures now raise ChangesStructured streaming errors
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/crewai/tests/test_llm.py`:
- Around line 1224-1228: Update the test around llm._handle_streaming_response
to capture the StructuredOutputConversionError via pytest.raises as exc_info,
then assert the wrapped exception’s message matches the preserved original
message and its __cause__ is the underlying conversion error. Keep the existing
response_model=_Answer setup and invalid response input unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 21c8409b-3c9b-478c-8977-6c4028995cd6
📒 Files selected for processing (2)
lib/crewai/src/crewai/llm.pylib/crewai/tests/test_llm.py
| with pytest.raises(StructuredOutputConversionError): | ||
| llm._handle_streaming_response( | ||
| {"messages": [{"role": "user", "content": "hi"}]}, | ||
| response_model=_Answer, | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the wrapped error contract, not only its type.
The implementation preserves the original message and chains the conversion error, but this test would still pass if either behavior regressed. Capture exc_info and assert the message and __cause__.
As per coding guidelines, unit tests should focus on observable behavior.
Suggested assertion
- with pytest.raises(StructuredOutputConversionError):
+ with pytest.raises(
+ StructuredOutputConversionError, match="schema mismatch"
+ ) as exc_info:
llm._handle_streaming_response(
{"messages": [{"role": "user", "content": "hi"}]},
response_model=_Answer,
)
+ assert isinstance(exc_info.value.__cause__, ValueError)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| with pytest.raises(StructuredOutputConversionError): | |
| llm._handle_streaming_response( | |
| {"messages": [{"role": "user", "content": "hi"}]}, | |
| response_model=_Answer, | |
| ) | |
| with pytest.raises( | |
| StructuredOutputConversionError, match="schema mismatch" | |
| ) as exc_info: | |
| llm._handle_streaming_response( | |
| {"messages": [{"role": "user", "content": "hi"}]}, | |
| response_model=_Answer, | |
| ) | |
| assert isinstance(exc_info.value.__cause__, ValueError) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/tests/test_llm.py` around lines 1224 - 1228, Update the test
around llm._handle_streaming_response to capture the
StructuredOutputConversionError via pytest.raises as exc_info, then assert the
wrapped exception’s message matches the preserved original message and its
__cause__ is the underlying conversion error. Keep the existing
response_model=_Answer setup and invalid response input unchanged.
Source: Coding guidelines
Problem
Fixes #6735.
When
response_modelis set on a streaming call,_handle_streaming_responseruns theInternalInstructor.to_pydantic()/model_dump_json()conversion inside thetrythat wraps chunk consumption. The
except Exceptionbelow thattryexists to salvage apartial response from a stream that broke mid-flight — it returns
full_responsewheneverthere is text to return.
As a result, a conversion failure is routed through the salvage path: a caller who asked
for a
BaseModel-shaped result gets the raw prose back, with no exception and no signalthat conversion was attempted and failed.
A conversion failure is not a broken stream. The stream completed successfully; the output
just didn't match the schema. Routing it through the salvage path conflates the two.
Fix
Wrap the
response_modelconversion in its owntry/exceptthat raises a dedicatedStructuredOutputConversionError, and re-raise that error before the salvageexcept Exception— mirroring the existing
except LLMContextLengthExceededError: raisehandling in the samemethod. Genuine mid-stream breaks still fall through to the salvage path unchanged.
Scope note
The issue also references the async handler. On
main,_ahandle_streaming_responsedoes notcurrently perform any
response_modelconversion (it returnsfull_responsedirectly), so thereis no conversion-swallow path to fix there today. This PR therefore targets the confirmed sync
path; the async streaming handler ignoring
response_modelentirely is a separate gap and out ofscope here.
Testing
Added
test_streaming_response_model_conversion_failure_raises, which drives a clean stream whosetext fails conversion (
to_pydanticpatched to raise) and asserts the method now raisesStructuredOutputConversionErrorinstead of returning the prose.Verified locally against crewai
1.15.9:'just some prose'(raw text, no exception).StructuredOutputConversionError.ruff 0.15.1checkandformatare clean on the changed source.🤖 Generated with Claude Code