[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
This commit is contained in:
parent
cd50f9e51d
commit
18673a562a
682 changed files with 25903 additions and 8390 deletions
|
|
@ -210,12 +210,17 @@ def test_structured_tool_call_after_visible_preface_is_executed(monkeypatch):
|
|||
},
|
||||
)
|
||||
]
|
||||
assert any(e.get("type") == "tool_end" and e.get("tool_name") == "render_html" for e in events)
|
||||
assert any(
|
||||
e.get("type") == "tool_end" and e.get("tool_name") == "render_html"
|
||||
for e in events
|
||||
)
|
||||
|
||||
# The second llama-server request should include the assistant preface
|
||||
# plus the structured tool call, preserving OpenAI-compatible ordering.
|
||||
assert len(payloads) == 2
|
||||
assistant_messages = [m for m in payloads[1]["messages"] if m.get("role") == "assistant"]
|
||||
assistant_messages = [
|
||||
m for m in payloads[1]["messages"] if m.get("role") == "assistant"
|
||||
]
|
||||
assert assistant_messages[-1]["content"] == "Here is the canvas.\n\n"
|
||||
assert assistant_messages[-1]["tool_calls"][0]["id"] == tool_call_id
|
||||
assert assistant_messages[-1]["tool_calls"][0]["function"]["name"] == "render_html"
|
||||
|
|
@ -246,12 +251,17 @@ def test_streamed_reasoning_answer_emits_backend_summary(monkeypatch):
|
|||
assert content_texts[0] == "<think>I am thinking."
|
||||
assert content_texts[1] == "<think>I am thinking. Still thinking."
|
||||
# The final event closes the block and appends the answer.
|
||||
assert content_texts[-1] == "<think>I am thinking. Still thinking.</think>Final answer."
|
||||
assert (
|
||||
content_texts[-1]
|
||||
== "<think>I am thinking. Still thinking.</think>Final answer."
|
||||
)
|
||||
|
||||
summary_index = next(
|
||||
i for i, event in enumerate(events) if event["type"] == "reasoning_summary"
|
||||
)
|
||||
final_content_index = max(i for i, event in enumerate(events) if event["type"] == "content")
|
||||
final_content_index = max(
|
||||
i for i, event in enumerate(events) if event["type"] == "content"
|
||||
)
|
||||
assert summary_index < final_content_index
|
||||
assert events[summary_index]["duration_ms"] == 62000
|
||||
|
||||
|
|
@ -354,8 +364,12 @@ def test_reasoning_before_structured_tool_closes_think_block(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
tool_start_index = next(i for i, e in enumerate(events) if e["type"] == "tool_start")
|
||||
content_before_tool = [e["text"] for e in events[:tool_start_index] if e["type"] == "content"]
|
||||
tool_start_index = next(
|
||||
i for i, e in enumerate(events) if e["type"] == "tool_start"
|
||||
)
|
||||
content_before_tool = [
|
||||
e["text"] for e in events[:tool_start_index] if e["type"] == "content"
|
||||
]
|
||||
# Reasoning streamed live, then closed before the tool -- balanced block.
|
||||
assert content_before_tool[0] == "<think>Let me search."
|
||||
assert content_before_tool[-1] == "<think>Let me search.</think>"
|
||||
|
|
@ -463,8 +477,12 @@ def test_reasoning_before_bare_json_tool_closes_think_block(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
tool_start_index = next(i for i, e in enumerate(events) if e["type"] == "tool_start")
|
||||
content_before_tool = [e["text"] for e in events[:tool_start_index] if e["type"] == "content"]
|
||||
tool_start_index = next(
|
||||
i for i, e in enumerate(events) if e["type"] == "tool_start"
|
||||
)
|
||||
content_before_tool = [
|
||||
e["text"] for e in events[:tool_start_index] if e["type"] == "content"
|
||||
]
|
||||
assert content_before_tool[0] == "<think>Searching now."
|
||||
assert content_before_tool[-1] == "<think>Searching now.</think>"
|
||||
# The bare-JSON call text was drained, never surfaced as content.
|
||||
|
|
@ -509,7 +527,8 @@ def test_consumed_tool_final_pass_emits_latest_reasoning_summary(monkeypatch):
|
|||
final_content_index = next(
|
||||
i
|
||||
for i, event in enumerate(events)
|
||||
if event.get("type") == "content" and "Final from tool." in event.get("text", "")
|
||||
if event.get("type") == "content"
|
||||
and "Final from tool." in event.get("text", "")
|
||||
)
|
||||
assert final_summary_index < final_content_index
|
||||
|
||||
|
|
@ -565,7 +584,9 @@ def test_repeat_render_html_nudge_is_not_user_visible_error(monkeypatch):
|
|||
]
|
||||
final_stream = [_sse({"content": "Short note."}), _done()]
|
||||
payloads: list[dict] = []
|
||||
backend = _make_backend(monkeypatch, [first_stream, repeat_stream, final_stream], payloads)
|
||||
backend = _make_backend(
|
||||
monkeypatch, [first_stream, repeat_stream, final_stream], payloads
|
||||
)
|
||||
|
||||
calls: list[tuple[str, dict]] = []
|
||||
|
||||
|
|
@ -674,11 +695,16 @@ def test_render_html_success_drops_tool_schema_before_final_pass(monkeypatch):
|
|||
|
||||
assert len(payloads) == 2
|
||||
assert "tools" not in payloads[1]
|
||||
assert any(event.get("type") == "content" and event.get("text") == "Done." for event in events)
|
||||
assert any(
|
||||
event.get("type") == "content" and event.get("text") == "Done."
|
||||
for event in events
|
||||
)
|
||||
final_user_messages = [
|
||||
m.get("content", "") for m in payloads[1]["messages"] if m.get("role") == "user"
|
||||
]
|
||||
assert not any("used all available tool calls" in message for message in final_user_messages)
|
||||
assert not any(
|
||||
"used all available tool calls" in message for message in final_user_messages
|
||||
)
|
||||
|
||||
|
||||
def test_non_consecutive_duplicate_web_search_is_internal_noop(monkeypatch):
|
||||
|
|
@ -759,7 +785,9 @@ def test_non_consecutive_duplicate_web_search_is_internal_noop(monkeypatch):
|
|||
|
||||
events = list(
|
||||
backend.generate_chat_completion_with_tools(
|
||||
messages = [{"role": "user", "content": "search gpus in 2026 prices and use python"}],
|
||||
messages = [
|
||||
{"role": "user", "content": "search gpus in 2026 prices and use python"}
|
||||
],
|
||||
tools = tools,
|
||||
max_tool_iterations = 3,
|
||||
)
|
||||
|
|
@ -874,7 +902,9 @@ def test_duplicate_web_search_noop_allows_distinct_followup_tool(monkeypatch):
|
|||
|
||||
events = list(
|
||||
backend.generate_chat_completion_with_tools(
|
||||
messages = [{"role": "user", "content": "search gpus in 2026 prices and use python"}],
|
||||
messages = [
|
||||
{"role": "user", "content": "search gpus in 2026 prices and use python"}
|
||||
],
|
||||
tools = tools,
|
||||
max_tool_iterations = 4,
|
||||
)
|
||||
|
|
@ -990,13 +1020,14 @@ def test_repeated_duplicate_noop_transitions_to_final_pass(monkeypatch):
|
|||
)
|
||||
|
||||
assert calls == [("web_search", {"query": "gpu prices 2026"})]
|
||||
assert [event.get("tool_call_id") for event in events if event.get("type") == "tool_end"] == [
|
||||
"call_search_1"
|
||||
]
|
||||
assert [
|
||||
event.get("tool_call_id") for event in events if event.get("type") == "tool_end"
|
||||
] == ["call_search_1"]
|
||||
assert len(payloads) == 4
|
||||
assert "tools" not in payloads[-1]
|
||||
assert any(
|
||||
event.get("type") == "content" and event.get("text") == "Final answer from first search."
|
||||
event.get("type") == "content"
|
||||
and event.get("text") == "Final answer from first search."
|
||||
for event in events
|
||||
)
|
||||
|
||||
|
|
@ -1050,9 +1081,9 @@ def test_same_turn_duplicate_web_search_is_internal_noop(monkeypatch):
|
|||
)
|
||||
|
||||
assert calls == [("web_search", {"query": "gpu prices 2026"})]
|
||||
assert [event.get("tool_call_id") for event in events if event.get("type") == "tool_end"] == [
|
||||
"call_search_1"
|
||||
]
|
||||
assert [
|
||||
event.get("tool_call_id") for event in events if event.get("type") == "tool_end"
|
||||
] == ["call_search_1"]
|
||||
assert not [
|
||||
event
|
||||
for event in events
|
||||
|
|
@ -1073,19 +1104,28 @@ def test_same_turn_duplicate_does_not_drop_later_parallel_call(monkeypatch):
|
|||
"index": 0,
|
||||
"id": "call_a1",
|
||||
"type": "function",
|
||||
"function": {"name": "web_search", "arguments": json.dumps({"query": "a"})},
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"arguments": json.dumps({"query": "a"}),
|
||||
},
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"id": "call_a2",
|
||||
"type": "function",
|
||||
"function": {"name": "web_search", "arguments": json.dumps({"query": "a"})},
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"arguments": json.dumps({"query": "a"}),
|
||||
},
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"id": "call_b",
|
||||
"type": "function",
|
||||
"function": {"name": "web_search", "arguments": json.dumps({"query": "b"})},
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"arguments": json.dumps({"query": "b"}),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -1135,7 +1175,9 @@ def test_same_turn_duplicate_does_not_drop_later_parallel_call(monkeypatch):
|
|||
assert "previous tool request" not in after[2]["content"].lower()
|
||||
|
||||
|
||||
def test_same_turn_repeated_render_html_does_not_emit_second_provisional_start(monkeypatch):
|
||||
def test_same_turn_repeated_render_html_does_not_emit_second_provisional_start(
|
||||
monkeypatch,
|
||||
):
|
||||
same_turn_render_calls = [
|
||||
_sse(
|
||||
{
|
||||
|
|
@ -1165,7 +1207,9 @@ def test_same_turn_repeated_render_html_does_not_emit_second_provisional_start(m
|
|||
]
|
||||
final_stream = [_sse({"content": "Final answer."}), _done()]
|
||||
payloads: list[dict] = []
|
||||
backend = _make_backend(monkeypatch, [same_turn_render_calls, final_stream], payloads)
|
||||
backend = _make_backend(
|
||||
monkeypatch, [same_turn_render_calls, final_stream], payloads
|
||||
)
|
||||
|
||||
calls: list[tuple[str, dict]] = []
|
||||
|
||||
|
|
@ -1242,7 +1286,9 @@ def test_disabled_tool_call_is_internal_noop(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
assert not [event for event in events if event.get("type") in {"tool_start", "tool_end"}]
|
||||
assert not [
|
||||
event for event in events if event.get("type") in {"tool_start", "tool_end"}
|
||||
]
|
||||
assert len(payloads) == 2
|
||||
disabled_nudges = [
|
||||
message
|
||||
|
|
@ -1325,7 +1371,8 @@ def test_render_html_success_does_not_reprompt_render_html_intent(monkeypatch):
|
|||
assert len(payloads) == 2
|
||||
assert len(calls) == 1
|
||||
assert any(
|
||||
event.get("type") == "content" and event.get("text") == "I will now use render_html again."
|
||||
event.get("type") == "content"
|
||||
and event.get("text") == "I will now use render_html again."
|
||||
for event in events
|
||||
)
|
||||
|
||||
|
|
@ -1370,7 +1417,9 @@ def test_internal_reprompt_attempts_do_not_duplicate_visible_text(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
|
||||
content_texts = [
|
||||
event.get("text", "") for event in events if event.get("type") == "content"
|
||||
]
|
||||
assert content_texts == ["I will use render_html now."]
|
||||
assert len(payloads) == _MAX_REPROMPTS + 1
|
||||
|
||||
|
|
@ -1414,7 +1463,9 @@ def test_forced_reprompt_plain_final_answer_is_visible(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
|
||||
content_texts = [
|
||||
event.get("text", "") for event in events if event.get("type") == "content"
|
||||
]
|
||||
assert content_texts == [
|
||||
"I will use render_html now.",
|
||||
"No tool is needed. Final answer: use a red square.",
|
||||
|
|
@ -1456,7 +1507,9 @@ def test_internal_reprompt_disabled_when_auto_heal_disabled(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
|
||||
content_texts = [
|
||||
event.get("text", "") for event in events if event.get("type") == "content"
|
||||
]
|
||||
assert content_texts == ["I will use render_html now."]
|
||||
assert len(payloads) == 1
|
||||
|
||||
|
|
@ -1498,7 +1551,9 @@ def test_internal_reprompt_disabled_when_nudge_tool_calls_false(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
|
||||
content_texts = [
|
||||
event.get("text", "") for event in events if event.get("type") == "content"
|
||||
]
|
||||
assert content_texts == ["I will use render_html now."]
|
||||
assert len(payloads) == 1
|
||||
|
||||
|
|
@ -1545,7 +1600,10 @@ def test_textual_mistral_marker_not_leaked_when_inline_with_preface(monkeypatch)
|
|||
# Textual Mistral ``[TOOL_CALLS]`` inline with visible preface: the DRAINING flush must use the
|
||||
# shared parser patterns (which know ``[TOOL_CALLS]``); the legacy set leaked the marker to clients.
|
||||
streams = [
|
||||
[_sse({"content": 'Let me search. [TOOL_CALLS]web_search{"query":"cats"}'}), _done()],
|
||||
[
|
||||
_sse({"content": 'Let me search. [TOOL_CALLS]web_search{"query":"cats"}'}),
|
||||
_done(),
|
||||
],
|
||||
[_sse({"content": "done"}), _done()],
|
||||
]
|
||||
payloads: list[dict] = []
|
||||
|
|
@ -1719,7 +1777,9 @@ def test_reprompted_tool_call_still_streams_final_answer(monkeypatch):
|
|||
)
|
||||
|
||||
assert len(calls) == 1
|
||||
content_texts = [event.get("text", "") for event in events if event.get("type") == "content"]
|
||||
content_texts = [
|
||||
event.get("text", "") for event in events if event.get("type") == "content"
|
||||
]
|
||||
assert content_texts == ["I will use render_html now.", "Final note after tool."]
|
||||
assert len(payloads) == 3
|
||||
|
||||
|
|
@ -1739,12 +1799,16 @@ def test_confirm_tool_calls_allow_executes_gguf_tool(monkeypatch):
|
|||
return "OK"
|
||||
|
||||
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)
|
||||
monkeypatch.setattr("core.inference.llama_cpp.new_approval_id", lambda: "approval-1")
|
||||
monkeypatch.setattr(
|
||||
"core.inference.llama_cpp.new_approval_id", lambda: "approval-1"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"core.inference.llama_cpp.begin_tool_decision",
|
||||
lambda *_a, **_k: object(),
|
||||
)
|
||||
monkeypatch.setattr("core.inference.llama_cpp.wait_tool_decision", lambda *_a, **_k: "allow")
|
||||
monkeypatch.setattr(
|
||||
"core.inference.llama_cpp.wait_tool_decision", lambda *_a, **_k: "allow"
|
||||
)
|
||||
|
||||
events = list(
|
||||
backend.generate_chat_completion_with_tools(
|
||||
|
|
@ -1761,7 +1825,10 @@ def test_confirm_tool_calls_allow_executes_gguf_tool(monkeypatch):
|
|||
assert starts[0]["approval_id"]
|
||||
assert starts[0]["awaiting_confirmation"] is True
|
||||
assert calls == [("python", {"code": "print(1)"})]
|
||||
assert any(event.get("type") == "tool_end" and event.get("result") == "OK" for event in events)
|
||||
assert any(
|
||||
event.get("type") == "tool_end" and event.get("result") == "OK"
|
||||
for event in events
|
||||
)
|
||||
|
||||
|
||||
def test_confirm_tool_calls_close_after_prompt_cleans_gguf_slot(monkeypatch):
|
||||
|
|
@ -1822,7 +1889,10 @@ def test_confirm_tool_calls_skips_gguf_rag_autoinject(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
assert any(event.get("type") == "content" and event.get("text") == "Done." for event in events)
|
||||
assert any(
|
||||
event.get("type") == "content" and event.get("text") == "Done."
|
||||
for event in events
|
||||
)
|
||||
|
||||
|
||||
def test_confirm_tool_calls_deny_skips_gguf_tool_and_retry_can_execute(monkeypatch):
|
||||
|
|
@ -1845,7 +1915,9 @@ def test_confirm_tool_calls_deny_skips_gguf_tool_and_retry_can_execute(monkeypat
|
|||
approvals = iter(["approval-1", "approval-2"])
|
||||
|
||||
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)
|
||||
monkeypatch.setattr("core.inference.llama_cpp.new_approval_id", lambda: next(approvals))
|
||||
monkeypatch.setattr(
|
||||
"core.inference.llama_cpp.new_approval_id", lambda: next(approvals)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"core.inference.llama_cpp.begin_tool_decision",
|
||||
lambda *_a, **_k: object(),
|
||||
|
|
@ -1882,7 +1954,9 @@ def _streamed_structured_tool_call(
|
|||
deltas (id + name on the first delta), mirroring how llama-server streams a
|
||||
large tool-call argument such as a full HTML/code file."""
|
||||
args_json = json.dumps(arguments)
|
||||
fragments = [args_json[i : i + frag] for i in range(0, len(args_json), frag)] or [""]
|
||||
fragments = [args_json[i : i + frag] for i in range(0, len(args_json), frag)] or [
|
||||
""
|
||||
]
|
||||
chunks = [
|
||||
_sse(
|
||||
{
|
||||
|
|
@ -1898,7 +1972,9 @@ def _streamed_structured_tool_call(
|
|||
)
|
||||
]
|
||||
for fragment in fragments[1:]:
|
||||
chunks.append(_sse({"tool_calls": [{"index": 0, "function": {"arguments": fragment}}]}))
|
||||
chunks.append(
|
||||
_sse({"tool_calls": [{"index": 0, "function": {"arguments": fragment}}]})
|
||||
)
|
||||
chunks.append(_done())
|
||||
return chunks
|
||||
|
||||
|
|
@ -1913,7 +1989,9 @@ def test_large_python_tool_call_emits_early_provisional_start(monkeypatch):
|
|||
args_json = json.dumps({"code": big_code})
|
||||
assert len(args_json) > _PROVISIONAL_ARGS_MIN_CHARS
|
||||
|
||||
first_stream = _streamed_structured_tool_call("python", {"code": big_code}, "call_py_big")
|
||||
first_stream = _streamed_structured_tool_call(
|
||||
"python", {"code": big_code}, "call_py_big"
|
||||
)
|
||||
final_stream = [_sse({"content": "Done."}), _done()]
|
||||
payloads: list[dict] = []
|
||||
backend = _make_backend(monkeypatch, [first_stream, final_stream], payloads)
|
||||
|
|
@ -1950,7 +2028,9 @@ def test_large_python_tool_call_emits_early_provisional_start(monkeypatch):
|
|||
assert events.index(provisional[0]) < events.index(real[0])
|
||||
|
||||
assert calls == [("python", {"code": big_code})]
|
||||
assert any(e.get("type") == "tool_end" and e.get("tool_name") == "python" for e in events)
|
||||
assert any(
|
||||
e.get("type") == "tool_end" and e.get("tool_name") == "python" for e in events
|
||||
)
|
||||
|
||||
|
||||
def test_auto_mode_render_html_suppresses_provisional_card_under_confirm(monkeypatch):
|
||||
|
|
@ -1964,7 +2044,9 @@ def test_auto_mode_render_html_suppresses_provisional_card_under_confirm(monkeyp
|
|||
payloads: list[dict] = []
|
||||
backend = _make_backend(monkeypatch, [first_stream, final_stream], payloads)
|
||||
|
||||
monkeypatch.setattr("core.inference.tools.execute_tool", lambda name, arguments, **_k: "OK")
|
||||
monkeypatch.setattr(
|
||||
"core.inference.tools.execute_tool", lambda name, arguments, **_k: "OK"
|
||||
)
|
||||
|
||||
events = list(
|
||||
backend.generate_chat_completion_with_tools(
|
||||
|
|
@ -1990,7 +2072,9 @@ def test_small_python_tool_call_has_no_provisional_start(monkeypatch):
|
|||
"""A small tool-call argument finishes streaming instantly, so it keeps the
|
||||
existing behavior of a single (real) tool_start with no provisional card."""
|
||||
|
||||
first_stream = _structured_tool_call("python", {"code": "print(1)"}, "call_py_small")
|
||||
first_stream = _structured_tool_call(
|
||||
"python", {"code": "print(1)"}, "call_py_small"
|
||||
)
|
||||
final_stream = [_sse({"content": "Done."}), _done()]
|
||||
payloads: list[dict] = []
|
||||
backend = _make_backend(monkeypatch, [first_stream, final_stream], payloads)
|
||||
|
|
@ -2017,7 +2101,9 @@ def _streamed_parallel_tool_calls(specs, frag: int = 24) -> list[str]:
|
|||
chunks: list[str] = []
|
||||
for index, (tool_name, arguments, call_id) in enumerate(specs):
|
||||
args_json = json.dumps(arguments)
|
||||
fragments = [args_json[i : i + frag] for i in range(0, len(args_json), frag)] or [""]
|
||||
fragments = [
|
||||
args_json[i : i + frag] for i in range(0, len(args_json), frag)
|
||||
] or [""]
|
||||
chunks.append(
|
||||
_sse(
|
||||
{
|
||||
|
|
@ -2034,7 +2120,13 @@ def _streamed_parallel_tool_calls(specs, frag: int = 24) -> list[str]:
|
|||
)
|
||||
for fragment in fragments[1:]:
|
||||
chunks.append(
|
||||
_sse({"tool_calls": [{"index": index, "function": {"arguments": fragment}}]})
|
||||
_sse(
|
||||
{
|
||||
"tool_calls": [
|
||||
{"index": index, "function": {"arguments": fragment}}
|
||||
]
|
||||
}
|
||||
)
|
||||
)
|
||||
chunks.append(_done())
|
||||
return chunks
|
||||
|
|
@ -2079,7 +2171,9 @@ def test_parallel_large_tool_calls_each_emit_provisional_start(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
provisional = [e for e in events if e.get("type") == "tool_start" and not e.get("arguments")]
|
||||
provisional = [
|
||||
e for e in events if e.get("type") == "tool_start" and not e.get("arguments")
|
||||
]
|
||||
assert sorted(e["tool_call_id"] for e in provisional) == ["call_py", "call_term"]
|
||||
assert all(e["provenance"].get("provisional") is True for e in provisional)
|
||||
# Both calls actually executed (parallel tool use is enabled by default).
|
||||
|
|
@ -2124,13 +2218,17 @@ def test_parallel_disabled_suppresses_provisional_for_later_calls(monkeypatch):
|
|||
)
|
||||
)
|
||||
|
||||
provisional = [e for e in events if e.get("type") == "tool_start" and not e.get("arguments")]
|
||||
provisional = [
|
||||
e for e in events if e.get("type") == "tool_start" and not e.get("arguments")
|
||||
]
|
||||
assert [e["tool_call_id"] for e in provisional] == ["call_py"]
|
||||
# Only the first call executes when parallel use is disabled.
|
||||
assert calls == [("python", {"code": big_code})]
|
||||
# The lone provisional is closed exactly once (no dangling card).
|
||||
closing = [
|
||||
e for e in events if e.get("type") == "tool_end" and e.get("tool_call_id") == "call_py"
|
||||
e
|
||||
for e in events
|
||||
if e.get("type") == "tool_end" and e.get("tool_call_id") == "call_py"
|
||||
]
|
||||
assert len(closing) == 1
|
||||
|
||||
|
|
@ -2142,7 +2240,9 @@ def test_connect_error_during_tool_call_closes_provisional_card(monkeypatch):
|
|||
import httpx
|
||||
|
||||
big_code = "total = 0\n" + "\n".join(f"total += {i}" for i in range(120))
|
||||
fragments = _streamed_structured_tool_call("python", {"code": big_code}, "call_py_err")
|
||||
fragments = _streamed_structured_tool_call(
|
||||
"python", {"code": big_code}, "call_py_err"
|
||||
)
|
||||
# Drop the trailing [DONE]; raise a connection error after the fragments
|
||||
# stream (and after the provisional card has been emitted).
|
||||
fragments = fragments[:-1]
|
||||
|
|
@ -2172,7 +2272,9 @@ def test_connect_error_during_tool_call_closes_provisional_card(monkeypatch):
|
|||
assert "Lost connection" in str(exc)
|
||||
|
||||
assert raised
|
||||
provisional = [e for e in collected if e.get("type") == "tool_start" and not e.get("arguments")]
|
||||
provisional = [
|
||||
e for e in collected if e.get("type") == "tool_start" and not e.get("arguments")
|
||||
]
|
||||
assert len(provisional) == 1
|
||||
assert provisional[0]["tool_call_id"] == "call_py_err"
|
||||
# The provisional card is closed before the error propagates.
|
||||
|
|
@ -2219,7 +2321,9 @@ def test_empty_tool_call_id_does_not_emit_provisional_card(monkeypatch):
|
|||
)
|
||||
|
||||
# No provisional card (empty-args tool_start) was surfaced for the empty id.
|
||||
provisional = [e for e in events if e.get("type") == "tool_start" and not e.get("arguments")]
|
||||
provisional = [
|
||||
e for e in events if e.get("type") == "tool_start" and not e.get("arguments")
|
||||
]
|
||||
assert provisional == []
|
||||
# The real call still executes despite the missing id.
|
||||
assert calls == [("python", {"code": big_code})]
|
||||
|
|
@ -2325,7 +2429,9 @@ def test_incomplete_bare_json_truncation_is_not_leaked(monkeypatch):
|
|||
assert all('{"name"' not in t for t in content_texts), content_texts
|
||||
|
||||
|
||||
def test_gguf_truncated_ordinary_json_with_name_key_is_shown_not_suppressed(monkeypatch):
|
||||
def test_gguf_truncated_ordinary_json_with_name_key_is_shown_not_suppressed(
|
||||
monkeypatch,
|
||||
):
|
||||
"""A truncated markerless object whose "name" is NOT an enabled tool (a person
|
||||
record cut off mid-stream, ``{"name":"Alice","age":``) must still be shown. The
|
||||
end-of-stream ``_is_bare_tc`` heuristic routed any ``{...,"name",...}`` fragment
|
||||
|
|
@ -2416,7 +2522,9 @@ def test_gguf_oversized_disabled_name_json_is_preserved(monkeypatch):
|
|||
cap = 16384
|
||||
big = "A" * (cap + 5000)
|
||||
answer = '{"name":"Alice","parameters":{"bio":"' + big # never closes
|
||||
first_stream = [_sse({"content": answer[i : i + 2000]}) for i in range(0, len(answer), 2000)]
|
||||
first_stream = [
|
||||
_sse({"content": answer[i : i + 2000]}) for i in range(0, len(answer), 2000)
|
||||
]
|
||||
first_stream.append(_done())
|
||||
payloads: list[dict] = []
|
||||
backend = _make_backend(monkeypatch, [first_stream], payloads)
|
||||
|
|
@ -2760,7 +2868,10 @@ def test_gguf_streaming_keeps_bare_args_before_think_block(monkeypatch):
|
|||
assert calls == [], calls
|
||||
content_texts = [e.get("text", "") for e in events if e.get("type") == "content"]
|
||||
assert content_texts, events
|
||||
assert content_texts[-1] == "Please pass foo[ARGS] <think>pause</think> to the template."
|
||||
assert (
|
||||
content_texts[-1]
|
||||
== "Please pass foo[ARGS] <think>pause</think> to the template."
|
||||
)
|
||||
|
||||
|
||||
def test_gguf_inactive_name_args_in_prose_is_not_drained(monkeypatch):
|
||||
|
|
@ -2795,10 +2906,14 @@ def test_gguf_inactive_name_args_in_prose_is_not_drained(monkeypatch):
|
|||
assert not any(e.get("type") in ("tool_start", "tool_end") for e in events), events
|
||||
content_texts = [e.get("text", "") for e in events if e.get("type") == "content"]
|
||||
# The inactive ``foo[ARGS]{...}`` is prose: the name-gated strip keeps the whole sentence.
|
||||
assert any('foo[ARGS]{"x":1} is just syntax.' in t for t in content_texts), content_texts
|
||||
assert any(
|
||||
'foo[ARGS]{"x":1} is just syntax.' in t for t in content_texts
|
||||
), content_texts
|
||||
|
||||
|
||||
def test_gguf_inactive_rehearsal_before_active_call_executes_and_keeps_prose(monkeypatch):
|
||||
def test_gguf_inactive_rehearsal_before_active_call_executes_and_keeps_prose(
|
||||
monkeypatch,
|
||||
):
|
||||
"""BUG X (#5704): an inactive ``foo[ARGS]{...}`` before a real ``web_search[ARGS]{...}``
|
||||
in one delta must NOT swallow the real call; web_search executes while the inactive
|
||||
rehearsal stays visible as prose."""
|
||||
|
|
@ -2854,7 +2969,9 @@ def test_gguf_rehearsal_prefix_and_tail_hold_recognise_spent_one_shot():
|
|||
assert not _is_rehearsal_prefix("render_html", active_only)
|
||||
assert _is_rehearsal_prefix("render_html", original)
|
||||
assert _held_rehearsal_tail_len("answer render_html", active_only) == 0
|
||||
assert _held_rehearsal_tail_len("answer render_html", original) == len("render_html")
|
||||
assert _held_rehearsal_tail_len("answer render_html", original) == len(
|
||||
"render_html"
|
||||
)
|
||||
|
||||
|
||||
def test_gguf_oversized_bare_json_not_leaked_and_executes(monkeypatch):
|
||||
|
|
@ -2863,7 +2980,9 @@ def test_gguf_oversized_bare_json_not_leaked_and_executes(monkeypatch):
|
|||
cap = 16384
|
||||
big = "A" * (cap + 5000)
|
||||
full = '{"name":"python","parameters":{"code":"' + big + '"}}'
|
||||
first_stream = [_sse({"content": full[i : i + 2000]}) for i in range(0, len(full), 2000)]
|
||||
first_stream = [
|
||||
_sse({"content": full[i : i + 2000]}) for i in range(0, len(full), 2000)
|
||||
]
|
||||
first_stream.append(_done())
|
||||
final_stream = [_sse({"content": "done"}), _done()]
|
||||
payloads: list[dict] = []
|
||||
|
|
@ -2884,7 +3003,9 @@ def test_gguf_oversized_bare_json_not_leaked_and_executes(monkeypatch):
|
|||
)
|
||||
|
||||
content_texts = [e.get("text", "") for e in events if e.get("type") == "content"]
|
||||
assert not any(t.lstrip().startswith('{"name') for t in content_texts), content_texts[:1]
|
||||
assert not any(
|
||||
t.lstrip().startswith('{"name') for t in content_texts
|
||||
), content_texts[:1]
|
||||
assert calls and calls[0][0] == "python"
|
||||
assert len(calls[0][1].get("code", "")) > cap
|
||||
|
||||
|
|
@ -2926,7 +3047,8 @@ def test_gguf_textual_fallback_caps_distinct_tool_calls_per_turn(monkeypatch):
|
|||
|
||||
n = _MAX_TOOL_CALLS_PER_TURN + 4
|
||||
blocks = "".join(
|
||||
'<tool_call>{"name":"t%d","arguments":{"i":%d}}</tool_call>' % (i, i) for i in range(n)
|
||||
'<tool_call>{"name":"t%d","arguments":{"i":%d}}</tool_call>' % (i, i)
|
||||
for i in range(n)
|
||||
)
|
||||
first_stream = [_sse({"content": blocks}), _done()]
|
||||
final_stream = [_sse({"content": "done"}), _done()]
|
||||
|
|
@ -2942,7 +3064,9 @@ def test_gguf_textual_fallback_caps_distinct_tool_calls_per_turn(monkeypatch):
|
|||
list(
|
||||
backend.generate_chat_completion_with_tools(
|
||||
messages = [{"role": "user", "content": "go"}],
|
||||
tools = [{"type": "function", "function": {"name": f"t{i}"}} for i in range(n)],
|
||||
tools = [
|
||||
{"type": "function", "function": {"name": f"t{i}"}} for i in range(n)
|
||||
],
|
||||
max_tool_iterations = 1,
|
||||
)
|
||||
)
|
||||
|
|
@ -2954,7 +3078,9 @@ def test_gguf_textual_fallback_caps_distinct_tool_calls_per_turn(monkeypatch):
|
|||
|
||||
def test_gguf_textual_fallback_collapses_duplicate_tool_calls(monkeypatch):
|
||||
"""Exact-duplicate textual calls in one turn collapse to a single execution."""
|
||||
blocks = '<tool_call>{"name":"web_search","arguments":{"query":"cats"}}</tool_call>' * 5
|
||||
blocks = (
|
||||
'<tool_call>{"name":"web_search","arguments":{"query":"cats"}}</tool_call>' * 5
|
||||
)
|
||||
first_stream = [_sse({"content": blocks}), _done()]
|
||||
final_stream = [_sse({"content": "done"}), _done()]
|
||||
payloads: list[dict] = []
|
||||
|
|
@ -2977,7 +3103,9 @@ def test_gguf_textual_fallback_collapses_duplicate_tool_calls(monkeypatch):
|
|||
assert len(calls) == 1, [c[0] for c in calls]
|
||||
|
||||
|
||||
def test_gguf_drain_truncated_enabled_name_json_preserved_when_auto_heal_disabled(monkeypatch):
|
||||
def test_gguf_drain_truncated_enabled_name_json_preserved_when_auto_heal_disabled(
|
||||
monkeypatch,
|
||||
):
|
||||
"""Auto-Heal OFF keeps a truncated enabled-name fragment visible; ON suppresses it (strip gated on auto_heal_tool_calls)."""
|
||||
|
||||
trunc = '{"name":"web_search","parameters":{"query":"weather'
|
||||
|
|
@ -2998,7 +3126,9 @@ def test_gguf_drain_truncated_enabled_name_json_preserved_when_auto_heal_disable
|
|||
auto_heal_tool_calls = auto_heal,
|
||||
)
|
||||
)
|
||||
contents = "".join(e.get("text", "") for e in events if e.get("type") == "content")
|
||||
contents = "".join(
|
||||
e.get("text", "") for e in events if e.get("type") == "content"
|
||||
)
|
||||
return calls, contents
|
||||
|
||||
calls_off, contents_off = _run(False)
|
||||
|
|
@ -3015,7 +3145,8 @@ def test_gguf_valid_tool_calls_respect_max_tool_iterations(monkeypatch):
|
|||
# More tool-call streams than the budget: if re-prompt slots leaked into the budget (the bug) the
|
||||
# loop would run 2+3=5 rounds; honouring it stops after 2, then a tool-less final-answer pass.
|
||||
streams = [
|
||||
_structured_tool_call("web_search", {"query": f"q{i}"}, f"call_{i}") for i in range(6)
|
||||
_structured_tool_call("web_search", {"query": f"q{i}"}, f"call_{i}")
|
||||
for i in range(6)
|
||||
]
|
||||
payloads: list[dict] = []
|
||||
backend = _make_backend(monkeypatch, streams, payloads)
|
||||
|
|
@ -3040,7 +3171,8 @@ def test_gguf_valid_tool_calls_respect_max_tool_iterations(monkeypatch):
|
|||
# The final pass is the budget-exhausted nudge and carries no tools.
|
||||
assert _tool_names(payloads[2]) == [], _tool_names(payloads[2])
|
||||
assert any(
|
||||
m.get("role") == "user" and "used all available tool calls" in m.get("content", "")
|
||||
m.get("role") == "user"
|
||||
and "used all available tool calls" in m.get("content", "")
|
||||
for m in payloads[2]["messages"]
|
||||
), payloads[2]["messages"]
|
||||
|
||||
|
|
@ -3126,7 +3258,9 @@ def test_structured_tool_args_stream_to_provisional_card(monkeypatch):
|
|||
|
||||
# The streamed display path must not perturb execution or the model view.
|
||||
assert executed == [("python", {"code": code})]
|
||||
assistant_messages = [m for m in payloads[1]["messages"] if m.get("role") == "assistant"]
|
||||
assistant_messages = [
|
||||
m for m in payloads[1]["messages"] if m.get("role") == "assistant"
|
||||
]
|
||||
tc = assistant_messages[-1]["tool_calls"][0]
|
||||
assert tc["id"] == call_id
|
||||
# Controller re-serializes args (normalized JSON); parsed payload unchanged.
|
||||
|
|
@ -3186,7 +3320,9 @@ def test_ordinary_json_answer_streams_no_tool_args(monkeypatch):
|
|||
"""A large ordinary JSON answer (no enabled tool name) must not spawn a
|
||||
provisional card or tool_args events; it stays a normal content answer."""
|
||||
|
||||
answer = json.dumps({"result": "fine", "data": ["x" * 40] * 12, "note": "not a tool call"})
|
||||
answer = json.dumps(
|
||||
{"result": "fine", "data": ["x" * 40] * 12, "note": "not a tool call"}
|
||||
)
|
||||
chunks = [answer[i : i + 64] for i in range(0, len(answer), 64)]
|
||||
stream = [_sse({"content": chunk}) for chunk in chunks] + [_done()]
|
||||
payloads: list[dict] = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue