From 0f45b2c285145c18d1a570f52c63d8b938c7badf Mon Sep 17 00:00:00 2001 From: Nidhi Rajani <97787490+nidhi1603@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:13:37 -0400 Subject: [PATCH] feat(openai): support `apply_patch` built-in tool (#37157) [Docs](https://github.com/langchain-ai/docs/pull/4370) Fixes #37031 Adds support for OpenAI Responses API `apply_patch` built-in tool. This PR: - Adds `apply_patch` to the OpenAI well-known tools list so `bind_tools([{"type": "apply_patch"}])` works. - Preserves `apply_patch_call` and `apply_patch_call_output` items when converting OpenAI Responses API outputs into LangChain `AIMessage.content`. - Preserves the same item types in streaming `AIMessageChunk` conversion. - Supports round-trip input conversion for `apply_patch_call` and `apply_patch_call_output`. - Adds unit tests for core tool passthrough, non-streaming conversion, streaming conversion, and round-trip input conversion. ## Testing - `cd libs/core && uv run --group test pytest tests/unit_tests/utils/test_function_calling.py -k "apply_patch" -vv` - `cd libs/partners/openai && uv run --group test pytest tests/unit_tests/chat_models/test_base.py -k "apply_patch" -vv` - `cd libs/core && uv run --all-groups ruff check langchain_core/utils/function_calling.py tests/unit_tests/utils/test_function_calling.py` - `cd libs/partners/openai && uv run --all-groups ruff check langchain_openai/chat_models/base.py tests/unit_tests/chat_models/test_base.py` --------- Co-authored-by: Mason Daugherty Co-authored-by: Mason Daugherty --- .../langchain_core/utils/function_calling.py | 3 +- .../unit_tests/utils/test_function_calling.py | 9 + libs/langchain_v1/uv.lock | 2 +- .../langchain_openai/chat_models/_compat.py | 6 +- .../langchain_openai/chat_models/base.py | 13 +- .../tests/cassettes/test_apply_patch.yaml.gz | Bin 0 -> 5096 bytes .../chat_models/test_responses_api.py | 56 ++++ .../tests/unit_tests/chat_models/test_base.py | 244 +++++++++++++++++- libs/partners/openai/uv.lock | 86 +++++- 9 files changed, 401 insertions(+), 18 deletions(-) create mode 100644 libs/partners/openai/tests/cassettes/test_apply_patch.yaml.gz diff --git a/libs/core/langchain_core/utils/function_calling.py b/libs/core/langchain_core/utils/function_calling.py index 0fefd98b0eb..c66df944371 100644 --- a/libs/core/langchain_core/utils/function_calling.py +++ b/libs/core/langchain_core/utils/function_calling.py @@ -508,6 +508,7 @@ _WellKnownOpenAITools = ( "web_search_preview", "web_search", "tool_search", + "apply_patch", "namespace", ) @@ -546,7 +547,7 @@ def convert_to_openai_tool( Return OpenAI Responses API-style tools unchanged. This includes any dict with `"type"` in `"file_search"`, `"function"`, - `"computer_use_preview"`, `"web_search_preview"`. + `"computer_use_preview"`, `"web_search_preview"`, `"apply_patch"`. !!! warning "Behavior changed in `langchain-core` 0.3.63" diff --git a/libs/core/tests/unit_tests/utils/test_function_calling.py b/libs/core/tests/unit_tests/utils/test_function_calling.py index f3aa224029b..d2906f59e35 100644 --- a/libs/core/tests/unit_tests/utils/test_function_calling.py +++ b/libs/core/tests/unit_tests/utils/test_function_calling.py @@ -1245,6 +1245,15 @@ def test_convert_to_openai_function_json_schema_missing_title_includes_schema() convert_to_openai_function(schema_without_title) +def test_convert_to_openai_tool_apply_patch_passthrough() -> None: + """Test apply_patch is passed through as an OpenAI built-in tool.""" + tool = {"type": "apply_patch"} + + result = convert_to_openai_tool(tool) + + assert result == tool + + def test_convert_to_openai_tool_computer_passthrough() -> None: """Test that the 'computer' tool type is passed through unchanged.""" computer_tool = { diff --git a/libs/langchain_v1/uv.lock b/libs/langchain_v1/uv.lock index 8218383723a..a92725dea79 100644 --- a/libs/langchain_v1/uv.lock +++ b/libs/langchain_v1/uv.lock @@ -2225,7 +2225,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.4.1" +version = "1.4.2" source = { editable = "../core" } dependencies = [ { name = "jsonpatch" }, diff --git a/libs/partners/openai/langchain_openai/chat_models/_compat.py b/libs/partners/openai/langchain_openai/chat_models/_compat.py index 1a7506ccd18..cc04a9235f9 100644 --- a/libs/partners/openai/langchain_openai/chat_models/_compat.py +++ b/libs/partners/openai/langchain_openai/chat_models/_compat.py @@ -105,6 +105,8 @@ def _convert_to_v03_ai_message( "image_generation_call", "tool_search_call", "tool_search_output", + "apply_patch_call", + "apply_patch_call_output", ): # Store built-in tool calls in additional_kwargs if "tool_outputs" not in message.additional_kwargs: @@ -285,11 +287,11 @@ def _consolidate_calls(items: Iterable[dict[str, Any]]) -> Iterator[dict[str, An try: nxt = next(items) # look-ahead one element - except StopIteration: # no “result” - just yield the call back + except StopIteration: # no "result" - just yield the call back yield current break - # If this really is the matching “result” - collapse + # If this really is the matching "result" - collapse if nxt.get("type") == "server_tool_result" and nxt.get( "tool_call_id" ) == current.get("id"): diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 8e2473fe221..3ba2ae24a05 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -192,6 +192,7 @@ WellKnownTools = ( "mcp", "image_generation", "tool_search", + "apply_patch", ) @@ -4490,6 +4491,8 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list: "mcp_approval_request", "tool_search_call", "tool_search_output", + "apply_patch_call", + "apply_patch_call_output", ): input_.append(_pop_index_and_sub_index(block)) elif block_type == "image_generation_call": @@ -4535,7 +4538,11 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list: elif msg["role"] in ("user", "system", "developer"): if isinstance(msg["content"], list): new_blocks = [] - non_message_item_types = ("mcp_approval_response", "tool_search_output") + non_message_item_types = ( + "mcp_approval_response", + "tool_search_output", + "apply_patch_call_output", + ) for block in msg["content"]: if block["type"] in ("text", "image_url", "file"): new_blocks.append( @@ -4704,6 +4711,8 @@ def _construct_lc_result_from_responses_api( "image_generation_call", "tool_search_call", "tool_search_output", + "apply_patch_call", + "apply_patch_call_output", ): content_blocks.append(output.model_dump(exclude_none=True, mode="json")) @@ -4958,6 +4967,8 @@ def _convert_responses_chunk_to_generation_chunk( "image_generation_call", "tool_search_call", "tool_search_output", + "apply_patch_call", + "apply_patch_call_output", ): _advance(chunk.output_index) tool_output = chunk.item.model_dump(exclude_none=True, mode="json") diff --git a/libs/partners/openai/tests/cassettes/test_apply_patch.yaml.gz b/libs/partners/openai/tests/cassettes/test_apply_patch.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..5be20dfcbfc6c5c3f99eb234874238a9190a6903 GIT binary patch literal 5096 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 None: assert tool_output["type"] == "web_search_call" +@pytest.mark.default_cassette("test_apply_patch.yaml.gz") +@pytest.mark.vcr +def test_apply_patch() -> None: + """Test the apply_patch built-in tool end-to-end. + + apply_patch is a client-executed tool: the model proposes a file operation + via an `apply_patch_call` block, the client applies it, and the result is + returned as an ``apply_patch_call_output`` block. Requires a model that + supports the tool. + """ + prompt = "Create a new file named hello.txt containing the line: hello world" + llm = ChatOpenAI(model="gpt-5.1", output_version="responses/v1") + tool = {"type": "apply_patch"} + + # Non-streaming: the model should emit an apply_patch_call block. + response = llm.invoke(prompt, tools=[tool]) + assert isinstance(response, AIMessage) + calls = [ + block + for block in response.content + if isinstance(block, dict) and block["type"] == "apply_patch_call" + ] + assert len(calls) == 1 + call = calls[0] + assert call["call_id"] + assert call["operation"]["type"] in ("create_file", "update_file", "delete_file") + + # Streaming: the apply_patch_call block survives chunk aggregation. + aggregated: BaseMessageChunk | None = None + for chunk in llm.stream(prompt, tools=[tool]): + assert isinstance(chunk, AIMessageChunk) + aggregated = chunk if aggregated is None else aggregated + chunk + assert isinstance(aggregated, AIMessageChunk) + assert any( + isinstance(block, dict) and block["type"] == "apply_patch_call" + for block in aggregated.content + ) + + # Round-trip: return an apply_patch_call_output and continue the conversation. + output_message = HumanMessage( + content=[ + { + "type": "apply_patch_call_output", + "call_id": call["call_id"], + "status": "completed", + "output": f"Created {call['operation']['path']}", + } + ] + ) + follow_up = llm.invoke( + [HumanMessage(prompt), response, output_message], + tools=[tool], + ) + assert isinstance(follow_up, AIMessage) + + @pytest.mark.default_cassette("test_function_calling.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "responses/v1", "v1"]) diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index 64d6237d60c..011729b2b4a 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -35,8 +35,14 @@ from langchain_core.runnables import RunnableLambda from langchain_core.runnables.base import RunnableBinding, RunnableSequence from langchain_core.tracers.base import BaseTracer from langchain_core.tracers.schemas import Run -from openai.types.responses import ResponseOutputMessage, ResponseReasoningItem +from openai.types.responses import ( + ResponseApplyPatchToolCall, + ResponseApplyPatchToolCallOutput, + ResponseOutputMessage, + ResponseReasoningItem, +) from openai.types.responses.response import IncompleteDetails, Response +from openai.types.responses.response_apply_patch_tool_call import OperationCreateFile from openai.types.responses.response_error import ResponseError from openai.types.responses.response_file_search_tool_call import ( ResponseFileSearchToolCall, @@ -71,6 +77,7 @@ from langchain_openai.chat_models.base import ( _construct_responses_api_input, _convert_dict_to_message, _convert_message_to_dict, + _convert_responses_chunk_to_generation_chunk, _convert_to_openai_response_format, _create_usage_metadata, _create_usage_metadata_responses, @@ -1924,6 +1931,178 @@ def test__construct_lc_result_from_responses_api_no_usage_metadata() -> None: assert cast(AIMessage, result.generations[0].message).usage_metadata is None +def test__construct_lc_result_from_responses_api_apply_patch_response() -> None: + """Test a response with apply_patch output.""" + response = Response( + id="resp_123", + created_at=1234567890, + model="gpt-4o", + object="response", + parallel_tool_calls=True, + tools=[], + tool_choice="auto", + output=[ + ResponseApplyPatchToolCall( + id="apply_patch_123", + type="apply_patch_call", + call_id="call_123", + operation=OperationCreateFile( + type="create_file", + path="hello.txt", + diff="+hello\n", + ), + status="completed", + ) + ], + ) + + result = _construct_lc_result_from_responses_api(response) + + message = cast(AIMessage, result.generations[0].message) + assert message.content == [ + { + "type": "apply_patch_call", + "id": "apply_patch_123", + "call_id": "call_123", + "operation": { + "type": "create_file", + "path": "hello.txt", + "diff": "+hello\n", + }, + "status": "completed", + } + ] + assert message.tool_calls == [] + assert message.invalid_tool_calls == [] + + +def test__construct_lc_result_from_responses_api_apply_patch_call_output() -> None: + """Test a response with apply_patch_call_output output.""" + response = Response( + id="resp_123", + created_at=1234567890, + model="gpt-4o", + object="response", + parallel_tool_calls=True, + tools=[], + tool_choice="auto", + output=[ + ResponseApplyPatchToolCallOutput( + id="apply_patch_output_123", + type="apply_patch_call_output", + call_id="call_123", + status="completed", + output="Created hello.txt", + ) + ], + ) + + result = _construct_lc_result_from_responses_api(response) + + message = result.generations[0].message + assert message.content == [ + { + "type": "apply_patch_call_output", + "id": "apply_patch_output_123", + "call_id": "call_123", + "status": "completed", + "output": "Created hello.txt", + } + ] + + +def test__construct_responses_api_input_apply_patch_round_trip() -> None: + """Test apply_patch content blocks are preserved when sent back as input.""" + messages = [ + AIMessage( + content=[ + { + "type": "apply_patch_call", + "call_id": "call_123", + "operation": { + "type": "create_file", + "path": "hello.txt", + "diff": "+hello\\n", + }, + "status": "completed", + } + ] + ), + HumanMessage( + content=[ + { + "type": "apply_patch_call_output", + "call_id": "call_123", + "status": "completed", + "output": "Created hello.txt", + } + ] + ), + ] + + result = _construct_responses_api_input(messages) + + assert result == [ + { + "type": "apply_patch_call", + "call_id": "call_123", + "operation": { + "type": "create_file", + "path": "hello.txt", + "diff": "+hello\\n", + }, + "status": "completed", + }, + { + "type": "apply_patch_call_output", + "call_id": "call_123", + "status": "completed", + "output": "Created hello.txt", + }, + ] + + +def test__convert_responses_chunk_to_generation_chunk_apply_patch_response() -> None: + """Test streamed apply_patch output item is preserved in message chunks.""" + chunk = MagicMock() + chunk.type = "response.output_item.done" + chunk.output_index = 0 + chunk.item = ResponseApplyPatchToolCall( + id="apply_patch_123", + type="apply_patch_call", + call_id="call_123", + operation=OperationCreateFile( + type="create_file", + path="hello.txt", + diff="+hello\n", + ), + status="completed", + ) + + _, _, _, generation_chunk = _convert_responses_chunk_to_generation_chunk( + chunk, + current_index=-1, + current_output_index=-1, + current_sub_index=-1, + ) + + assert generation_chunk is not None + assert generation_chunk.message.content == [ + { + "type": "apply_patch_call", + "id": "apply_patch_123", + "call_id": "call_123", + "operation": { + "type": "create_file", + "path": "hello.txt", + "diff": "+hello\n", + }, + "status": "completed", + "index": 0, + } + ] + + def test__construct_lc_result_from_responses_api_web_search_response() -> None: """Test a response with web search output.""" from openai.types.responses.response_function_web_search import ( @@ -2747,6 +2926,57 @@ def test_compat_responses_v03() -> None: assert message_v03_output is not message_v03 +def test_compat_responses_v03_apply_patch_tool_outputs() -> None: + message = AIMessage( + content=[ + {"type": "text", "text": "Done.", "id": "msg_123"}, + { + "type": "apply_patch_call", + "id": "apply_patch_123", + "call_id": "call_123", + "operation": { + "type": "create_file", + "path": "hello.txt", + "diff": "+hello\n", + }, + "status": "completed", + }, + { + "type": "apply_patch_call_output", + "id": "apply_patch_output_123", + "call_id": "call_123", + "status": "completed", + "output": "Created hello.txt", + }, + ], + id="resp_123", + ) + + message_v03_output = _convert_to_v03_ai_message(message) + + assert message_v03_output.content == [{"type": "text", "text": "Done."}] + assert message_v03_output.additional_kwargs["tool_outputs"] == [ + { + "type": "apply_patch_call", + "id": "apply_patch_123", + "call_id": "call_123", + "operation": { + "type": "create_file", + "path": "hello.txt", + "diff": "+hello\n", + }, + "status": "completed", + }, + { + "type": "apply_patch_call_output", + "id": "apply_patch_output_123", + "call_id": "call_123", + "status": "completed", + "output": "Created hello.txt", + }, + ] + + @pytest.mark.parametrize( ("message_v1", "expected"), [ @@ -3695,6 +3925,18 @@ def test_get_request_payload_responses_api_input_file_blocks_passthrough() -> No ] +def test_apply_patch_passthrough() -> None: + """Test that apply_patch dict is passed through as a built-in tool.""" + llm = ChatOpenAI(model="gpt-4o", api_key=SecretStr("test-api-key")) + bound = llm.bind_tools([{"type": "apply_patch"}]) + payload = bound._get_request_payload( # type: ignore[attr-defined] + "test", + **bound.kwargs, # type: ignore[attr-defined] + ) + assert {"type": "apply_patch"} in payload["tools"] + assert "input" in payload + + def test_tool_search_passthrough() -> None: """Test that tool_search dict is passed through as a built-in tool.""" llm = ChatOpenAI(model="gpt-4o") diff --git a/libs/partners/openai/uv.lock b/libs/partners/openai/uv.lock index a7ce6505e6b..fc84b054e44 100644 --- a/libs/partners/openai/uv.lock +++ b/libs/partners/openai/uv.lock @@ -559,7 +559,7 @@ wheels = [ [[package]] name = "langchain" -version = "1.3.1" +version = "1.3.4" source = { editable = "../../langchain_v1" } dependencies = [ { name = "langchain-core" }, @@ -587,7 +587,7 @@ requires-dist = [ { name = "langchain-perplexity", marker = "extra == 'perplexity'" }, { name = "langchain-together", marker = "extra == 'together'" }, { name = "langchain-xai", marker = "extra == 'xai'" }, - { name = "langgraph", specifier = ">=1.2.0,<1.3.0" }, + { name = "langgraph", specifier = ">=1.2.4,<1.3.0" }, { name = "pydantic", specifier = ">=2.7.4,<3.0.0" }, ] provides-extras = ["community", "anthropic", "openai", "azure-ai", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "baseten", "deepseek", "xai", "perplexity"] @@ -624,7 +624,7 @@ typing = [ [[package]] name = "langchain-core" -version = "1.4.0" +version = "1.4.2" source = { editable = "../../core" } dependencies = [ { name = "jsonpatch" }, @@ -773,14 +773,14 @@ typing = [ [[package]] name = "langchain-protocol" -version = "0.0.14" +version = "0.0.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/bf/efb5e2ed832e4d6d45590e25a9e5191986b291b543bc6a807b48bee070b0/langchain_protocol-0.0.14.tar.gz", hash = "sha256:bc1e8553122e6ede310280462d5813023a172ff2785ccbbdec54d43f3a15e5f2", size = 5862, upload-time = "2026-04-29T16:40:18.657Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/e7/8300ba22d968653051fd06e3117d783872dddf3dcebdd6b1d386836eb43c/langchain_protocol-0.0.16.tar.gz", hash = "sha256:806c7cdd951b1c4f692fa40fce60821ff0f221d4360e27673ddf2c2b99c2b7ff", size = 5969, upload-time = "2026-05-28T23:05:11.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/e9/06c47ecb2aff08f83dfa30058da3bf86be64862c19569043ed5331bbeecd/langchain_protocol-0.0.14-py3-none-any.whl", hash = "sha256:ffc35089779bd8ca217015180cef5e660fc3b074efdaa0f2e95df73583f1a047", size = 6984, upload-time = "2026-04-29T16:40:17.841Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9c/06dfcc88d02a6364e8d864c421ddd3736305cb0a6c853f75c302c80fe17c/langchain_protocol-0.0.16-py3-none-any.whl", hash = "sha256:3658c142c5d0fb3a023a4be442ce4c15c6d626aab6135eb79a76dc64ad19c3c3", size = 7037, upload-time = "2026-05-28T23:05:10.163Z" }, ] [[package]] @@ -830,7 +830,7 @@ typing = [ [[package]] name = "langgraph" -version = "1.2.0" +version = "1.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -840,9 +840,9 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/61/d5d25e783035aa307d289b37e082258a6061c0fb4caa4a284f3bf1e87169/langgraph-1.2.0.tar.gz", hash = "sha256:4a9baaf62afc5d5f63144a50095140a34b9aa9b7cea695d25326d564775348e7", size = 690248, upload-time = "2026-05-12T03:46:39.164Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/43/dac5a2621c1e57f8eb7f0703f6f6fe34a5caf62f8f0fb4d2bb395bb454ea/langgraph-1.2.4.tar.gz", hash = "sha256:5df076973a2d23efb13eceb279d1e5b46feebcbbeded0a86a2ef669abd9e4399", size = 720374, upload-time = "2026-06-02T17:07:37.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/e8/e3304ac0015c2bdb04ad9785e4ed65c788855ce7857ce6104dd2f5d322db/langgraph-1.2.0-py3-none-any.whl", hash = "sha256:03fd5895a8d4b70db1ff63ebc3bacead29dd20cd794a8b1a483e7ec9018f7a65", size = 234262, upload-time = "2026-05-12T03:46:37.971Z" }, + { url = "https://files.pythonhosted.org/packages/48/9e/31ca236104966d7bb14ea9e93cfd73350aea8c41008ddf057b65794ed10d/langgraph-1.2.4-py3-none-any.whl", hash = "sha256:ffe3e1e31dce28907640f82525858470f293506d2b272d07ea3b3ce97974b067", size = 245402, upload-time = "2026-06-02T17:07:35.977Z" }, ] [[package]] @@ -873,15 +873,18 @@ wheels = [ [[package]] name = "langgraph-sdk" -version = "0.3.3" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, { name = "orjson" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/0f/ed0634c222eed48a31ba48eab6881f94ad690d65e44fe7ca838240a260c1/langgraph_sdk-0.3.3.tar.gz", hash = "sha256:c34c3dce3b6848755eb61f0c94369d1ba04aceeb1b76015db1ea7362c544fb26", size = 130589, upload-time = "2026-01-13T00:30:43.894Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/be/4ad511bacfdd854afb12974f407cb30010dceb982dc20c55491867b34526/langgraph_sdk-0.3.3-py3-none-any.whl", hash = "sha256:a52ebaf09d91143e55378bb2d0b033ed98f57f48c9ad35c8f81493b88705fc7b", size = 67021, upload-time = "2026-01-13T00:30:42.264Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, ] [[package]] @@ -2271,6 +2274,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + [[package]] name = "wrapt" version = "1.17.3"