mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 22:15:10 +00:00
fix(llm): record Responses chain state only for completed responses
The non-streaming path recorded the response id, reasoning items and the staged system-head hash before checking the terminal status, so a response that came back failed could become the id the next call chains onto and commit a head the stored transcript never received. Chain state is now recorded after the status checks; a response cut off by max_output_tokens still records it, since its input was accepted.
This commit is contained in:
1 parent
dbee30a048
commit
5837c300df
2 files changed
+57
-2
No files matched your search
@@ -1551,11 +1551,13 @@ class OpenAILLM(BaseLLM):
|
||||
"OpenAI Responses request completed id=%s",
|
||||
getattr(response, "id", None),
|
||||
)
|
||||
self._record_responses_metadata(response)
|
||||
content, tool_calls, reasoning_items = self._parse_responses_output(
|
||||
response
|
||||
)
|
||||
self._last_reasoning_items = reasoning_items
|
||||
# Chain state (response id, head hash, reasoning items) is recorded
|
||||
# only for a response the provider actually completed — a failed one
|
||||
# must not become the id the next call chains onto, nor commit a head
|
||||
# the stored transcript never received.
|
||||
details = getattr(response, "incomplete_details", None)
|
||||
incomplete_reason = getattr(details, "reason", None)
|
||||
if getattr(response, "status", None) == "incomplete":
|
||||
@@ -1564,6 +1566,9 @@ class OpenAILLM(BaseLLM):
|
||||
self._responses_status_error(response)
|
||||
or "Responses API incomplete: unknown reason"
|
||||
)
|
||||
# Cut off by the output cap: the input was accepted and stored.
|
||||
self._record_responses_metadata(response)
|
||||
self._last_reasoning_items = reasoning_items
|
||||
self._last_finish_reason = "length"
|
||||
if tools:
|
||||
return _RespChoice(
|
||||
@@ -1574,6 +1579,8 @@ class OpenAILLM(BaseLLM):
|
||||
status_error = self._responses_status_error(response)
|
||||
if status_error:
|
||||
raise RuntimeError(status_error)
|
||||
self._record_responses_metadata(response)
|
||||
self._last_reasoning_items = reasoning_items
|
||||
if tools:
|
||||
self._remember_reasoning(tool_calls, reasoning_items)
|
||||
message = _RespMessage(
|
||||
|
||||
@@ -354,3 +354,51 @@ def test_failed_chained_request_resends_changed_head_on_retry(monkeypatch):
|
||||
_accepted(llm, "resp_2")
|
||||
after, _ = llm._build_responses_input(_messages("sys v2"), "resp_2")
|
||||
assert _roles(after) == ["user"]
|
||||
|
||||
|
||||
# ── a failed non-streaming response must not become chain state ─────────────
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_failed_response_records_no_chain_state(monkeypatch):
|
||||
llm = _make_llm(monkeypatch)
|
||||
# An accepted turn with head "sys v1".
|
||||
llm._build_responses_input(_messages("sys v1"), None)
|
||||
_accepted(llm, "resp_ok")
|
||||
committed = llm._chain_system_hash
|
||||
|
||||
failed = types.SimpleNamespace(
|
||||
id="resp_bad", status="failed", error=types.SimpleNamespace(message="boom"),
|
||||
incomplete_details=None, output=[], usage=None,
|
||||
)
|
||||
llm.client.responses.create = MagicMock(return_value=failed)
|
||||
with pytest.raises(RuntimeError):
|
||||
llm._responses_gen(
|
||||
"gpt-5.6", _messages("sys v2"), tools=None, previous_response_id="resp_ok"
|
||||
)
|
||||
|
||||
# Neither the failed id nor the head it carried became chain state
|
||||
# (the in-turn id is cleared before every request; the retry chains via
|
||||
# the caller's previous_response_id)...
|
||||
assert llm._last_response_id != "resp_bad"
|
||||
assert llm._chain_system_hash == committed
|
||||
# ...so the retry chains onto resp_ok and re-sends the changed head.
|
||||
retry, prev = llm._build_responses_input(_messages("sys v2"), "resp_ok")
|
||||
assert prev == "resp_ok"
|
||||
assert _roles(retry) == ["system", "user"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_output_capped_response_still_records_chain_state(monkeypatch):
|
||||
llm = _make_llm(monkeypatch)
|
||||
capped = types.SimpleNamespace(
|
||||
id="resp_len", status="incomplete",
|
||||
incomplete_details=types.SimpleNamespace(reason="max_output_tokens"),
|
||||
output=[types.SimpleNamespace(type="message", content=[
|
||||
types.SimpleNamespace(type="output_text", text="partial")])],
|
||||
usage=None, error=None,
|
||||
)
|
||||
llm.client.responses.create = MagicMock(return_value=capped)
|
||||
assert llm._responses_gen("gpt-5.6", _messages(), tools=None) == "partial"
|
||||
assert llm._last_response_id == "resp_len"
|
||||
assert llm._chain_system_hash is not None
|
||||
Reference in new issue
Block a user