mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 02:23:42 +00:00
feat(types): expose native_finish_reason in provider_specific_fields
When a provider's finish_reason is mapped to a different OpenAI-compatible value (e.g. "MALFORMED_FUNCTION_CALL" → "stop"), the original value is now preserved in choices[].provider_specific_fields["native_finish_reason"]. This allows agent loops to distinguish between different stop conditions without breaking the unified OpenAI-compatible finish_reason mapping. Also returns a defensive copy from get_finish_reason_mapping() to prevent accidental mutation of the global _FINISH_REASON_MAP.
This commit is contained in:
@@ -51,6 +51,28 @@ Here's what an example response looks like
|
||||
}
|
||||
```
|
||||
|
||||
## Native Finish Reason
|
||||
|
||||
LiteLLM maps all provider-specific `finish_reason` values to OpenAI-compatible values (`stop`, `length`, `tool_calls`, `function_call`, `content_filter`). When the original provider value differs from the mapped value, it is preserved in `provider_specific_fields["native_finish_reason"]`.
|
||||
|
||||
This is useful for agent loops that need to distinguish between different stop conditions (e.g., Gemini's `MALFORMED_FUNCTION_CALL` vs a normal `stop`).
|
||||
|
||||
```python
|
||||
response = completion(model="gemini/gemini-2.0-flash", messages=messages)
|
||||
|
||||
choice = response.choices[0]
|
||||
print(choice.finish_reason) # "stop" (OpenAI-compatible)
|
||||
|
||||
# Access the original provider value when it differs:
|
||||
if hasattr(choice, "provider_specific_fields") and choice.provider_specific_fields:
|
||||
native = choice.provider_specific_fields.get("native_finish_reason")
|
||||
if native == "MALFORMED_FUNCTION_CALL":
|
||||
# Handle malformed function call differently from a normal stop
|
||||
pass
|
||||
```
|
||||
|
||||
When the provider already returns an OpenAI-compatible value (e.g., `stop`), `native_finish_reason` is not set.
|
||||
|
||||
## Additional Attributes
|
||||
|
||||
You can also access information like latency.
|
||||
|
||||
@@ -1241,7 +1241,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
||||
"""
|
||||
from litellm.litellm_core_utils.core_helpers import _FINISH_REASON_MAP
|
||||
|
||||
return _FINISH_REASON_MAP
|
||||
return dict(_FINISH_REASON_MAP)
|
||||
|
||||
def translate_exception_str(self, exception_string: str):
|
||||
if (
|
||||
|
||||
@@ -1326,7 +1326,11 @@ class Choices(SafeAttributeModel, OpenAIObject):
|
||||
**params,
|
||||
):
|
||||
if finish_reason is not None:
|
||||
params["finish_reason"] = map_finish_reason(finish_reason)
|
||||
mapped = map_finish_reason(finish_reason)
|
||||
params["finish_reason"] = mapped
|
||||
if finish_reason != mapped:
|
||||
provider_specific_fields = provider_specific_fields or {}
|
||||
provider_specific_fields["native_finish_reason"] = finish_reason
|
||||
else:
|
||||
params["finish_reason"] = "stop"
|
||||
if index is not None:
|
||||
|
||||
@@ -223,3 +223,59 @@ def test_chat_completion_token_logprob_invalid_top_logprobs_rejected():
|
||||
logprob=-0.31725305,
|
||||
top_logprobs="invalid_string",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# native_finish_reason in provider_specific_fields
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestNativeFinishReason:
|
||||
"""Choices exposes the raw provider finish_reason in provider_specific_fields
|
||||
when it differs from the mapped OpenAI-compatible value."""
|
||||
|
||||
def test_provider_reason_exposed_when_mapped(self):
|
||||
from litellm.types.utils import Choices
|
||||
|
||||
choice = Choices(finish_reason="end_turn")
|
||||
assert choice.finish_reason == "stop"
|
||||
assert choice.provider_specific_fields["native_finish_reason"] == "end_turn"
|
||||
|
||||
def test_provider_reason_not_set_when_already_openai(self):
|
||||
from litellm.types.utils import Choices
|
||||
|
||||
choice = Choices(finish_reason="stop")
|
||||
assert choice.finish_reason == "stop"
|
||||
assert not hasattr(choice, "provider_specific_fields")
|
||||
|
||||
def test_provider_reason_merged_with_existing_fields(self):
|
||||
from litellm.types.utils import Choices
|
||||
|
||||
choice = Choices(
|
||||
finish_reason="max_tokens",
|
||||
provider_specific_fields={"citations": [{"url": "http://example.com"}]},
|
||||
)
|
||||
assert choice.finish_reason == "length"
|
||||
assert choice.provider_specific_fields["native_finish_reason"] == "max_tokens"
|
||||
assert choice.provider_specific_fields["citations"] == [{"url": "http://example.com"}]
|
||||
|
||||
def test_gemini_safety_reason_exposed(self):
|
||||
from litellm.types.utils import Choices
|
||||
|
||||
choice = Choices(finish_reason="SAFETY")
|
||||
assert choice.finish_reason == "content_filter"
|
||||
assert choice.provider_specific_fields["native_finish_reason"] == "SAFETY"
|
||||
|
||||
def test_anthropic_tool_use_reason_exposed(self):
|
||||
from litellm.types.utils import Choices
|
||||
|
||||
choice = Choices(finish_reason="tool_use")
|
||||
assert choice.finish_reason == "tool_calls"
|
||||
assert choice.provider_specific_fields["native_finish_reason"] == "tool_use"
|
||||
|
||||
def test_max_tokens_reason_exposed(self):
|
||||
from litellm.types.utils import Choices
|
||||
|
||||
choice = Choices(finish_reason="MAX_TOKENS")
|
||||
assert choice.finish_reason == "length"
|
||||
assert choice.provider_specific_fields["native_finish_reason"] == "MAX_TOKENS"
|
||||
|
||||
Reference in New Issue
Block a user