From ad2732152a1321c7843758fe10b1c0a293c8e8bb Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Thu, 18 Dec 2025 20:23:20 -0800 Subject: [PATCH 1/4] attachment upload method --- chatkit/types.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/chatkit/types.py b/chatkit/types.py index a0ceeea..48d7961 100644 --- a/chatkit/types.py +++ b/chatkit/types.py @@ -726,16 +726,34 @@ class ToolChoice(BaseModel): id: str +class AttachmentUploadDescriptor(BaseModel): + """Two-phase upload instructions.""" + + url: AnyUrl + method: Literal["put", "post"] + """The HTTP method to use when uploading the file for two-phase upload.""" + headers: dict[str, str] = Field(default_factory=dict) + """Optional headers to include in the upload request.""" + + class AttachmentBase(BaseModel): """Base metadata shared by all attachments.""" id: str name: str mime_type: str - upload_url: AnyUrl | None = None + upload_url: AnyUrl | None = Field( + default=None, + deprecated=True, + description="""This field is still supported but will + be removed in the next major version update. Please + use the `upload_descriptor` field instead.""", + ) + upload_descriptor: AttachmentUploadDescriptor | None = None """ - The URL to upload the file, used for two-phase upload. - Should be set to None after upload is complete or when using direct upload where uploading happens when creating the attachment object. + Two-phase upload instructions. + Should be set to None after upload is complete or when using direct upload + where uploading happens when creating the attachment object. """ From 7c90a733c0125b3bf9c7b8a48283b028e510ed38 Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Fri, 19 Dec 2025 09:45:41 -0800 Subject: [PATCH 2/4] remove deprecated field; make ChatKitServer handle attachment saving for 2-phase upload --- chatkit/server.py | 1 + chatkit/types.py | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/chatkit/server.py b/chatkit/server.py index 01372cb..65645f6 100644 --- a/chatkit/server.py +++ b/chatkit/server.py @@ -435,6 +435,7 @@ async def _process_non_streaming( attachment = await attachment_store.create_attachment( request.params, context ) + await self.store.save_attachment(attachment, context=context) return self._serialize(attachment) case AttachmentsDeleteReq(): attachment_store = self._get_attachment_store() diff --git a/chatkit/types.py b/chatkit/types.py index 48d7961..8263cb3 100644 --- a/chatkit/types.py +++ b/chatkit/types.py @@ -742,13 +742,6 @@ class AttachmentBase(BaseModel): id: str name: str mime_type: str - upload_url: AnyUrl | None = Field( - default=None, - deprecated=True, - description="""This field is still supported but will - be removed in the next major version update. Please - use the `upload_descriptor` field instead.""", - ) upload_descriptor: AttachmentUploadDescriptor | None = None """ Two-phase upload instructions. From 212c0c3c90857f44e8535590d9477537e6641d0f Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Mon, 5 Jan 2026 11:29:45 -0800 Subject: [PATCH 3/4] Update tests --- tests/test_chatkit_server.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_chatkit_server.py b/tests/test_chatkit_server.py index 41be193..10b7131 100644 --- a/tests/test_chatkit_server.py +++ b/tests/test_chatkit_server.py @@ -31,6 +31,7 @@ AttachmentDeleteParams, AttachmentsCreateReq, AttachmentsDeleteReq, + AttachmentUploadDescriptor, ClientToolCallItem, FeedbackKind, FileAttachment, @@ -107,14 +108,22 @@ async def create_attachment( mime_type=input.mime_type, name=input.name, preview_url=AnyUrl(f"https://example.com/{id}/preview"), - upload_url=AnyUrl(f"https://example.com/{id}/upload"), + upload_descriptor=AttachmentUploadDescriptor( + url=AnyUrl(f"https://example.com/{id}/upload"), + method="put", + headers={"X-My-Header": "my-value"}, + ), ) else: attachment = FileAttachment( id=id, mime_type=input.mime_type, name=input.name, - upload_url=AnyUrl(f"https://example.com/{id}/upload"), + upload_descriptor=AttachmentUploadDescriptor( + url=AnyUrl(f"https://example.com/{id}/upload"), + method="put", + headers={"X-My-Header": "my-value"}, + ), ) self.files[attachment.id] = attachment return attachment @@ -679,6 +688,7 @@ async def responder( assert events[1].type == "thread.item.done" assert events[1].item.type == "assistant_message" + async def test_respond_with_tool_status(): async def responder( thread: ThreadMetadata, input: UserMessageItem | None, context: Any @@ -1019,9 +1029,12 @@ async def test_create_file(): assert attachment.mime_type == file_content_type assert attachment.name == file_name assert attachment.type == "file" - assert attachment.upload_url == AnyUrl( + assert attachment.upload_descriptor is not None + assert attachment.upload_descriptor.url == AnyUrl( f"https://example.com/{attachment.id}/upload" ) + assert attachment.upload_descriptor.method == "put" + assert attachment.upload_descriptor.headers == {"X-My-Header": "my-value"} assert attachment.id in store.files @@ -1049,9 +1062,12 @@ async def test_create_image_file(): assert attachment.preview_url == AnyUrl( f"https://example.com/{attachment.id}/preview" ) - assert attachment.upload_url == AnyUrl( + assert attachment.upload_descriptor is not None + assert attachment.upload_descriptor.url == AnyUrl( f"https://example.com/{attachment.id}/upload" ) + assert attachment.upload_descriptor.method == "put" + assert attachment.upload_descriptor.headers == {"X-My-Header": "my-value"} assert attachment.id in store.files From 24b53f988d27487e8a5be6f81d48aa85c551766a Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Mon, 5 Jan 2026 13:32:09 -0800 Subject: [PATCH 4/4] use call caps for http method --- chatkit/types.py | 2 +- tests/test_chatkit_server.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chatkit/types.py b/chatkit/types.py index 8263cb3..cbb5900 100644 --- a/chatkit/types.py +++ b/chatkit/types.py @@ -730,7 +730,7 @@ class AttachmentUploadDescriptor(BaseModel): """Two-phase upload instructions.""" url: AnyUrl - method: Literal["put", "post"] + method: Literal["POST", "PUT"] """The HTTP method to use when uploading the file for two-phase upload.""" headers: dict[str, str] = Field(default_factory=dict) """Optional headers to include in the upload request.""" diff --git a/tests/test_chatkit_server.py b/tests/test_chatkit_server.py index 10b7131..ff7177c 100644 --- a/tests/test_chatkit_server.py +++ b/tests/test_chatkit_server.py @@ -110,7 +110,7 @@ async def create_attachment( preview_url=AnyUrl(f"https://example.com/{id}/preview"), upload_descriptor=AttachmentUploadDescriptor( url=AnyUrl(f"https://example.com/{id}/upload"), - method="put", + method="PUT", headers={"X-My-Header": "my-value"}, ), ) @@ -121,7 +121,7 @@ async def create_attachment( name=input.name, upload_descriptor=AttachmentUploadDescriptor( url=AnyUrl(f"https://example.com/{id}/upload"), - method="put", + method="PUT", headers={"X-My-Header": "my-value"}, ), ) @@ -1033,7 +1033,7 @@ async def test_create_file(): assert attachment.upload_descriptor.url == AnyUrl( f"https://example.com/{attachment.id}/upload" ) - assert attachment.upload_descriptor.method == "put" + assert attachment.upload_descriptor.method == "PUT" assert attachment.upload_descriptor.headers == {"X-My-Header": "my-value"} assert attachment.id in store.files @@ -1066,7 +1066,7 @@ async def test_create_image_file(): assert attachment.upload_descriptor.url == AnyUrl( f"https://example.com/{attachment.id}/upload" ) - assert attachment.upload_descriptor.method == "put" + assert attachment.upload_descriptor.method == "PUT" assert attachment.upload_descriptor.headers == {"X-My-Header": "my-value"} assert attachment.id in store.files