From b1a8c2893e80249a08a9406dafec92d3930b240d Mon Sep 17 00:00:00 2001 From: anshal21 Date: Sat, 25 Oct 2025 22:46:25 +0530 Subject: [PATCH] Fix: Preserve metadata in _to_thread_response method Previously, the _to_thread_response method in ChatKitServer was not preserving the metadata field when converting ThreadMetadata/Thread objects to Thread response objects. This caused metadata to be lost in ThreadUpdatedEvent and other responses. Changes: - Added metadata field to Thread object creation in _to_thread_response - Added comprehensive test to verify metadata preservation - Test verifies metadata is correctly preserved in ThreadUpdatedEvent - Test also ensures metadata is properly saved to the store --- chatkit/server.py | 1 + tests/test_chatkit_server.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/chatkit/server.py b/chatkit/server.py index fe29c66..2fec241 100644 --- a/chatkit/server.py +++ b/chatkit/server.py @@ -726,4 +726,5 @@ def is_hidden(item: ThreadItem) -> bool: created_at=thread.created_at, items=items, status=thread.status, + metadata=thread.metadata, ) diff --git a/tests/test_chatkit_server.py b/tests/test_chatkit_server.py index 44748ef..0aebfa3 100644 --- a/tests/test_chatkit_server.py +++ b/tests/test_chatkit_server.py @@ -362,6 +362,62 @@ async def responder( assert events[-1].type == "thread.updated" +async def test_metadata_preserved_in_thread_response(): + """Test that metadata is preserved when _to_thread_response converts ThreadMetadata/Thread to Thread.""" + + async def responder( + thread: ThreadMetadata, input: UserMessageItem | None, context: Any + ) -> AsyncIterator[ThreadStreamEvent]: + # Set metadata on the thread + thread.metadata["test_key"] = "test_value" + thread.metadata["another_key"] = {"nested": "data"} + return + yield + + with make_server(responder) as server: + events = await server.process_streaming( + ThreadsCreateReq( + params=ThreadCreateParams( + input=UserMessageInput( + content=[UserMessageTextContent(text="Hello, world!")], + attachments=[], + inference_options=InferenceOptions(), + ) + ) + ) + ) + + # The ThreadCreatedEvent won't have metadata because it's sent before responder runs + # This is expected behavior + thread_created_event = next( + event for event in events if event.type == "thread.created" + ) + assert thread_created_event.thread.metadata == {}, ( + "ThreadCreatedEvent should have empty metadata initially" + ) + + # The ThreadUpdatedEvent should have the metadata set by the responder + # This tests that _to_thread_response preserves metadata + thread_updated_event = next( + event for event in events if event.type == "thread.updated" + ) + assert thread_updated_event.thread.metadata == { + "test_key": "test_value", + "another_key": {"nested": "data"}, + }, ( + f"Metadata not preserved in ThreadUpdatedEvent: {thread_updated_event.thread.metadata}" + ) + + # Also verify metadata is saved in the store + stored_thread = await server.store.load_thread( + thread_created_event.thread.id, DEFAULT_CONTEXT + ) + assert stored_thread.metadata == { + "test_key": "test_value", + "another_key": {"nested": "data"}, + }, f"Metadata not saved in store: {stored_thread.metadata}" + + async def test_saves_thread_locked_fields(): async def responder( thread: ThreadMetadata, input: UserMessageItem | None, context: Any