mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(core): preserve unmapped finish_reason and add 'refusal' mapping
Previously, map_finish_reason() would default to 'stop' for any unmapped finish_reason, which is lossy. This restores the previous behavior of passing through unmapped values as-is, so callers can handle provider-specific finish reasons correctly. Also adds 'refusal' to the finish reason map and type, as Anthropic can return this stop reason (see https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#refusal). Fixes #23793
This commit is contained in:
parent
278c9babc6
commit
017461627e
3 changed files with 12 additions and 8 deletions
|
|
@ -98,16 +98,18 @@ _FINISH_REASON_MAP: dict[str, OpenAIChatCompletionFinishReason] = {
|
|||
"content_filter": "content_filter",
|
||||
# Anthropic Sonnet 4
|
||||
"content_filtered": "content_filter",
|
||||
# Anthropic refusal
|
||||
"refusal": "refusal",
|
||||
}
|
||||
|
||||
|
||||
def map_finish_reason(finish_reason: str) -> OpenAIChatCompletionFinishReason:
|
||||
def map_finish_reason(finish_reason: str) -> str:
|
||||
mapped = _FINISH_REASON_MAP.get(finish_reason)
|
||||
if mapped is None:
|
||||
verbose_logger.warning(
|
||||
"Unmapped finish_reason '%s', defaulting to 'stop'", finish_reason
|
||||
"Unmapped finish_reason '%s', passing through as-is", finish_reason
|
||||
)
|
||||
return "stop"
|
||||
return finish_reason
|
||||
return mapped
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2131,6 +2131,7 @@ OpenAIChatCompletionFinishReason = Literal[
|
|||
"function_call",
|
||||
"tool_calls",
|
||||
"length",
|
||||
"refusal",
|
||||
"guardrail_intervened",
|
||||
"eos",
|
||||
"finish_reason_unspecified",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ def test_reconstruct_model_name_returns_original_for_other_providers():
|
|||
# map_finish_reason tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
VALID_OPENAI_FINISH_REASONS = {"stop", "length", "tool_calls", "function_call", "content_filter"}
|
||||
VALID_OPENAI_FINISH_REASONS = {"stop", "length", "tool_calls", "function_call", "content_filter", "refusal"}
|
||||
|
||||
|
||||
class TestMapFinishReasonAnthropic:
|
||||
|
|
@ -68,6 +68,7 @@ class TestMapFinishReasonAnthropic:
|
|||
("tool_use", "tool_calls"),
|
||||
("compaction", "length"),
|
||||
("content_filtered", "content_filter"),
|
||||
("refusal", "refusal"),
|
||||
],
|
||||
)
|
||||
def test_anthropic_finish_reasons(self, provider_reason: str, expected: str) -> None:
|
||||
|
|
@ -132,11 +133,11 @@ class TestMapFinishReasonOpenAIPassthrough:
|
|||
|
||||
|
||||
class TestMapFinishReasonUnknown:
|
||||
def test_unknown_value_defaults_to_stop(self):
|
||||
assert map_finish_reason("some_unknown_value") == "stop"
|
||||
def test_unknown_value_passes_through(self):
|
||||
assert map_finish_reason("some_unknown_value") == "some_unknown_value"
|
||||
|
||||
def test_empty_string_defaults_to_stop(self):
|
||||
assert map_finish_reason("") == "stop"
|
||||
def test_empty_string_passes_through(self):
|
||||
assert map_finish_reason("") == ""
|
||||
|
||||
|
||||
class TestFinishReasonMapOutputsAreValid:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue