core/agent: unify provider finish_reason mapping

This commit is contained in:
2026-08-01 01:05:26 +08:00
parent 5198a8da20
commit 626012e92f
4 changed files with 73 additions and 14 deletions
+33
View File
@@ -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"]
+3 -9
View File
@@ -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:
+5 -5
View File
@@ -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: