fix(gateway): stamp turn_duration on last AI message only in /messages/page (#4755)

_enrich_thread_message_page inlined its own turn_duration loop that
stamped EVERY AI message of a run, re-introducing #4152 on the
/messages/page endpoint (the legacy /messages and /history endpoints
already route through stamp_turn_duration_on_last_ai after #4163, but
that fix missed the page path introduced earlier in #4065). A
multi-step turn thus rendered the same run lifetime beside every
intermediate AI message, reading as repeated thinking latency.

Replace the inline loop with the shared stamp_turn_duration_on_last_ai
helper so all three message endpoints agree: the run's wall-clock
duration lands on its final visible AI message only.
This commit is contained in:
Baldwinzc 2026-08-11 22:00:57 +08:00 committed by GitHub
parent 6bb376abfd
commit baaf2bad47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 39 additions and 3 deletions

View File

@ -1329,9 +1329,14 @@ async def _enrich_thread_message_page(
"comment": feedback.get("comment"),
}
content = row.get("content")
if isinstance(content, dict) and content.get("type") == "ai" and run_id in run_durations:
content.setdefault("additional_kwargs", {})["turn_duration"] = run_durations[run_id]
# ``turn_duration`` is the run's wall-clock lifetime, not model thinking
# time — stamp it on the run's LAST visible AI message only so the UI does
# not repeat the same number on every intermediate AI message of a
# multi-step turn (#4152). The legacy ``GET /messages`` and ``/history``
# endpoints already use ``stamp_turn_duration_on_last_ai``; the page
# endpoint was inlining the equivalent loop but stamping every AI row,
# which #4163 fixed for the other paths and missed here.
stamp_turn_duration_on_last_ai(data, run_durations)
return data

View File

@ -443,6 +443,37 @@ def test_thread_page_batch_hydrates_duration_for_old_runs():
assert response.json()["data"][0]["content"]["additional_kwargs"]["turn_duration"] == 7
def test_thread_page_stamps_turn_duration_on_last_ai_message_only():
# #4152 regression: a run with multiple AI messages (multi-step turn) must
# carry turn_duration on the LAST AI message only, not on every one.
store = MemoryRunEventStore()
async def seed():
await _put_message(store, "run-multi", "ai", "first-answer")
await _put_message(store, "run-multi", "ai", "second-answer")
asyncio.run(seed())
record = RunRecord(
run_id="run-multi",
thread_id="thread-1",
assistant_id=None,
status="success",
on_disconnect="cancel",
created_at="2026-01-01T00:00:00Z",
updated_at="2026-01-01T00:00:07Z",
)
app = _make_app(store, records={"run-multi": record})
with TestClient(app) as client:
response = client.get("/api/threads/thread-1/messages/page")
data = response.json()["data"]
# Messages are returned newest-first; the last AI message is data[0].
first_row = next(row for row in data if row["content"]["id"] == "first-answer")
second_row = next(row for row in data if row["content"]["id"] == "second-answer")
assert second_row["content"]["additional_kwargs"]["turn_duration"] == 7
assert "turn_duration" not in first_row["content"].get("additional_kwargs", {})
def test_thread_page_preserves_tool_and_subagent_wrapper_metadata():
store = MemoryRunEventStore()
asyncio.run(