Coverage for src / lexigram / ai / relay / finish_reasons.py: 100%
24 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-08 23:08 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-08 23:08 +0800
1"""Unified finish-reason and Responses-status mapping tables.
3Every canonical ``finish_reason`` -> wire translation for the OpenAI chat,
4OpenAI Responses, Claude, and Gemini wire formats lives here so mappers and
5emitters never duplicate or drift. Unknown canonical values fall back to a
6safe terminal value rather than raising ``KeyError``; callers that can record
7diagnostics attach a ``finish_reason_adapted`` loss around the fallback.
8"""
10from __future__ import annotations
12from collections.abc import Mapping
14from lexigram.contracts.ai.relay.ir import normalize_finish_reason
15from lexigram.contracts.ai.relay.types import RelayFormat
17__all__ = [
18 "FINISH_REASON_TO_WIRE",
19 "RESPONSES_STATUS_FROM_FINISH",
20 "finish_reason_to_wire",
21 "normalize_finish_reason",
22 "responses_incomplete_from_finish",
23 "responses_status_from_finish",
24]
26#: Canonical finish reason -> wire value per target format.
27FINISH_REASON_TO_WIRE: Mapping[str, Mapping[RelayFormat, str]] = {
28 "stop": {
29 RelayFormat.OPENAI_CHAT: "stop",
30 RelayFormat.CLAUDE: "end_turn",
31 RelayFormat.GEMINI: "STOP",
32 },
33 "length": {
34 RelayFormat.OPENAI_CHAT: "length",
35 RelayFormat.CLAUDE: "max_tokens",
36 RelayFormat.GEMINI: "MAX_TOKENS",
37 },
38 "tool_calls": {
39 RelayFormat.OPENAI_CHAT: "tool_calls",
40 RelayFormat.CLAUDE: "tool_use",
41 RelayFormat.GEMINI: "STOP",
42 },
43 "function_call": {
44 RelayFormat.OPENAI_CHAT: "function_call",
45 RelayFormat.CLAUDE: "tool_use",
46 RelayFormat.GEMINI: "STOP",
47 },
48 "content_filter": {
49 RelayFormat.OPENAI_CHAT: "content_filter",
50 RelayFormat.CLAUDE: "end_turn",
51 RelayFormat.GEMINI: "SAFETY",
52 },
53 "other": {
54 RelayFormat.OPENAI_CHAT: "other",
55 RelayFormat.CLAUDE: "end_turn",
56 RelayFormat.GEMINI: "OTHER",
57 },
58}
60#: Safe terminal value per format for canonical values absent from the table.
61_WIRE_FALLBACK: Mapping[RelayFormat, str] = {
62 RelayFormat.OPENAI_CHAT: "stop",
63 RelayFormat.CLAUDE: "end_turn",
64 RelayFormat.GEMINI: "OTHER",
65}
67#: Canonical finish reason -> (Responses status, incomplete-detail reason).
68RESPONSES_STATUS_FROM_FINISH: Mapping[str, tuple[str, str | None]] = {
69 "stop": ("completed", None),
70 "tool_calls": ("completed", None),
71 "function_call": ("completed", None),
72 "length": ("incomplete", "max_output_tokens"),
73 "content_filter": ("incomplete", "content_filter"),
74 "other": ("incomplete", "other"),
75}
78def finish_reason_to_wire(finish_reason: str | None, target: RelayFormat) -> str:
79 """Map a canonical finish reason onto ``target``'s wire value.
81 Unknown or missing values fall back to the target's terminal value
82 rather than raising ``KeyError``.
83 """
84 if finish_reason is None:
85 return _WIRE_FALLBACK[target]
86 row = FINISH_REASON_TO_WIRE.get(finish_reason)
87 if row is None:
88 return _WIRE_FALLBACK[target]
89 return row.get(target, _WIRE_FALLBACK[target])
92def responses_status_from_finish(
93 finish_reason: str | None,
94) -> tuple[str, str | None]:
95 """Derive a Responses status and incomplete-detail reason."""
96 if finish_reason is None:
97 return "completed", None
98 return RESPONSES_STATUS_FROM_FINISH.get(finish_reason, ("completed", None))
101def responses_incomplete_from_finish(finish_reason: str | None) -> str | None:
102 """Return the Responses incomplete-detail reason, or ``None``."""
103 if finish_reason is None:
104 return None
105 _status, detail = RESPONSES_STATUS_FROM_FINISH.get(
106 finish_reason, ("completed", None)
107 )
108 return detail