fix(fireworks): set finish_reason=tool_calls for content-embedded tool calls - #33037
Conversation
…l calls Fireworks AI returns tool calls as a JSON string in message.content with finish_reason='stop' (issue BerriAI#7209). transform_response builds the ModelResponse directly via ModelResponse(**completion_response), bypassing convert_to_model_response_object, then moves the content into message.tool_calls without updating finish_reason. The response therefore had tool_calls set but finish_reason='stop', so OpenAI-style agent loops (and litellm routing) that branch on finish_reason=='tool_calls' treated the turn as finished and never executed the tool. After converting content to tool_calls, set finish_reason to 'tool_calls' when a tool call is present and finish_reason is still 'stop' - matching the shared converter (convert_dict_to_response) and providers like Ollama/xAI. Plain-text responses are unaffected. Adds regression tests for both cases.
Greptile SummaryThis PR fixes Fireworks AI's non-streaming
Confidence Score: 5/5The change is a small, focused one-liner inside a Fireworks-specific code path with no impact on other providers. The fix is correctly scoped to transform_response (the non-streaming path where the bug existed), does not touch shared infrastructure, and is covered by two new offline regression tests. The only finding is a minor redundant length guard. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/llms/fireworks_ai/chat/transformation.py | Adds finish_reason="tool_calls" correction after content-embedded tool calls are promoted to message.tool_calls; logic is correct and well-scoped to the non-streaming path. |
| tests/test_litellm/llms/fireworks_ai/chat/test_fireworks_ai_chat_transformation.py | Adds two focused regression tests: one confirming finish_reason flips to "tool_calls" when content holds a tool call, one confirming plain-text responses remain "stop". Both are offline/mock-only. |
Reviews (1): Last reviewed commit: "fix(fireworks): set finish_reason=tool_c..." | Re-trigger Greptile
| if ( | ||
| typed_choice.finish_reason == "stop" | ||
| and typed_choice.message.tool_calls | ||
| and len(typed_choice.message.tool_calls) > 0 | ||
| ): |
There was a problem hiding this comment.
The
len(typed_choice.message.tool_calls) > 0 guard is redundant — the preceding truthiness check typed_choice.message.tool_calls already returns False for both None and an empty list, so the length check adds no new protection.
| if ( | |
| typed_choice.finish_reason == "stop" | |
| and typed_choice.message.tool_calls | |
| and len(typed_choice.message.tool_calls) > 0 | |
| ): | |
| if ( | |
| typed_choice.finish_reason == "stop" | |
| and typed_choice.message.tool_calls | |
| ): |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Heads up: the red |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The truthiness check on message.tool_calls already excludes None and empty lists, so the explicit len(...) > 0 is redundant (per Greptile review on BerriAI#33037).
Relevant issues
Fixes #33036
Pre-Submission checklist
Screenshots / Proof of Fix
Reproducible offline (no keys/network).
Before (base
3d63eda) — tool call present but finish_reason wrong:After (this PR):
New regression tests (the tool-call one fails on base, passes here) + full Fireworks file green:
Type
🐛 Bug Fix
Changes
transform_responsebuilds theModelResponsedirectly (bypassingconvert_to_model_response_object) and converts Fireworks' content-embedded tool call intomessage.tool_callswithout updatingfinish_reason. Setfinish_reason="tool_calls"after the conversion when a tool call is present andfinish_reasonis still"stop"— matching the shared response converter and providers like Ollama/xAI. Plain-text responses are unaffected.cc @ishaan-jaff @krrish-berri-2 — isolated Fireworks tool-calling fix with regression tests. Thanks!