Skip to content

Commit 5f9b33d

Browse files
authored
fix: preserve reasoning and thinking metadata in Message.fit (#327)
**Key Changes:** - Updated `Message.fit` to retain `thinking_blocks` and `reasoning_content` - Added tests to ensure preservation of these metadata fields - Improved robustness for handling objects with optional metadata fields **Added:** - Unit tests for `Message.fit` verifying that `thinking_blocks` and `reasoning_content` are correctly preserved or ignored as appropriate in various scenarios - `tests/test_chat.py` **Changed:** - Enhanced `Message.fit` method to detect and copy `thinking_blocks` and `reasoning_content` from input objects into the resulting `Message`'s metadata only if they are present and non-empty - `rigging/message.py` - Updated logic to ensure that empty or missing `thinking_blocks` and `reasoning_content` are not included in metadata, improving message object cleanliness and consistency Fixes ENG-3882
1 parent 5242afa commit 5f9b33d

File tree

2 files changed

+107
-5
lines changed

2 files changed

+107
-5
lines changed

rigging/message.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,11 +1360,19 @@ def fit(cls, message: t.Union["Message", MessageDict, Content, str]) -> "Message
13601360
"""Helper function to convert various common types to a Message object."""
13611361
if isinstance(message, (str, *ContentTypes)):
13621362
return cls(role="user", content=[message])
1363-
return (
1364-
cls.model_validate(message)
1365-
if isinstance(message, dict)
1366-
else message.model_copy(deep=True)
1367-
)
1363+
if isinstance(message, dict):
1364+
return cls.model_validate(message)
1365+
1366+
metadata: dict[str, t.Any] = {}
1367+
if hasattr(message, "thinking_blocks") and message.thinking_blocks:
1368+
metadata["thinking_blocks"] = message.thinking_blocks
1369+
if hasattr(message, "reasoning_content") and message.reasoning_content:
1370+
metadata["reasoning_content"] = message.reasoning_content
1371+
1372+
result = message.model_copy(deep=True)
1373+
if metadata:
1374+
result.metadata.update(metadata)
1375+
return result
13681376

13691377
@classmethod
13701378
def apply_to_list(cls, messages: t.Sequence["Message"], **kwargs: str) -> list["Message"]:

tests/test_chat.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,100 @@ def test_message_from_str() -> None:
4444
assert msg.content == "Please say hello."
4545

4646

47+
def test_message_fit_preserves_thinking_blocks() -> None:
48+
from pydantic import BaseModel
49+
50+
class LiteLLMMessage(BaseModel):
51+
model_config = {"extra": "allow"}
52+
role: str
53+
content: str
54+
thinking_blocks: list[dict[str, t.Any]] | None = None
55+
metadata: dict[str, t.Any] = {}
56+
57+
def model_copy(self, deep: bool = False) -> "Message": # noqa: ARG002
58+
return Message(role=self.role, content=self.content, metadata=self.metadata.copy()) # type: ignore[return-value]
59+
60+
original = LiteLLMMessage(
61+
role="assistant",
62+
content="Hello!",
63+
thinking_blocks=[{"type": "thinking", "content": "Let me think..."}],
64+
)
65+
fitted = Message.fit(original) # type: ignore[arg-type]
66+
assert fitted.metadata.get("thinking_blocks") == [{"type": "thinking", "content": "Let me think..."}]
67+
68+
69+
def test_message_fit_preserves_reasoning_content() -> None:
70+
from pydantic import BaseModel
71+
72+
class LiteLLMMessage(BaseModel):
73+
model_config = {"extra": "allow"}
74+
role: str
75+
content: str
76+
reasoning_content: str | None = None
77+
metadata: dict[str, t.Any] = {}
78+
79+
def model_copy(self, deep: bool = False) -> "Message": # noqa: ARG002
80+
return Message(role=self.role, content=self.content, metadata=self.metadata.copy()) # type: ignore[return-value]
81+
82+
original = LiteLLMMessage(
83+
role="assistant",
84+
content="Hello!",
85+
reasoning_content="I reasoned about this carefully.",
86+
)
87+
fitted = Message.fit(original) # type: ignore[arg-type]
88+
assert fitted.metadata.get("reasoning_content") == "I reasoned about this carefully."
89+
90+
91+
def test_message_fit_preserves_both_thinking_fields() -> None:
92+
from pydantic import BaseModel
93+
94+
class LiteLLMMessage(BaseModel):
95+
model_config = {"extra": "allow"}
96+
role: str
97+
content: str
98+
thinking_blocks: list[dict[str, t.Any]] | None = None
99+
reasoning_content: str | None = None
100+
metadata: dict[str, t.Any] = {}
101+
102+
def model_copy(self, deep: bool = False) -> "Message": # noqa: ARG002
103+
return Message(role=self.role, content=self.content, metadata=self.metadata.copy()) # type: ignore[return-value]
104+
105+
original = LiteLLMMessage(
106+
role="assistant",
107+
content="Hello!",
108+
thinking_blocks=[{"type": "thinking", "content": "Thinking..."}],
109+
reasoning_content="Reasoning...",
110+
)
111+
fitted = Message.fit(original) # type: ignore[arg-type]
112+
assert fitted.metadata.get("thinking_blocks") == [{"type": "thinking", "content": "Thinking..."}]
113+
assert fitted.metadata.get("reasoning_content") == "Reasoning..."
114+
115+
116+
def test_message_fit_ignores_empty_thinking_blocks() -> None:
117+
from pydantic import BaseModel
118+
119+
class LiteLLMMessage(BaseModel):
120+
model_config = {"extra": "allow"}
121+
role: str
122+
content: str
123+
thinking_blocks: list[dict[str, t.Any]] | None = None
124+
reasoning_content: str | None = None
125+
metadata: dict[str, t.Any] = {}
126+
127+
def model_copy(self, deep: bool = False) -> "Message": # noqa: ARG002
128+
return Message(role=self.role, content=self.content, metadata=self.metadata.copy()) # type: ignore[return-value]
129+
130+
original = LiteLLMMessage(
131+
role="assistant",
132+
content="Hello!",
133+
thinking_blocks=[],
134+
reasoning_content="",
135+
)
136+
fitted = Message.fit(original) # type: ignore[arg-type]
137+
assert "thinking_blocks" not in fitted.metadata
138+
assert "reasoning_content" not in fitted.metadata
139+
140+
47141
def test_message_str_representation() -> None:
48142
msg = Message("assistant", "I am an AI assistant.")
49143
assert str(msg) == "[assistant]: I am an AI assistant."

0 commit comments

Comments
 (0)