-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Expand file tree
/
Copy pathtest_response_input_builder.py
More file actions
212 lines (162 loc) · 7.33 KB
/
test_response_input_builder.py
File metadata and controls
212 lines (162 loc) · 7.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from openai.lib import validate_response_input, get_response_input_items
def _make_reasoning_item(item_id: str = "rs_001") -> Any:
item = MagicMock()
item.type = "reasoning"
item.id = item_id
item.model_dump.return_value = {"type": "reasoning", "id": item_id, "summary": []}
return item
def _make_message_item(item_id: str = "msg_001") -> Any:
item = MagicMock()
item.type = "message"
item.role = "assistant"
item.id = item_id
item.model_dump.return_value = {"type": "message", "role": "assistant", "id": item_id, "content": []}
return item
def _make_response(output: list[Any]) -> Any:
response = MagicMock()
response.output = output
return response
# ---------------------------------------------------------------------------
# get_response_input_items
# ---------------------------------------------------------------------------
def test_get_response_input_items_reasoning_and_message() -> None:
"""Returns both reasoning and message items in order."""
reasoning = _make_reasoning_item("rs_1")
message = _make_message_item("msg_1")
response = _make_response([reasoning, message])
result = get_response_input_items(response)
assert len(result) == 2
assert result[0] == {"type": "reasoning", "id": "rs_1", "summary": []}
assert result[1] == {"type": "message", "role": "assistant", "id": "msg_1", "content": []}
def test_get_response_input_items_message_only() -> None:
"""Returns message items when there are no reasoning items."""
message = _make_message_item("msg_2")
response = _make_response([message])
result = get_response_input_items(response)
assert len(result) == 1
assert result[0] == {"type": "message", "role": "assistant", "id": "msg_2", "content": []}
def test_get_response_input_items_empty() -> None:
"""Returns empty list for empty output."""
response = _make_response([])
result = get_response_input_items(response)
assert result == []
# ---------------------------------------------------------------------------
# validate_response_input
# ---------------------------------------------------------------------------
def test_validate_passes_for_consecutive_pair_dicts() -> None:
"""No error when reasoning immediately precedes assistant message (dict form)."""
items = [
{"type": "reasoning", "id": "rs_1", "summary": []},
{"type": "message", "role": "assistant", "id": "msg_1", "content": []},
]
validate_response_input(items) # should not raise
def test_validate_raises_for_orphaned_reasoning_dicts() -> None:
"""ValueError raised when reasoning is not followed by assistant message (dict form)."""
items = [
{"type": "message", "role": "user", "content": "hello"},
{"type": "reasoning", "id": "rs_1", "summary": []},
{"type": "message", "role": "user", "content": "follow-up"},
{"type": "message", "role": "assistant", "id": "msg_orphan", "content": []},
]
with pytest.raises(ValueError, match="rs_1"):
validate_response_input(items)
def test_validate_raises_error_describes_constraint() -> None:
"""Error message explains the reasoning+message pairing constraint."""
items = [
{"type": "reasoning", "id": "rs_1", "summary": []},
{"type": "message", "role": "user", "content": "hi"},
{"type": "message", "role": "assistant", "id": "msg_bad", "content": []},
]
with pytest.raises(ValueError, match="consecutive pair"):
validate_response_input(items)
def test_validate_passes_for_user_only_messages() -> None:
"""No error when there are only user messages and no reasoning items."""
items = [
{"type": "message", "role": "user", "content": "hello"},
{"type": "message", "role": "user", "content": "how are you"},
]
validate_response_input(items) # should not raise
def test_validate_passes_for_empty_input() -> None:
"""No error for empty input list."""
validate_response_input([]) # should not raise
def test_validate_passes_with_object_form() -> None:
"""Works with object-form items (not dicts), consecutive pair."""
reasoning = MagicMock()
reasoning.type = "reasoning"
reasoning.id = "rs_obj"
message = MagicMock()
message.type = "message"
message.role = "assistant"
message.id = "msg_obj"
validate_response_input([reasoning, message]) # should not raise
def test_validate_raises_with_object_form_orphaned() -> None:
"""Raises ValueError with object-form items when reasoning is not followed by assistant."""
reasoning = MagicMock()
reasoning.type = "reasoning"
reasoning.id = "rs_obj"
user_msg = MagicMock()
user_msg.type = "message"
user_msg.role = "user"
asst_msg = MagicMock()
asst_msg.type = "message"
asst_msg.role = "assistant"
asst_msg.id = "msg_orphan_obj"
with pytest.raises(ValueError, match="rs_obj"):
validate_response_input([reasoning, user_msg, asst_msg])
def test_validate_passes_for_standalone_assistant_with_later_pair() -> None:
"""Standalone assistant message is valid even when a reasoning+assistant pair exists later."""
items = [
{"type": "message", "role": "assistant", "id": "msg_standalone", "content": []},
{"type": "message", "role": "user", "content": "follow-up"},
{"type": "reasoning", "id": "rs_1", "summary": []},
{"type": "message", "role": "assistant", "id": "msg_paired", "content": []},
]
validate_response_input(items) # should not raise
def test_validate_raises_for_reasoning_at_end() -> None:
"""ValueError raised when reasoning item is the last item with no following message."""
items = [
{"type": "message", "role": "user", "content": "hello"},
{"type": "reasoning", "id": "rs_trailing", "summary": []},
]
with pytest.raises(ValueError, match="rs_trailing"):
validate_response_input(items)
def test_get_response_input_items_excludes_parsed_fields() -> None:
"""SDK-only 'parsed' field is stripped from nested content items."""
message = MagicMock()
message.type = "message"
message.model_dump.return_value = {
"type": "message",
"role": "assistant",
"id": "msg_parsed",
"content": [
{"type": "output_text", "text": "hello", "parsed": {"key": "value"}},
],
}
response = _make_response([message])
result = get_response_input_items(response)
assert len(result) == 1
assert "parsed" not in result[0]["content"][0] # type: ignore[index]
def test_get_response_input_items_respects_api_exclude() -> None:
"""SDK-only fields listed in __api_exclude__ are excluded from output."""
full_data = {
"type": "function_call",
"id": "fc_1",
"name": "my_func",
"arguments": "{}",
"parsed_arguments": {"key": "value"},
}
tool_call = MagicMock()
tool_call.type = "function_call"
tool_call.__api_exclude__ = {"parsed_arguments"}
tool_call.model_dump.side_effect = lambda **kwargs: {
k: v for k, v in full_data.items()
if k not in (kwargs.get("exclude") or set())
}
response = _make_response([tool_call])
result = get_response_input_items(response)
assert len(result) == 1
assert "parsed_arguments" not in result[0]