Skip to content

Commit f20f303

Browse files
authored
Update test_client.py
1 parent e2bd187 commit f20f303

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

tests/test_client.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,87 @@ def test_reasoning_content_streamed_and_captured(self):
7171
)
7272
c.close()
7373

74+
def test_tool_call_null_index_does_not_merge_or_crash(self):
75+
"""A chunk with ``"index": null`` must neither crash nor merge.
76+
77+
``dict.get("index", 0)`` only defaults when the key is ABSENT, so
78+
an explicit null used to land None in the accumulator: the final
79+
``sorted(tc_index)`` then raised TypeError on the mixed int/None
80+
keys. The differing id marks a new call, so the two stay apart.
81+
"""
82+
fake_openai_server.reset_state()
83+
fake_openai_server.STREAM_CHUNKS = [
84+
{
85+
"choices": [
86+
{
87+
"delta": {
88+
"tool_calls": [
89+
{
90+
"index": 0,
91+
"id": "call_1",
92+
"function": {"name": "Read", "arguments": "{}"},
93+
}
94+
]
95+
}
96+
}
97+
]
98+
},
99+
{
100+
"choices": [
101+
{
102+
"delta": {
103+
"tool_calls": [
104+
{
105+
"index": None,
106+
"id": "call_2",
107+
"function": {"name": "Grep", "arguments": "{}"},
108+
}
109+
]
110+
}
111+
}
112+
]
113+
},
114+
]
115+
c = make_client()
116+
try:
117+
msg, _ = c.chat([Message(role="user", content="hi")])
118+
finally:
119+
c.close()
120+
fake_openai_server.reset_state()
121+
self.assertEqual(
122+
[(t.id, t.name) for t in msg.tool_calls],
123+
[("call_1", "Read"), ("call_2", "Grep")],
124+
)
125+
126+
def test_tool_call_fragments_without_index_still_merge(self):
127+
"""Fragments of ONE call from a backend that omits ``index``
128+
keep accumulating into the same slot: only a differing id (not a
129+
missing index) starts a new call."""
130+
fake_openai_server.reset_state()
131+
fake_openai_server.STREAM_CHUNKS = [
132+
{
133+
"choices": [
134+
{
135+
"delta": {
136+
"tool_calls": [
137+
{"id": "call_1", "function": {"name": "Read", "arguments": '{"a"'}}
138+
]
139+
}
140+
}
141+
]
142+
},
143+
{"choices": [{"delta": {"tool_calls": [{"function": {"arguments": ": 1}"}}]}}]},
144+
]
145+
c = make_client()
146+
try:
147+
msg, _ = c.chat([Message(role="user", content="hi")])
148+
finally:
149+
c.close()
150+
fake_openai_server.reset_state()
151+
self.assertEqual(len(msg.tool_calls), 1)
152+
self.assertEqual(msg.tool_calls[0].id, "call_1")
153+
self.assertEqual(msg.tool_calls[0].arguments, '{"a": 1}')
154+
74155
def test_streaming_default_sends_stream_true(self):
75156
"""The default mode is streaming: the request body must carry
76157
stream=True plus stream_options requesting usage chunks."""
@@ -191,6 +272,43 @@ def test_sync_chat_tool_calls_and_reasoning(self):
191272
self.assertEqual(usage.input_tokens, 7)
192273
self.assertEqual(usage.output_tokens, 9)
193274

275+
def test_sync_chat_null_indices_stay_separate_calls(self):
276+
"""Non-streaming calls carrying ``"index": null`` must not collapse
277+
into one slot: ``tc.get("index", i)`` returned None for every call,
278+
splicing their ids, names and arguments into a single garbage
279+
call. The positional fallback keeps them apart."""
280+
fake_openai_server.NON_STREAM_RESPONSE = {
281+
"choices": [
282+
{
283+
"message": {
284+
"role": "assistant",
285+
"content": "",
286+
"tool_calls": [
287+
{
288+
"index": None,
289+
"id": "call_1",
290+
"function": {"name": "Read", "arguments": '{"a": 1}'},
291+
},
292+
{
293+
"index": None,
294+
"id": "call_2",
295+
"function": {"name": "Grep", "arguments": '{"b": 2}'},
296+
},
297+
],
298+
}
299+
}
300+
],
301+
}
302+
c = make_client()
303+
try:
304+
msg, _ = c.chat([Message(role="user", content="hi")], stream=False)
305+
finally:
306+
c.close()
307+
self.assertEqual(
308+
[(t.id, t.name, t.arguments) for t in msg.tool_calls],
309+
[("call_1", "Read", '{"a": 1}'), ("call_2", "Grep", '{"b": 2}')],
310+
)
311+
194312
def test_sync_chat_dict_arguments_normalized(self):
195313
"""Some backends return tool-call arguments as an object rather
196314
than a JSON string; they must be normalized to a string."""

0 commit comments

Comments
 (0)