mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
core/agent: unify provider finish_reason mapping
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
"""Canonical chat-completions ``finish_reason`` vocabulary for provider bridges.
|
||||
|
||||
Provider statuses/stops differ (Responses ``status``/``incomplete_details``,
|
||||
Anthropic ``stop_reason``, DeepSeek reasoning, …); the agent loop and tool
|
||||
execution only understand the chat-completions vocabulary. This module is the
|
||||
single mapping point between raw provider reasons and that vocabulary.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
_LENGTH_REASONS = frozenset({"max_tokens", "max_output_tokens", "length"})
|
||||
_CONTENT_FILTER_REASONS = frozenset({"content_filter", "refusal"})
|
||||
_TOOL_REASONS = frozenset({"tool_use", "tool_calls"})
|
||||
|
||||
|
||||
def chat_finish_reason(reason: str | None, *, has_tool_calls: bool) -> str:
|
||||
"""Map a raw provider reason onto the chat ``finish_reason`` vocabulary.
|
||||
|
||||
Canonical values: ``stop``, ``length``, ``content_filter``, ``tool_calls``.
|
||||
Provider-specific stop indicators (``end_turn``, ``stop_sequence``,
|
||||
``completed``, ``None``, …) collapse to ``stop``.
|
||||
"""
|
||||
raw = (reason or "").lower() or None
|
||||
if raw in _LENGTH_REASONS:
|
||||
return "length"
|
||||
if raw in _CONTENT_FILTER_REASONS:
|
||||
return "content_filter"
|
||||
if raw in _TOOL_REASONS or has_tool_calls:
|
||||
return "tool_calls"
|
||||
return "stop"
|
||||
|
||||
|
||||
__all__ = ["chat_finish_reason"]
|
||||
@@ -228,15 +228,9 @@ def anthropic_stop_to_finish_reason(
|
||||
has_tool_calls: bool,
|
||||
) -> str:
|
||||
"""Map Anthropic ``stop_reason`` to a chat-style finish_reason."""
|
||||
reason = (stop_reason or "").lower() or None
|
||||
if reason == "max_tokens":
|
||||
return "length"
|
||||
if reason in {"refusal", "content_filter"}:
|
||||
return "content_filter"
|
||||
if reason == "tool_use" or has_tool_calls:
|
||||
return "tool_calls"
|
||||
# end_turn, stop_sequence, None, …
|
||||
return "stop"
|
||||
from .finish_reason import chat_finish_reason
|
||||
|
||||
return chat_finish_reason(stop_reason, has_tool_calls=has_tool_calls)
|
||||
|
||||
|
||||
def anthropic_usage_to_dict(usage: AnthropicUsage | dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
|
||||
@@ -161,20 +161,20 @@ def responses_status_to_finish_reason(
|
||||
has_tool_calls: bool,
|
||||
) -> str:
|
||||
"""Map Responses ``status`` to a chat-style finish_reason for the agent loop."""
|
||||
from .finish_reason import chat_finish_reason
|
||||
|
||||
status = response.status
|
||||
status_s = status if isinstance(status, str) else None
|
||||
if status_s == "incomplete":
|
||||
details = response.incomplete_details
|
||||
if details is not UNSET and details is not None:
|
||||
raw_reason = details.reason
|
||||
if raw_reason is not UNSET and raw_reason == "content_filter":
|
||||
return "content_filter"
|
||||
if raw_reason is not UNSET:
|
||||
return chat_finish_reason(raw_reason, has_tool_calls=False)
|
||||
return "length"
|
||||
if status_s in {"failed", "cancelled"}:
|
||||
return status_s
|
||||
if has_tool_calls:
|
||||
return "tool_calls"
|
||||
return "stop"
|
||||
return chat_finish_reason(status_s, has_tool_calls=has_tool_calls)
|
||||
|
||||
|
||||
def response_to_chat_completion(response: Response) -> ChatCompletionResponse:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Tests for the canonical finish_reason vocabulary shared by provider bridges."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from plyngent.agent.finish_reason import chat_finish_reason
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw", "has_tool_calls", "expected"),
|
||||
[
|
||||
(None, False, "stop"),
|
||||
(None, True, "tool_calls"),
|
||||
("", False, "stop"),
|
||||
("end_turn", False, "stop"),
|
||||
("stop_sequence", False, "stop"),
|
||||
("completed", False, "stop"),
|
||||
("stop", False, "stop"),
|
||||
("max_tokens", False, "length"),
|
||||
("max_output_tokens", False, "length"),
|
||||
("length", False, "length"),
|
||||
("content_filter", False, "content_filter"),
|
||||
("refusal", False, "content_filter"),
|
||||
("tool_use", False, "tool_calls"),
|
||||
("tool_calls", False, "tool_calls"),
|
||||
("Tool_Use", False, "tool_calls"),
|
||||
("MAX_TOKENS", False, "length"),
|
||||
],
|
||||
)
|
||||
def test_chat_finish_reason(raw: str | None, has_tool_calls: bool, expected: str) -> None:
|
||||
assert chat_finish_reason(raw, has_tool_calls=has_tool_calls) == expected
|
||||
Reference in New Issue
Block a user