fix(streaming): map unknown finish_reason values to finish_reason_unspecified to prevent ValidationError in stream_chunk_builder - #22673
Conversation
…pecified Some LLM providers return non-standard finish_reason values that are not in the OpenAIChatCompletionFinishReason Literal (e.g. ZhipuAI/GLM returns 'network_error' when a streaming error occurs mid-response). Previously map_finish_reason() fell through with return finish_reason, passing the unknown value directly to Choices.__init__() which calls Pydantic validation. This caused a ValidationError that was caught by stream_chunk_builder() and re-raised as the misleading: litellm.APIError: Error building chunks for logging/streaming usage calculation Fix: after all known provider-specific mappings, check if the value is in the valid set (stop, length, tool_calls, content_filter, function_call, guardrail_intervened, eos, finish_reason_unspecified, malformed_function_call). Any value not in this set is mapped to 'finish_reason_unspecified' instead of being returned as-is. This is consistent with how other unknown stop reasons (e.g. Vertex AI's FINISH_REASON_UNSPECIFIED) are already handled.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR fixes a
Confidence Score: 5/5
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/core_helpers.py | Adds a fallback guard in map_finish_reason() that maps any unrecognized finish_reason value to "finish_reason_unspecified" with a warning log. Uses get_args() on the source-of-truth Literal type to derive the valid set at module level. |
| tests/test_litellm/litellm_core_utils/test_core_helpers.py | Adds comprehensive unit tests for map_finish_reason() covering all known pass-through values, provider-specific mappings (Anthropic, Cohere, Vertex AI), unknown values, empty strings, and a ZhipuAI regression case. All tests are pure function calls with no network access. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["map_finish_reason(finish_reason)"] --> B{Matches known\nprovider mapping?}
B -->|Yes| C["Return mapped value\n(e.g. 'stop', 'length',\n'tool_calls', etc.)"]
B -->|No| D{In valid\nOpenAI set?}
D -->|Yes| E["Return finish_reason as-is"]
D -->|No| F["Log warning\nReturn 'finish_reason_unspecified'"]
F --> G["Choices.__init__() succeeds\n(no ValidationError)"]
C --> G
E --> G
Last reviewed commit: 7ea1202
Per code review feedback: replace the hardcoded _valid_finish_reasons set with a module-level frozenset derived dynamically from the source-of-truth Literal type via typing.get_args(). This ensures the valid-reason check stays in sync automatically when new finish reasons are added to the Literal, and avoids recreating the set on every streaming chunk call.
|
Thanks for the review @greptile-apps! Addressed in the follow-up commit:
Also updated the provider description in the comment from 'ZhipuAI/GLM' → 'ZhipuAI/GLM-5' to be more precise. |
|
Perfect! The changes look great: ✓ Using The implementation is clean and maintains sync with the Literal type automatically. Nice work! |
| # Unknown finish_reason values (e.g. provider-specific error codes like | ||
| # "network_error" from ZhipuAI/GLM-5) are not in OpenAIChatCompletionFinishReason | ||
| # Literal and will cause a Pydantic ValidationError in Choices.__init__. | ||
| # Map them to "finish_reason_unspecified" so the stream can be assembled | ||
| # without raising an exception. | ||
| if finish_reason not in _VALID_OPENAI_FINISH_REASONS: | ||
| return "finish_reason_unspecified" |
There was a problem hiding this comment.
Consider logging unknown finish reasons
Silently mapping unknown values to "finish_reason_unspecified" is the right approach to prevent the ValidationError, but it may hide useful debugging information. Consider adding a verbose_logger.warning() (already imported) when an unknown finish reason is encountered so operators can track which providers are returning non-standard values:
| # Unknown finish_reason values (e.g. provider-specific error codes like | |
| # "network_error" from ZhipuAI/GLM-5) are not in OpenAIChatCompletionFinishReason | |
| # Literal and will cause a Pydantic ValidationError in Choices.__init__. | |
| # Map them to "finish_reason_unspecified" so the stream can be assembled | |
| # without raising an exception. | |
| if finish_reason not in _VALID_OPENAI_FINISH_REASONS: | |
| return "finish_reason_unspecified" | |
| # Unknown finish_reason values (e.g. provider-specific error codes like | |
| # "network_error" from ZhipuAI/GLM-5) are not in OpenAIChatCompletionFinishReason | |
| # Literal and will cause a Pydantic ValidationError in Choices.__init__. | |
| # Map them to "finish_reason_unspecified" so the stream can be assembled | |
| # without raising an exception. | |
| if finish_reason not in _VALID_OPENAI_FINISH_REASONS: | |
| verbose_logger.warning("Unknown finish_reason '%s' mapped to 'finish_reason_unspecified'", finish_reason) | |
| return "finish_reason_unspecified" |
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!
…inish reasons - Add TestMapFinishReason class in test_core_helpers.py covering: - All known OpenAI-native values pass through unchanged (parametrized) - Provider-specific mappings: Anthropic, Cohere, Vertex AI - Unknown/provider-specific values map to 'finish_reason_unspecified' - Regression test for ZhipuAI/GLM-5 'network_error' case - Add verbose_logger.warning() in map_finish_reason() when an unknown finish_reason is encountered, so operators can track which providers return non-standard values
|
I'm waiting for this feature. |
|
Thanks for the review, @krrishdholakia! To clarify — the guard is inside def map_finish_reason(finish_reason: str):
# ... all existing provider-specific if/elif branches unchanged ...
elif finish_reason == "compaction":
return "length"
# new: catch-all for anything not already mapped above
if finish_reason not in _VALID_OPENAI_FINISH_REASONS:
verbose_logger.warning(
"Unknown finish_reason '%s' mapped to 'finish_reason_unspecified'", finish_reason
)
return "finish_reason_unspecified"
return finish_reasonThe If you had a different spot in mind where you think the logic is duplicated, happy to take a look and consolidate. Just point me at the line! |
810de55
into
BerriAI:litellm_oss_staging_03_10_2026
…pecified to prevent ValidationError in stream_chunk_builder (BerriAI#22673) * fix(streaming): map unknown finish_reason values to finish_reason_unspecified Some LLM providers return non-standard finish_reason values that are not in the OpenAIChatCompletionFinishReason Literal (e.g. ZhipuAI/GLM returns 'network_error' when a streaming error occurs mid-response). Previously map_finish_reason() fell through with return finish_reason, passing the unknown value directly to Choices.__init__() which calls Pydantic validation. This caused a ValidationError that was caught by stream_chunk_builder() and re-raised as the misleading: litellm.APIError: Error building chunks for logging/streaming usage calculation Fix: after all known provider-specific mappings, check if the value is in the valid set (stop, length, tool_calls, content_filter, function_call, guardrail_intervened, eos, finish_reason_unspecified, malformed_function_call). Any value not in this set is mapped to 'finish_reason_unspecified' instead of being returned as-is. This is consistent with how other unknown stop reasons (e.g. Vertex AI's FINISH_REASON_UNSPECIFIED) are already handled. * refactor: use get_args(OpenAIChatCompletionFinishReason) for valid set Per code review feedback: replace the hardcoded _valid_finish_reasons set with a module-level frozenset derived dynamically from the source-of-truth Literal type via typing.get_args(). This ensures the valid-reason check stays in sync automatically when new finish reasons are added to the Literal, and avoids recreating the set on every streaming chunk call. * test(map_finish_reason): add unit tests and warning log for unknown finish reasons - Add TestMapFinishReason class in test_core_helpers.py covering: - All known OpenAI-native values pass through unchanged (parametrized) - Provider-specific mappings: Anthropic, Cohere, Vertex AI - Unknown/provider-specific values map to 'finish_reason_unspecified' - Regression test for ZhipuAI/GLM-5 'network_error' case - Add verbose_logger.warning() in map_finish_reason() when an unknown finish_reason is encountered, so operators can track which providers return non-standard values
Problem
Some LLM providers return non-standard
finish_reasonvalues that are not in theOpenAIChatCompletionFinishReasonLiteral. For example, ZhipuAI/GLM returns"network_error"when a mid-stream network error occurs on their side.map_finish_reason()handles many known provider-specific values but previously fell through withreturn finish_reasonfor unrecognized values. When this unknown value reachesChoices.__init__(), Pydantic validation fails, andstream_chunk_builder()catches the exception and raises a misleading generic error:The real root cause (a provider-specific
finish_reasonvalue) was hidden.Full error traceback (production)
Related issue: #22671
Solution
After all known provider-specific mappings in
map_finish_reason(), validate the result against the set of validOpenAIChatCompletionFinishReasonvalues. Any unrecognized value is mapped to"finish_reason_unspecified"rather than being returned as-is.This is consistent with:
FINISH_REASON_UNSPECIFIEDis already handled (maps to"finish_reason_unspecified")Changes
litellm/litellm_core_utils/core_helpers.py: Add validation guard at the end ofmap_finish_reason()to return"finish_reason_unspecified"for any value not in the valid setTesting
Added manual verification: