fix: address completion verdict review findings

- fidelity cacheability also requires a non-empty projected body, so
  think-tag-only answers are no longer cached
- a no-argument tool sent with arguments null only is a deterministic
  dialect: ResultInvalid tool_arguments_null instead of retried
- text mode treats choice index null as 0 and no longer lets False pose
  as 0
- null-only tool_calls elements no longer produce an empty tool_calls
  list; non-stream fidelity checks role before the verdict; warning
  paths are length-bounded
- docs: ARCHITECTURE 7.8 field counts, README cache/watchdog wording,
  CHANGELOG notes, review record in the design, hook note in the plan

Red: 8 new tests failed before this change (e.g.
test_openai_cacheable_requires_projected_body,
test_null_arguments_tool_is_result_invalid_stream,
test_text_mode_choice_index_null_is_zero[None-True]).
This commit is contained in:
2026-10-15 00:22:07 -04:00
parent 31942f55f1
commit abcdf6930e
12 changed files with 179 additions and 27 deletions
+36 -1
View File
@@ -85,7 +85,7 @@ async def test_eof_after_finish_usage_only_frame_and_tools():
result = await _run(frames, done=False, mode="openai")
tool = result.completion.message["tool_calls"][0]
assert tool["function"] == {"name": "lookup", "arguments": '{"q": 1}'}
assert result.stream_end == "eof_after_finish"
assert result.stream_end == "eof_after_finish" and result.usage_source == "measured"
@pytest.mark.parametrize(
@@ -416,3 +416,38 @@ async def test_openai_reasoning_length_single_attempt_via_client():
await client._transport.aclose()
assert len(calls) == 1 and response.call_stats.attempts == 1
assert response.stream_end == "eof_after_finish" and response.finish_reason == "length"
async def test_eof_after_finish_stop_single_attempt_via_client():
"""#31 验收 1 经治理链: stop + usage 无 DONE,attempts=1、不重试到耗尽。"""
from tests.unit.test_client import _client
calls = []
def handler(_):
calls.append(1)
return _sse_stream(_assistant(content="ok"), _f({}, "stop", _USAGE), done=False)
client = _client(handler=handler)
try:
response = await client.chat([{"role": "user", "content": "hi"}])
finally:
await client._transport.aclose()
assert len(calls) == 1 and response.call_stats.attempts == 1
assert response.stream_end == "eof_after_finish" and response.usage_source == "measured"
async def test_text_salvage_with_length_partial_content_delivered():
"""文本 salvage + length + 部分正文(finish 后无 usage): 交付、salvaged。"""
frames = [_assistant(content="part"), _f({}, finish="length")]
result = await _run(frames, done=False, missing_done="salvage")
assert result.content == "part" and result.stream_end == "salvaged"
assert result.finish_reason == "length"
async def test_null_arguments_tool_is_result_invalid_stream():
"""审查 I2: 无参函数 arguments 全程 null → ResultInvalid,不重试。"""
tool = {"index": 0, "id": "c", "type": "function", "function": {"name": "f", "arguments": None}}
frames = [_assistant(tool_calls=[tool]), _f({}, "tool_calls", _USAGE)]
with pytest.raises(ResultInvalidError, match="tool_arguments_null"):
await _run(frames, done=True, mode="openai")