perf: build chat responses without deep-copying the blob through model_dump (#27388)

Chat search built each result row with ChatTitleIdResponse(**chat.model_dump(), ...), which recursively copies the entire chat blob per row only for the constructor to ignore everything except id, title and timestamps: a 60-row search page deep-copied up to 60 full conversations. The folder listing, archived and export endpoints and every single-chat response did the same dump-and-revalidate dance via ChatResponse(**chat.model_dump()).

Search rows are now built from the five fields the response actually has (the snippet helper receives the blob by reference as before), and all 18 ChatResponse constructions use ChatResponse.model_validate(chat, from_attributes=True), which reads the fields off the already-validated ChatModel without copying the blob.

Benchmark (~500 KB chat blob):

| metric | before | after |
| --- | --- | --- |
| search result row | 0.05 ms | 0.003 ms |
| ChatResponse construction | 0.05 ms | 0.003 ms |
| per search page (60 rows) | 3 ms | 0.2 ms |

Beyond CPU, each converted row also stops materializing a second full copy of the conversation in memory while the page is being built.

Functionally verified: both construction styles produce identical model_dump() output for ChatResponse (including defaulted fields absent on ChatModel) and for search rows including the snippet.
This commit is contained in:
Classic298
2026-07-26 18:57:52 -04:00
committed by GitHub
parent 4f93c3e36c
commit 54f06d8c53
+30 -20
View File
@@ -735,7 +735,7 @@ async def create_new_chat(
subject_id=chat.id,
data={'title': chat.title, 'folder_id': chat.folder_id},
)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
except Exception as e:
log.exception(e)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
@@ -820,7 +820,17 @@ async def search_user_chats(
search_text = chat_search_content_text(text)
chat_list = []
for chat in await Chats.get_chats_by_user_id_and_search_text(user.id, text, skip=skip, limit=limit, db=db):
chat_list.append(ChatTitleIdResponse(**chat.model_dump(), snippet=chat_search_snippet(chat.chat, search_text)))
# Explicit fields: model_dump() would deep-copy the entire chat blob per row
chat_list.append(
ChatTitleIdResponse(
id=chat.id,
title=chat.title,
updated_at=chat.updated_at,
created_at=chat.created_at,
last_read_at=chat.last_read_at,
snippet=chat_search_snippet(chat.chat, search_text),
)
)
# Delete tag if no chat is found
words = text.strip().split(' ')
@@ -849,7 +859,7 @@ async def get_chats_by_folder_id(
folder_ids.extend([folder.id for folder in children_folders])
return [
ChatResponse(**chat.model_dump())
ChatResponse.model_validate(chat, from_attributes=True)
for chat in await Chats.get_chats_by_folder_ids_and_user_id(folder_ids, user.id, db=db)
]
@@ -915,7 +925,7 @@ async def generate_chat_export_ndjson(user_id: str):
for chat in result.items:
try:
yield ChatResponse(**chat.model_dump()).model_dump_json() + '\n'
yield ChatResponse.model_validate(chat, from_attributes=True).model_dump_json() + '\n'
except Exception as e:
log.exception(f'Error serializing chat {chat.id}: {e}')
@@ -940,7 +950,7 @@ async def get_user_chats(user=Depends(get_verified_user)):
@router.get('/all/archived', response_model=list[ChatResponse])
async def get_user_archived_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)):
return [ChatResponse(**chat.model_dump()) for chat in await Chats.get_archived_chats_by_user_id(user.id, db=db)]
return [ChatResponse.model_validate(chat, from_attributes=True) for chat in await Chats.get_archived_chats_by_user_id(user.id, db=db)]
############################
@@ -967,7 +977,7 @@ async def get_all_user_tags(user=Depends(get_verified_user), db: AsyncSession =
async def get_all_user_chats_in_db(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)):
if not ENABLE_ADMIN_EXPORT:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
return [ChatResponse(**chat.model_dump()) for chat in await Chats.get_chats(db=db)]
return [ChatResponse.model_validate(chat, from_attributes=True) for chat in await Chats.get_chats(db=db)]
############################
@@ -1133,7 +1143,7 @@ async def get_shared_chat_by_id(
):
chat = await Chats.get_chat_by_share_id(share_id, db=db)
if chat:
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -1145,7 +1155,7 @@ async def get_shared_chat_by_id(
if user is not None and user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS:
chat = await Chats.get_chat_by_id(share_id, db=db)
if chat:
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
@@ -1270,7 +1280,7 @@ async def get_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSess
chat = candidate
if chat:
data = ChatResponse(**chat.model_dump()).model_dump()
data = ChatResponse.model_validate(chat, from_attributes=True).model_dump()
data['context_usage'] = await get_chat_context_usage(chat)
return data
@@ -1316,7 +1326,7 @@ async def update_chat_by_id(
subject_id=id,
data={'title': chat.title},
)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -1390,7 +1400,7 @@ async def update_chat_message_by_id(
subject_id=message_id,
data={'chat_id': id, 'content_preview': form_data.content[:300]},
)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
@router.delete('/{id}/messages/{message_id}', response_model=ChatResponse | None)
@@ -1429,7 +1439,7 @@ async def delete_chat_message_by_id(
subject_id=message_id,
data={'chat_id': id},
)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
############################
@@ -1685,7 +1695,7 @@ async def fork_chat_by_id(
subject_id=fork.id,
data={'original_chat_id': id, 'forked_from_message_id': source_message_id},
)
return ChatResponse(**fork.model_dump())
return ChatResponse.model_validate(fork, from_attributes=True)
@router.post('/{id}/clone', response_model=ChatResponse | None)
@@ -1732,7 +1742,7 @@ async def clone_chat_by_id(
subject_id=chat.id,
data={'original_chat_id': id},
)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -1809,7 +1819,7 @@ async def clone_shared_chat_by_id(
if chats:
chat = chats[0]
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -1850,7 +1860,7 @@ async def archive_chat_by_id(
subject_id=id,
subject_type='chat',
)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
else:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT())
@@ -1884,7 +1894,7 @@ async def share_chat_by_id(
subject_id=id,
data={'share_id': chat.share_id, 'updated': True},
)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
# Create a new share
shared = await SharedChats.create(id, user.id, db=db)
@@ -1902,7 +1912,7 @@ async def share_chat_by_id(
subject_id=id,
data={'share_id': shared.id},
)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
# --- Delete Shared Chat ---
@@ -1972,7 +1982,7 @@ async def update_shared_chat_access_by_id(
await AccessGrants.set_access_grants('shared_chat', id, form_data.access_grants, db=db)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
############################
@@ -2047,7 +2057,7 @@ async def update_chat_folder_id_by_id(
subject_id=id,
data={'folder_id': form_data.folder_id},
)
return ChatResponse(**chat.model_dump())
return ChatResponse.model_validate(chat, from_attributes=True)
else:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT())