From 3f3c261a02f32cfee97c8aff03d6d3b79b2b3176 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:24:44 -0700 Subject: [PATCH 01/23] build(deps): bump langchain-openai to 1.7.2 and lift semantic-router's openai cap in the lock --- pyproject.toml | 4 +- uv.lock | 421 +++++++++++++++++++++++++------------------------ 2 files changed, 217 insertions(+), 208 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9076760153a..039d540a43d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -291,7 +291,7 @@ ci = [ "blockbuster==1.5.26", "beautifulsoup4==4.14.3", "pylint==4.0.5", - "langchain-openai==1.1.14", + "langchain-openai==1.7.2", "langgraph>=1.2.4,<1.3.0", "langgraph-prebuilt>=1.1.0,<1.3.0", "claude-agent-sdk==0.2.165", @@ -363,6 +363,8 @@ constraint-dependencies = [ override-dependencies = [ # a2a-sdk 1.x requires packaging>=24.0; lunary 1.4.x still caps at <24.0. "packaging>=24.0", + # semantic-router (through 1.1.0) caps openai at <3.0.0 but only imports it for encoders LiteLLM never builds. + "openai>=2.20.0,<4.0.0", "cryptography>=50.0.0,<51.0", "mcp>=2.3.0,<3", ] diff --git a/uv.lock b/uv.lock index 03403828225..301225a8277 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.10, <3.15" resolution-markers = [ "python_full_version >= '3.14'", @@ -10,7 +10,7 @@ resolution-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-10-18T01:19:57.352406047Z" exclude-newer-span = "P3D" [manifest] @@ -30,6 +30,7 @@ constraints = [ overrides = [ { name = "cryptography", specifier = ">=50.0.0,<51.0" }, { name = "mcp", specifier = ">=2.3.0,<3" }, + { name = "openai", specifier = ">=2.20.0,<4.0.0" }, { name = "packaging", specifier = ">=24.0" }, ] @@ -227,9 +228,9 @@ name = "aiologic" version = "0.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio" }, - { name = "typing-extensions" }, - { name = "wrapt" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/a7/809482759f40079f4c4328c7318bf569ae25d457f5017aad30a1b9aafedc/aiologic-0.17.0.tar.gz", hash = "sha256:65aa058e858c94cd208badb188e7f00b54dcabb3ba85b34f794db98074d108b9", size = 251625, upload-time = "2026-06-14T12:24:35.367Z" } wheels = [ @@ -521,14 +522,14 @@ name = "aurelio-sdk" version = "0.0.19" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiofiles" }, - { name = "aiohttp" }, - { name = "colorlog" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "tornado" }, + { name = "aiofiles", marker = "python_full_version < '3.14'" }, + { name = "aiohttp", marker = "python_full_version < '3.14'" }, + { name = "colorlog", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "python-dotenv", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "requests-toolbelt", marker = "python_full_version < '3.14'" }, + { name = "tornado", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/0e/c2e369ad173fb3d76448e46d10beb3dcc53388318933ddf8169a3f21a810/aurelio_sdk-0.0.19.tar.gz", hash = "sha256:14107e7440ff2efd0b4a08c52fb595e7680bd4bc973a0ddfb3b64157c6666b91", size = 15258, upload-time = "2025-03-24T14:37:32.203Z" } wheels = [ @@ -540,9 +541,9 @@ name = "aws-sdk-bedrock-runtime" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "smithy-aws-core", extra = ["eventstream", "json"] }, - { name = "smithy-core" }, - { name = "smithy-http", extra = ["aiohttp"] }, + { name = "smithy-aws-core", extra = ["eventstream", "json"], marker = "python_full_version >= '3.12'" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, + { name = "smithy-http", extra = ["aiohttp"], marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/b3/9c225cbfe9f17ea2e3d75a0fdd0b325ef79839b9c09a376bda63a7bf3bb3/aws_sdk_bedrock_runtime-0.11.0.tar.gz", hash = "sha256:f2c45d34625bf6a7b56375e29a53a16b376880bda771e4bbf7d84491622eb193", size = 173854, upload-time = "2026-08-24T21:17:16.304Z" } wheels = [ @@ -551,7 +552,7 @@ wheels = [ [package.optional-dependencies] awscrt = [ - { name = "smithy-http", extra = ["awscrt"] }, + { name = "smithy-http", extra = ["awscrt"], marker = "python_full_version >= '3.12'" }, ] [[package]] @@ -1308,7 +1309,7 @@ name = "colorlog" version = "6.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } wheels = [ @@ -1332,7 +1333,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -1405,7 +1406,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } @@ -1675,8 +1676,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic" }, - { name = "typing-extensions" }, + { name = "aiologic", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -1930,7 +1931,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -2513,11 +2514,11 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, + { name = "google-auth", marker = "python_full_version >= '3.14'" }, + { name = "googleapis-common-protos", marker = "python_full_version >= '3.14'" }, + { name = "proto-plus", marker = "python_full_version >= '3.14'" }, + { name = "protobuf", marker = "python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/cd/63f1557235c2440fe0577acdbc32577c5c002684c58c7f4d770a92366a24/google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300", size = 166266, upload-time = "2025-10-03T00:07:34.778Z" } wheels = [ @@ -2526,8 +2527,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, + { name = "grpcio", marker = "python_full_version >= '3.14'" }, + { name = "grpcio-status", marker = "python_full_version >= '3.14'" }, ] [[package]] @@ -2541,11 +2542,11 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, + { name = "google-auth", marker = "python_full_version < '3.14'" }, + { name = "googleapis-common-protos", marker = "python_full_version < '3.14'" }, + { name = "proto-plus", marker = "python_full_version < '3.14'" }, + { name = "protobuf", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } wheels = [ @@ -2554,8 +2555,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, + { name = "grpcio", marker = "python_full_version < '3.14'" }, + { name = "grpcio-status", marker = "python_full_version < '3.14'" }, ] [[package]] @@ -2741,12 +2742,12 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" } }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-crc32c" }, - { name = "google-resumable-media" }, - { name = "requests" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "google-auth", marker = "python_full_version >= '3.14'" }, + { name = "google-cloud-core", marker = "python_full_version >= '3.14'" }, + { name = "google-crc32c", marker = "python_full_version >= '3.14'" }, + { name = "google-resumable-media", marker = "python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/ef/7cefdca67a6c8b3af0ec38612f9e78e5a9f6179dd91352772ae1a9849246/google_cloud_storage-3.4.1.tar.gz", hash = "sha256:6f041a297e23a4b485fad8c305a7a6e6831855c208bcbe74d00332a909f82268", size = 17238203, upload-time = "2025-10-08T18:43:39.665Z" } wheels = [ @@ -2764,12 +2765,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" } }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-crc32c" }, - { name = "google-resumable-media" }, - { name = "requests" }, + { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "google-auth", marker = "python_full_version < '3.14'" }, + { name = "google-cloud-core", marker = "python_full_version < '3.14'" }, + { name = "google-crc32c", marker = "python_full_version < '3.14'" }, + { name = "google-resumable-media", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" } wheels = [ @@ -4231,13 +4232,13 @@ name = "langchain-classic" version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core" }, - { name = "langchain-text-splitters" }, - { name = "langsmith" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sqlalchemy" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'" }, + { name = "langchain-text-splitters", marker = "python_full_version >= '3.11'" }, + { name = "langsmith", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9b/78/84b5065816f348c39fefa4316f209f0135e8410216340a953bec17d9e4e4/langchain_classic-1.0.7.tar.gz", hash = "sha256:debbec8065e69b95108d2652e8d5c44f4516e19aa8d716c02ed2211c3aee099d", size = 10554118, upload-time = "2026-05-07T15:46:56.8Z" } wheels = [ @@ -4252,18 +4253,18 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "aiohttp" }, - { name = "dataclasses-json" }, - { name = "httpx-sse" }, - { name = "langchain" }, - { name = "langchain-core" }, - { name = "langsmith" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "pydantic-settings" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sqlalchemy" }, - { name = "tenacity" }, + { name = "aiohttp", marker = "python_full_version < '3.11'" }, + { name = "dataclasses-json", marker = "python_full_version < '3.11'" }, + { name = "httpx-sse", marker = "python_full_version < '3.11'" }, + { name = "langchain", marker = "python_full_version < '3.11'" }, + { name = "langchain-core", marker = "python_full_version < '3.11'" }, + { name = "langsmith", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pydantic-settings", marker = "python_full_version < '3.11'" }, + { name = "pyyaml", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "sqlalchemy", marker = "python_full_version < '3.11'" }, + { name = "tenacity", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/49/2ff5354273809e9811392bc24bcffda545a196070666aef27bc6aacf1c21/langchain_community-0.3.31.tar.gz", hash = "sha256:250e4c1041539130f6d6ac6f9386cb018354eafccd917b01a4cff1950b80fd81", size = 33241237, upload-time = "2025-10-07T20:17:57.857Z" } wheels = [ @@ -4281,19 +4282,19 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "aiohttp" }, - { name = "dataclasses-json" }, - { name = "httpx-sse" }, - { name = "langchain-classic" }, - { name = "langchain-core" }, - { name = "langsmith" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "aiohttp", marker = "python_full_version >= '3.11'" }, + { name = "dataclasses-json", marker = "python_full_version >= '3.11'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.11'" }, + { name = "langchain-classic", marker = "python_full_version >= '3.11'" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'" }, + { name = "langsmith", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pydantic-settings" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sqlalchemy" }, - { name = "tenacity" }, + { name = "pydantic-settings", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.11'" }, + { name = "tenacity", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/97/a03585d42b9bdb6fbd935282d6e3348b10322a24e6ce12d0c99eb461d9af/langchain_community-0.4.1.tar.gz", hash = "sha256:f3b211832728ee89f169ddce8579b80a085222ddb4f4ed445a46e977d17b1e85", size = 33241144, upload-time = "2025-10-27T15:20:32.504Z" } wheels = [ @@ -4302,9 +4303,10 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.4.8" +version = "1.6.12" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "httpx" }, { name = "jsonpatch" }, { name = "langchain-protocol" }, { name = "langsmith" }, @@ -4315,23 +4317,24 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/e3/bea6d0080acf183332f24dcd74c208aee5857cf8f783c3fb0bd86027d8fb/langchain_core-1.4.8.tar.gz", hash = "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", size = 957974, upload-time = "2026-06-18T19:39:23.636Z" } +sdist = { url = "https://files.pythonhosted.org/packages/12/c2/9779d64322d4e270f6d5392bc86b30f735581a87b5eefd6ad15a0828a169/langchain_core-1.6.12.tar.gz", hash = "sha256:dc6b2a965c351b4cc94df1c80220bcf3c5472e14437d2186c8290ea22f4d0219", size = 1015639, upload-time = "2026-10-16T19:28:31.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/d6/bdf6f0481cc57ef300d6b1eb48cf1400c0409be715d6eb3cabadd1142a09/langchain_core-1.4.8-py3-none-any.whl", hash = "sha256:d84c28b05e3ba8d4271d0827aad5b592ccdaaf986e76768c23503f0a2045e8aa", size = 557416, upload-time = "2026-06-18T19:39:21.902Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7b/49de1d4d6b9ff62e7c40a6fceb6aeecd8c190ac5f49dd7a99948c6650c16/langchain_core-1.6.12-py3-none-any.whl", hash = "sha256:39b07aed976baac14c47b7a21e3c7d4073362bccf06cf8799c585dcc9548547d", size = 573838, upload-time = "2026-10-16T19:28:29.466Z" }, ] [[package]] name = "langchain-openai" -version = "1.1.14" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "certifi" }, { name = "langchain-core" }, { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/f5/b1a56f703fb90952b07ff9fb5507123a39df1267d62a7f2bb821c5dbb628/langchain_openai-1.1.14.tar.gz", hash = "sha256:71b4262932fabe506ce79c175dbc956cc48f24d81e20b27662df493147750643", size = 1115195, upload-time = "2026-04-16T14:55:24.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/ca/b64799a9f2ee688aa6c238f5230b20d150b19cc5892bdcb6faf2b59866f1/langchain_openai-1.7.2.tar.gz", hash = "sha256:e1fcbc7a96f7c8c94679d61639a979bba88f27889c0a68dfa225b8057345328d", size = 3405949, upload-time = "2026-10-14T13:50:59.956Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/fa/8c33befbc0cf81b21371cc1dab4e7bf94a80b8116194f263a5021ec02529/langchain_openai-1.1.14-py3-none-any.whl", hash = "sha256:cb525d2011f9813fc15a7dcfd4bca5b87badcbcb2c113a7fbe45d1b8a1bbb69c", size = 88705, upload-time = "2026-04-16T14:55:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/39/5d/8c4888a21087e4cd1c32965613a1a17cfc8e90e41ba5205c3dad302f3410/langchain_openai-1.7.2-py3-none-any.whl", hash = "sha256:9be7980edf355ca2dd595b795a5125836ccf04515ef9377228913246156e59f5", size = 146367, upload-time = "2026-10-14T13:50:58.697Z" }, ] [[package]] @@ -4351,7 +4354,7 @@ name = "langchain-text-splitters" version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580, upload-time = "2026-04-16T14:20:39.162Z" } wheels = [ @@ -4439,23 +4442,27 @@ wheels = [ [[package]] name = "langsmith" -version = "0.8.18" +version = "0.14.11" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "httpx" }, + { name = "anyio" }, + { name = "distro" }, + { name = "httpx2" }, { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, { name = "packaging" }, { name = "pydantic" }, { name = "requests" }, { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, { name = "uuid-utils" }, { name = "websockets" }, { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/a6681aa9847bbbc5ec21abe20a5e233b94e5edcfe39624db607ac7e8ccb4/langsmith-0.8.18.tar.gz", hash = "sha256:32dde9c0e67e053e0fb738921fc8ced768af7b8fa83d7a0e3fd63597cf8776dd", size = 4526988, upload-time = "2026-06-19T13:12:17.123Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/c3/a9a98d7adf573fdd5ea5c92cadbc26d7c597ab36f4dc31c093eb19507808/langsmith-0.14.11.tar.gz", hash = "sha256:d9e7ffe587e12ce51573cab654d02e84feb924d3b1f582274c4b1412ebeabf4c", size = 5020659, upload-time = "2026-10-16T07:16:56.865Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/70/0e0cc80a3b064c8d6c8d697c3125ed86e39d5a7393ec6dc8b07cb1cf13c4/langsmith-0.8.18-py3-none-any.whl", hash = "sha256:3940183349993faef48e6c7d08e4822ee9cefd906b362d0e3c2d650314d2f282", size = 508108, upload-time = "2026-06-19T13:12:15.348Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1d/b683b3d8777e625e021efeaaf72dc04f709be59e6a5f2d65725cebaedcfb/langsmith-0.14.11-py3-none-any.whl", hash = "sha256:9ecca7633df228ef2c44563f7187e426b8a79e7c5c2351c5dd8c9074c61512e7", size = 869018, upload-time = "2026-10-16T07:16:54.634Z" }, ] [[package]] @@ -4996,7 +5003,7 @@ ci = [ { name = "google-generativeai", specifier = "==0.8.6" }, { name = "jsonlines", specifier = "==4.0.0" }, { name = "langchain", specifier = "==1.3.9" }, - { name = "langchain-openai", specifier = "==1.1.14" }, + { name = "langchain-openai", specifier = "==1.7.2" }, { name = "langgraph", specifier = ">=1.2.4,<1.3.0" }, { name = "langgraph-prebuilt", specifier = ">=1.1.0,<1.3.0" }, { name = "logfire", specifier = "==4.6.0" }, @@ -5087,10 +5094,10 @@ name = "litellm-admin-mcp" version = "0.1.0" source = { url = "https://github.com/BerriAI/liteadmin-mcp/archive/d35ec9c19c117d4c50cc1eccf6ce1296aac25a1a.tar.gz" } dependencies = [ - { name = "httpx2" }, - { name = "jsonschema" }, - { name = "mcp" }, - { name = "uvicorn" }, + { name = "httpx2", marker = "python_full_version >= '3.12'" }, + { name = "jsonschema", marker = "python_full_version >= '3.12'" }, + { name = "mcp", marker = "python_full_version >= '3.12'" }, + { name = "uvicorn", marker = "python_full_version >= '3.12'" }, ] sdist = { hash = "sha256:86b930f6706fb2da10d53d1798ee7fd14e44fb0afdef0da122cf0e6bb88fa2c3" } @@ -5193,16 +5200,16 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "aiohttp" }, - { name = "chevron" }, - { name = "jsonpickle" }, - { name = "langchain-community", version = "0.3.31", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyhumps" }, - { name = "requests" }, - { name = "setuptools" }, - { name = "tenacity" }, + { name = "aiohttp", marker = "python_full_version < '3.11'" }, + { name = "chevron", marker = "python_full_version < '3.11'" }, + { name = "jsonpickle", marker = "python_full_version < '3.11'" }, + { name = "langchain-community", version = "0.3.31", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pydantic", marker = "python_full_version < '3.11'" }, + { name = "pyhumps", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "setuptools", marker = "python_full_version < '3.11'" }, + { name = "tenacity", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4a/6f/9ca1acf766848aaf5f0ac4140c34c91ad0dbfad2654359699644be3352c9/lunary-1.4.36.tar.gz", hash = "sha256:53f002f385c83d9c0e6368e7999923acffbde987f53c5205c2c249c38ee2d75c", size = 20253, upload-time = "2026-02-09T20:49:30.56Z" } wheels = [ @@ -5220,16 +5227,16 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "aiohttp" }, - { name = "chevron" }, - { name = "jsonpickle" }, - { name = "langchain-community", version = "0.4.1", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyhumps" }, - { name = "requests" }, - { name = "setuptools" }, - { name = "tenacity" }, + { name = "aiohttp", marker = "python_full_version >= '3.11'" }, + { name = "chevron", marker = "python_full_version >= '3.11'" }, + { name = "jsonpickle", marker = "python_full_version >= '3.11'" }, + { name = "langchain-community", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "pyhumps", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "setuptools", marker = "python_full_version >= '3.11'" }, + { name = "tenacity", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/37/ef/1acbc6957585cc0110e648d787663871717ced3df27fcd3cb5e18fa418f3/lunary-1.4.37.tar.gz", hash = "sha256:1781091e9dceffcc28ebc4be7e085c9fec4102d98d7ca945ed0021e9ce03c36f", size = 20248, upload-time = "2026-02-12T08:15:02.091Z" } wheels = [ @@ -9090,10 +9097,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -9140,11 +9147,11 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } wheels = [ @@ -9194,7 +9201,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -9256,7 +9263,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } @@ -9341,20 +9348,20 @@ name = "semantic-router" version = "0.1.15" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp" }, - { name = "aurelio-sdk" }, - { name = "colorama" }, - { name = "colorlog" }, - { name = "litellm" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "aiohttp", marker = "python_full_version < '3.14'" }, + { name = "aurelio-sdk", marker = "python_full_version < '3.14'" }, + { name = "colorama", marker = "python_full_version < '3.14'" }, + { name = "colorlog", marker = "python_full_version < '3.14'" }, + { name = "litellm", marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "regex" }, - { name = "tiktoken" }, - { name = "tornado" }, - { name = "urllib3" }, + { name = "openai", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "pyyaml", marker = "python_full_version < '3.14'" }, + { name = "regex", marker = "python_full_version < '3.14'" }, + { name = "tiktoken", marker = "python_full_version < '3.14'" }, + { name = "tornado", marker = "python_full_version < '3.14'" }, + { name = "urllib3", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/a9/1a689e916e8b280f1fd8fb335cc059be626a22fe4533baa045d32fcd6de5/semantic_router-0.1.15.tar.gz", hash = "sha256:328256ddc3c2b713101ec69561d6585aecbf1198ea3461e1486289d8c3a35288", size = 95605, upload-time = "2026-05-23T12:58:15.444Z" } wheels = [ @@ -9428,9 +9435,9 @@ name = "smithy-aws-core" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aws-sdk-signers" }, - { name = "smithy-core" }, - { name = "smithy-http" }, + { name = "aws-sdk-signers", marker = "python_full_version >= '3.12'" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, + { name = "smithy-http", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/d3/501c0023548173416109ac42298ca33b708469dc922005770811a597949f/smithy_aws_core-0.11.0.tar.gz", hash = "sha256:29ee89976a520a87e3db557e03e115fdc21a0a60b81161e95174395a1b064da1", size = 38791, upload-time = "2026-08-24T21:16:59.631Z" } wheels = [ @@ -9439,10 +9446,10 @@ wheels = [ [package.optional-dependencies] eventstream = [ - { name = "smithy-aws-event-stream" }, + { name = "smithy-aws-event-stream", marker = "python_full_version >= '3.12'" }, ] json = [ - { name = "smithy-json" }, + { name = "smithy-json", marker = "python_full_version >= '3.12'" }, ] [[package]] @@ -9450,7 +9457,7 @@ name = "smithy-aws-event-stream" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "smithy-core" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/0e/6efb3a4ed92c0f1ada6de060ac92e7115a1e34d0ab1fb99a6056734a88ea/smithy_aws_event_stream-0.3.0.tar.gz", hash = "sha256:a0e227367a973144e205a075d0a424f95c92f26656a1018d08900da2ae547c49", size = 12818, upload-time = "2026-05-05T18:04:14.317Z" } wheels = [ @@ -9471,7 +9478,7 @@ name = "smithy-http" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "smithy-core" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/78/b5f3113d6c8f0bc1f9777a7f5ca84b892d29efac05850e14f7d4f7e645b5/smithy_http-0.5.0.tar.gz", hash = "sha256:bb4a19672f7c7eeb872a308f777eb505281a5bafb1ee3d1ea9c760c06c352510", size = 31122, upload-time = "2026-08-24T21:16:56.488Z" } wheels = [ @@ -9480,11 +9487,11 @@ wheels = [ [package.optional-dependencies] aiohttp = [ - { name = "aiohttp" }, - { name = "yarl" }, + { name = "aiohttp", marker = "python_full_version >= '3.12'" }, + { name = "yarl", marker = "python_full_version >= '3.12'" }, ] awscrt = [ - { name = "awscrt" }, + { name = "awscrt", marker = "python_full_version >= '3.12'" }, ] [[package]] @@ -9492,8 +9499,8 @@ name = "smithy-json" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ijson" }, - { name = "smithy-core" }, + { name = "ijson", marker = "python_full_version >= '3.12'" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/ac/04164eefb3da7479f52f6535b4b39cc8384c292cb2bb74279f2acc4f4b4d/smithy_json-0.3.0.tar.gz", hash = "sha256:c81c7034587e01bc64767cbbecb05a7d65ca9070612fd94e8a03e80540290a22", size = 7956, upload-time = "2026-08-20T17:55:32.177Z" } wheels = [ @@ -9571,23 +9578,23 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, - { name = "tomli" }, + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -9602,23 +9609,23 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -9635,23 +9642,23 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -9799,8 +9806,8 @@ name = "standard-aifc" version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "audioop-lts" }, - { name = "standard-chunk" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "standard-chunk", marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } wheels = [ @@ -9821,7 +9828,7 @@ name = "standard-sunau" version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "audioop-lts" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908", size = 9368, upload-time = "2024-10-30T16:01:41.626Z" } wheels = [ @@ -9855,8 +9862,8 @@ name = "taskgroup" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup" }, - { name = "typing-extensions" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" } wheels = [ From 356087654a8c23b90ae91bda89e3c3e69ba7f766 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:26:24 -0700 Subject: [PATCH 02/23] build(deps): lock openai 3.35.1 so the Docker image runs OpenAI SDK 3 --- uv.lock | 202 +++++++++++++++++++++++++++----------------------------- 1 file changed, 98 insertions(+), 104 deletions(-) diff --git a/uv.lock b/uv.lock index 301225a8277..5143f7168f0 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-10-18T01:19:57.352406047Z" +exclude-newer = "2026-10-18T01:25:51.279086783Z" exclude-newer-span = "P3D" [manifest] @@ -3859,105 +3859,101 @@ wheels = [ [[package]] name = "jiter" -version = "0.14.0" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/1f/8176d92e001f86505424b41664032ae26a882bc9ca41a32c803f373f9195/jiter-0.17.0.tar.gz", hash = "sha256:03e432f226a453851079fb84cd17c6da9991eab723e28d716f14ae3d906e0c12", size = 229037, upload-time = "2026-09-12T15:14:14.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/2e/a9959997739c403378d0a4a3a1c4ed80b60aeace216c4d37b303a9fc60a4/jiter-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:02f36a5c700f105ac04a6556fe664a59037a2c200db3b7e88784fac2ddf02531", size = 316927, upload-time = "2026-04-10T14:25:40.753Z" }, - { url = "https://files.pythonhosted.org/packages/27/72/b6de8a531e0adbadd839bec301165feb1fccf00e9ff55073ba2dd20f0043/jiter-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41eab6c09ceffb6f0fe25e214b3068146edb1eda3649ca2aee2a061029c7ba2e", size = 321181, upload-time = "2026-04-10T14:25:42.621Z" }, - { url = "https://files.pythonhosted.org/packages/db/d8/2040b9efa13c917f855c40890ae4119fe02c25b7c7677d5b4fa820a851fc/jiter-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf4d4c109641f9cfaf4a7b6aebd51654e405cd00fa9ebbf87163b8b97b325aa", size = 347387, upload-time = "2026-04-10T14:25:44.212Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/655c0ad5ce6a8e90f9068c175b8a236877d753e460762b3183c136db1c5b/jiter-0.14.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80c7b41a628e6be2213ad0ece763c5f88aa5ee003fa394d58acaaee1f4b8342", size = 373083, upload-time = "2026-04-10T14:25:45.55Z" }, - { url = "https://files.pythonhosted.org/packages/f1/66/549c40fa068f08710b7570869c306a051eb67a29758bd64f4114f730554c/jiter-0.14.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb3dbf7cc0d4dbe73cce307ebe7eefa7f73a7d3d854dd119ea0c243f03e40927", size = 463639, upload-time = "2026-04-10T14:25:47.452Z" }, - { url = "https://files.pythonhosted.org/packages/25/2f/97a32a05fed14ed58a18e181fdfb619e05163f3726b54ee6080ec0539c09/jiter-0.14.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7054adcdeb06b46efd17b5734f75817a44a2d06d3748e36c3a023a1bb52af9ec", size = 380735, upload-time = "2026-04-10T14:25:49.305Z" }, - { url = "https://files.pythonhosted.org/packages/2a/3b/4347e1d6c2a973d653bbb7a2d671a2d2426e54b52ba735b8ff0d0a29b75c/jiter-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d597cd1bf6790376f3fffc7c708766e57301d99a19314824ea0ccc9c3c70e1e2", size = 358632, upload-time = "2026-04-10T14:25:50.931Z" }, - { url = "https://files.pythonhosted.org/packages/ef/24/ca452fbf2ea33548ed30ce68a39a50442d3f7c9bf0704a7af958a930c057/jiter-0.14.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:df63a14878da754427926281626fd3ee249424a186e25a274e78176d42945264", size = 359969, upload-time = "2026-04-10T14:25:52.381Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a3/94470a0d199287caabeb4da2bb2ae5f6d17f3cf05dfc975d7cb064d58e0f/jiter-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ea73187627bcc5810e085df715e8a99da8bdfd96a7eb36b4b4df700ba6d4c9c", size = 397529, upload-time = "2026-04-10T14:25:53.801Z" }, - { url = "https://files.pythonhosted.org/packages/cf/71/6768edc09d7c45c39f093feb3de105fa718a3e982b5208b8a2ed6382b44b/jiter-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9f541eaf7bb8382367a1a23d6fc3d6aad57f8dd8c18c3c17f838bee20f217220", size = 522342, upload-time = "2026-04-10T14:25:55.396Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6b/5c2e17559a0f4e96e934479f7137df46c939e983fa05244e674815befb73/jiter-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:107465250de4fce00fdb47166bcd51df8e634e049541174fe3c71848e44f52ce", size = 556784, upload-time = "2026-04-10T14:25:56.927Z" }, - { url = "https://files.pythonhosted.org/packages/b1/83/c25f3556a60fc74d11199100f1b6cc0c006b815c8494dea8ca16fe398732/jiter-0.14.0-cp310-cp310-win32.whl", hash = "sha256:ffb2a08a406465bb076b7cc1df41d833106d3cf7905076cc73f0cb90078c7d10", size = 208439, upload-time = "2026-04-10T14:25:58.796Z" }, - { url = "https://files.pythonhosted.org/packages/2e/99/781a1b413f0989b7f2ea203b094b331685f1a35e52e0a45e5d000ecaab27/jiter-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb8b682d10cb0cce7ff4c1af7244af7022c9b01ae16d46c357bdd0df13afb25d", size = 204558, upload-time = "2026-04-10T14:26:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, - { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, - { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, - { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, - { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, - { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, - { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, - { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, - { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, - { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, - { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, - { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, - { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, - { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, - { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, - { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, - { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, - { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, - { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, - { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, - { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, - { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, - { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, - { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, - { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, - { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, - { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, - { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, - { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, - { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, - { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, - { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, - { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, - { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, - { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, - { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, - { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, - { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, - { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, - { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, - { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, - { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, - { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, - { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, - { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, - { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, - { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/e8/fb/0b68a8666a203ab349334e502285153ac97e84a1a3e2f80de8385ed0899c/jiter-0.17.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ed1a24005daac667d577402d75a2922f9775a165b146b883ff1ad3602d8be689", size = 290968, upload-time = "2026-09-12T15:11:10.44Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/5993079500530586655c6f2823e2926b3f5ceddd49918c5a86757eadba3a/jiter-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b847b18d066c46b3b7ae49d6c94a7634c5e4a8983146ee25562a092000f5e3ad", size = 324072, upload-time = "2026-09-12T15:11:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/6f5dbffaa5b0e647386e3c3376aa150987538f1b13f6da1a9582787957af/jiter-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b68d3495d95da120651a5628c7ebadee84ed001a1b76e6afc325c42482f15b5", size = 344891, upload-time = "2026-09-12T15:11:13.387Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9d/8dd5719add6c77be95b23c129682a1484c5339ffd66736d66bfa6831a2f3/jiter-0.17.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c1a5336c04a41b1f1cf9572e294aec27cc569767ff73de7bf87a91f0bea7cb9", size = 328213, upload-time = "2026-09-12T15:11:14.655Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e5/82f885863afaa79efac066efdb7b70bd242ef83d3e548453d61b3737e54a/jiter-0.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b75f85660108965a94be77911a25a253429307294d9415b3c597118977a614de", size = 343033, upload-time = "2026-09-12T15:11:16.046Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/ee3cd7db1704ec9c274ae549803ef1369c801490f7f6d48d0aa3b9ac1ae0/jiter-0.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32aaaa764604496610a3ad2d98503ae88ccb2fbe769e892ff4533e778e85f708", size = 351701, upload-time = "2026-09-12T15:11:19.188Z" }, + { url = "https://files.pythonhosted.org/packages/93/2c/af9ca503b38683a82b6732352a861515f28e2d46fe445aea57e07fc86e06/jiter-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:826871c42cebaae22f0a2b5673a4a1a75c851bb2d13b3c17764a630a6b298984", size = 348754, upload-time = "2026-09-12T15:11:20.486Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ba/692b071b194270b019b7d1790adb8658193379b951029e5ad15a14a2809d/jiter-0.17.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:00b5a98df3e3a3e8cf7b619f4ac2f8bf975bbf3d95d02c5d17b8dbfe5c8b8245", size = 329263, upload-time = "2026-09-12T15:11:21.701Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8f/35a22beb6b9eb025b7e2f88a38ca3b9c87017aac237feec2583eabe243ad/jiter-0.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6af5b74073bd25bae695e6d00919f6a9be7ed5a9f8836d981eb1ffe84139e6fb", size = 337572, upload-time = "2026-09-12T15:11:23.145Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0f/4341ceb3ce08199b4e8a798acc0bb0c79b3e861b6cecdb9d3b5138965c5d/jiter-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:16dd0c1baf098ae70b8f3616574eb3fedf34e26670b89e16a7e67561f737ed2d", size = 485174, upload-time = "2026-09-12T15:11:24.394Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6d/92fa138ac82db9001b68563df204c32f2203bee84301df083260039ca23b/jiter-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:545c36a0f3b2238c242cc9785439d3242a871b7bc39fe3f441bcaa07bf3aa83e", size = 522115, upload-time = "2026-09-12T15:11:25.645Z" }, + { url = "https://files.pythonhosted.org/packages/52/92/9b11557ddf898e84c253d1c42cc1bddd8ad1a754b46b01ae35a6a253526f/jiter-0.17.0-cp310-cp310-win32.whl", hash = "sha256:155be7355bdb7ca76ab0961be8982c225f964a5c073a83984183f22391cc29fc", size = 188163, upload-time = "2026-09-12T15:11:26.793Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f3/b4381cc962fed7d505913d5920e9f51f1fd5cf340a563134e8cadf3604d2/jiter-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:37150a9e02e869475854fa20b7d0d5e26d18d0f8bc17293999973ff27e99ae7a", size = 231679, upload-time = "2026-09-12T15:11:28.911Z" }, + { url = "https://files.pythonhosted.org/packages/a1/50/17afdaffcc8af4bf4fddf2b6c26d066553aa2221983f2affcde435fc2532/jiter-0.17.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cfafd7be8b16ceadd298db542cead37cddc211c4c49e04ad2596924df18625b1", size = 290699, upload-time = "2026-09-12T15:11:30.085Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/c185d32d5b3657ad84da26c84a9eb15f00aa1b39d6882fcc0052dba2d7c2/jiter-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8adca2e793288e5f1bb29279bb439d0d3cfbb50eddca7e7e6ffd42ff4f482406", size = 324246, upload-time = "2026-09-12T15:11:31.3Z" }, + { url = "https://files.pythonhosted.org/packages/24/7a/8b8903bfe91a90a8fa1ec9b45d9fda5b6287a386693b69d720a882d73f3c/jiter-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c692d567ba206c7cca38c9d1d0ccc70c9786290173c184d871ca12e9981ed7", size = 343275, upload-time = "2026-09-12T15:11:32.758Z" }, + { url = "https://files.pythonhosted.org/packages/a2/5d/6821fae2abc71a3c3a84bef8598d31fc4f27d9edfb55bd8f6c08afb8ef93/jiter-0.17.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81c83c0abe614446a283d994d2c07c4f58632dea2cdf66ba9e2921bb8ccd593e", size = 328034, upload-time = "2026-09-12T15:11:33.9Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/8e7a963b1c2dfdc01d6228b004f50a2a3d7c46f0549d7b096a5d15ef81d5/jiter-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:073dc68c1a700c8fc480e877864a6b6ffc887533e261f4380c08c16bf09d057a", size = 342457, upload-time = "2026-09-12T15:11:36.414Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/8b2a267e3bda45d9298331cacfe3521e761f0a2b05ad10a23c6548d08358/jiter-0.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:492f37230bbf9581ab2c17bcda862c249afb9ae2e3ab2dd6db59943bc4cc3153", size = 351271, upload-time = "2026-09-12T15:11:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8f/5c74e5e142a6736833d7a991ab04d5c0738038dc44db05af0bb3cd2559e8/jiter-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5888fe5abc1ca2fa834a3e1b4c7ef0dcece286a7d7e95a609ef0934b777b9fc9", size = 346052, upload-time = "2026-09-12T15:11:39.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/9c/f54920f06d1696e80b1be841d56412871c6856b1b1e3b541b1ed35346554/jiter-0.17.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:84ac78df457e1ee3f7e733bd114823302ae8c5ad5542d7e6647d92ffaa090a04", size = 328915, upload-time = "2026-09-12T15:11:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/21/53/080f126863bceb055db9f1fd5431485eb493b35545a3c35e961ac18cd924/jiter-0.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7573e80232c5bcf80c24c038cf7e53a463f5c3b1dd1dd4109d66304f4dccc233", size = 337240, upload-time = "2026-09-12T15:11:42.356Z" }, + { url = "https://files.pythonhosted.org/packages/f0/76/3ab742823a0e0e70e143c6c90a482d9d90396ac2445e7b9483eb7245d3b5/jiter-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:11902505d401691720f5785c15b02204248526edee11b635cd6c40cd52b81599", size = 484917, upload-time = "2026-09-12T15:11:43.549Z" }, + { url = "https://files.pythonhosted.org/packages/14/e0/8ca71bc8b9cc9ed96c9da565863e00f3bb875a8fb82abcf03e975e067902/jiter-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:64846211a2debe7c071d2146d2283d2b0c1c93dc8fd5fb7794faac2ca6061b5c", size = 521868, upload-time = "2026-09-12T15:11:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/c1/02/81f8719dcedb75713082a2048405376c81f6546b75af8167f3bba01a1ed0/jiter-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c19b9357309b8cc6de8a48fca8e44a8c9c2feaaa2f5896d037fa505d48fcab80", size = 187844, upload-time = "2026-09-12T15:11:45.874Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8c/59693f348488f01ed12d862e99ab8da14961152d3e9c39b9b1ef363f3572/jiter-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:e654b6b04e39c9cb19cb8b04c6ddf1f2db07751fa14156413969fd78bad0e5cb", size = 231402, upload-time = "2026-09-12T15:11:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/fe/89/fb35e286463cb9f01edc2c4e47df6e5477ee36bca5096414e9ea87985588/jiter-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:3ad556afc289f15d2b181b941982d01f06190863c07440185b9f354e1bd2def3", size = 187006, upload-time = "2026-09-12T15:11:48.245Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f8/07bd8c3a23f7a8a6875e6a820bbffe1483a18f18f9398a91b5495123176e/jiter-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ebf918dfd6a74adc1b9ad71f63c4ab00902fcd3b7fd39f2e24d871db8d713b91", size = 291633, upload-time = "2026-09-12T15:11:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5e/0de4c6f84ffefa6809ffc2d550b9a314365acf7e7ec9b6c7375d49047900/jiter-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61aed66ee042b3b49ef85fdf75714234d055d89d8496ac1c6e47f89e7a30d5e4", size = 321695, upload-time = "2026-09-12T15:11:52.727Z" }, + { url = "https://files.pythonhosted.org/packages/20/ac/befe2e82065bee37a0252081666ed2f48c1ac5f5c6c318c2de8168ba393d/jiter-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76eb4a5c20e86f9f848286f167024890f2862258a965d254774deb7fc1545ca1", size = 341967, upload-time = "2026-09-12T15:11:54.231Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cd/9797c1e529746750ae589da7c1a8c24373f00d88e11a989f9e5eb1959079/jiter-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bcc064f99183a9cbe7f26ed648c352031a74145cd61ed75d34632c73eb46a5a8", size = 326546, upload-time = "2026-09-12T15:11:55.41Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fd/e6914c38d6347bab4ebff2b1f0c0f191db276e7a1d5c376176757da42fe3/jiter-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73b64e69c4150748e020356d958af94bec33c70a0a93d665cfa8f6d580fe1a63", size = 340995, upload-time = "2026-09-12T15:11:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7d/611b3abf6f88945b5474da5cdc6d1a185e805ac9bf446bb7766dcda6ea87/jiter-0.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0bc7f684b65bcda9c20434267577db71bf9905ceddd32b60d1d93278d8c8d3a", size = 352188, upload-time = "2026-09-12T15:11:59.414Z" }, + { url = "https://files.pythonhosted.org/packages/52/f8/b6e513ecbdf3b3cebe587c2279281ecf775b729a58cf4cc7bdf898ded029/jiter-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c21265b251d99bbb40080d178a8953e35601d3a1564e05c4de4c0d2ca616797", size = 345025, upload-time = "2026-09-12T15:12:00.697Z" }, + { url = "https://files.pythonhosted.org/packages/28/a8/fe26d06c5a6c5a4cfe703c5154c8a140da1305671eb3681aba9422d4f393/jiter-0.17.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:f3d7f7b34114f7ddc6d72a8e882d49de636b35d9fd12b4d420d3c5729f6c9812", size = 329180, upload-time = "2026-09-12T15:12:01.831Z" }, + { url = "https://files.pythonhosted.org/packages/e1/58/e6d66a26af40a20e62486feb7e222fd50f6e7aaa4f107abd89675dcc835b/jiter-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5078ab00664307fab2019b522a93aeb191122789f085daf5fd9e362154021d4a", size = 335805, upload-time = "2026-09-12T15:12:03.056Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/96520aa2fef5ef831d95483a902140bfab83dcac9eaa74f7df61b5e50a1b/jiter-0.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:470e1b1e4c42f1ead2189166a299691871a2df5056c976e7fb96feafaf5f9d44", size = 484121, upload-time = "2026-09-12T15:12:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8f/5d9d92fe538bf36ff481a2278c48147e59c1cf8eb2f7be665260665febe5/jiter-0.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6eb6aedeb7352b8f3b6af9cbd67983840165c00428e63f1b420a85885128ea31", size = 521310, upload-time = "2026-09-12T15:12:05.612Z" }, + { url = "https://files.pythonhosted.org/packages/50/06/a09f979b22e652afbc3de66c709b2ba92edcef555f7535ab937c86b4f21a/jiter-0.17.0-cp312-cp312-win32.whl", hash = "sha256:362bb47423886d45a9f705d2d9d4008c6eedd4e41eb1bab4e96fb6daa06b33fd", size = 185029, upload-time = "2026-09-12T15:12:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/98265a005b2473ec2be5a84e2b64c2f65382c673879f1574845cd4bcd77c/jiter-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:9bd3caac219df476dd0cc3fe01d2f1581ed588906feac767abd9614c1c12f8b3", size = 227381, upload-time = "2026-09-12T15:12:08.823Z" }, + { url = "https://files.pythonhosted.org/packages/a8/11/2e05bf5a56e57a543ebb8f585074adf09383e99d7b062dac92eab1f4d57f/jiter-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:36ee6e69027396664e59995b9a635a947a5304ee9837279584a0bb8145c8f6b8", size = 183610, upload-time = "2026-09-12T15:12:10.374Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/2c4a8075ed5ea02b56911e9375d4c8d7784572ff4af32e5a99ae0d071044/jiter-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b18434638228c0c184281609bf3d9459026a0f1ea48fb76c205e3ef72069caa", size = 290991, upload-time = "2026-09-12T15:12:11.641Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b1/34bfa29599d420423baac6ff7cada6674fe63d5a7a2ccb3900b904678783/jiter-0.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ec89771f4272b989487a6364e519db6bbaba323e8bbf949ac89a45ea9c18b7a3", size = 321425, upload-time = "2026-09-12T15:12:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/11/71/a5ac64a62a04aebd556afadab14a6b730001e16df87266ded943a100a1d9/jiter-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3f052c671d5f425cca5ea5901cf11a831369fba4a55a3862cab93c323b4c3b", size = 343138, upload-time = "2026-09-12T15:12:15.046Z" }, + { url = "https://files.pythonhosted.org/packages/01/dd/f761e320ea473314cb68612bc6a435393464dbd198051399b36848b4ebf3/jiter-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785a216bbaf8f15fc974e964ced7322cd3d774bb0e86949edd78c6bffd6ba35b", size = 325805, upload-time = "2026-09-12T15:12:16.506Z" }, + { url = "https://files.pythonhosted.org/packages/19/1a/27d8e40f0fb29bbc7a5adf30907144396a115dbe93d5d8976c054a6dfe96/jiter-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d85c558c9f8532bba287a990ac63767c7daf756f0d8c030219f62499b1fa228a", size = 340230, upload-time = "2026-09-12T15:12:17.682Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c0/30bcde78a28155461f965d16b7aca4ffca6d17494d905f7a0bb072e6c64e/jiter-0.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c23849235d2142ce444b2b8c6eceee9f82f4cc0bd5c9081602e4155c6197807", size = 351343, upload-time = "2026-09-12T15:12:19.337Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/91420b156315ae22732f5ee1a7b5725a030aab9dc8fd7dcdacfb4aa588d3/jiter-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58df29268a95e910f17db7ec9178eb7f15aa8619aaca3575275c4e6b3f4fe4c5", size = 344990, upload-time = "2026-09-12T15:12:20.705Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a2/ae6d5672644cc11127970277c9aeb0fa6fae376845587f5b0a8e8828167c/jiter-0.17.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a277f97eba7d66b1ee27eb5dab5b774ff46a10c78d89a1d3dcce04ce1357c8ca", size = 328624, upload-time = "2026-09-12T15:12:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/04/62/45cb1162f6aa586536e4a973fc339d72dc6b08cca030d70a838a307aa778/jiter-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe15ddf316f1f1f643347d3a474e74ce61880c79a11ec5dca53df20c071bd3e8", size = 334731, upload-time = "2026-09-12T15:12:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/45c1574b644da7deda0b7591c349520dcf83ce45b24d7ca19922dab1fc27/jiter-0.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:02adebb7ce6413c44d40af9ad59d1c1cd79630ccdcb6f7bdd2d461e48c03d8f9", size = 483649, upload-time = "2026-09-12T15:12:27.557Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d3/ebea1ecb5b241c519f192b30215c79a8e47f42f1621acbcd6f8830728416/jiter-0.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:55d0e0e613a3f9ad600cf436e0e2b8057d1b52bcf1d91b2d36ac53451231e6a8", size = 520758, upload-time = "2026-09-12T15:12:28.99Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/682b641ff0765ea9bdc349dbc7d223de5c8af8ec1abda0db3406992f92fe/jiter-0.17.0-cp313-cp313-win32.whl", hash = "sha256:2c45ad7c973ef33fe5114a953377b35a95240f4542c0724d9f781e47dc24bac7", size = 184334, upload-time = "2026-09-12T15:12:30.813Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d2/9a49aac2b27af4cc5015e368c0cc3588491a532f717a668ffce1f1ac57da/jiter-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:a3cebb1fe4a1abb00465f3f8a17e09112603e8b7c59e5c3adbcd9f7815a64acd", size = 226601, upload-time = "2026-09-12T15:12:32.096Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ce/9a43e9f614608eafa78de22aedcff54cd21324467b5d442d5c9b00244145/jiter-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:96b8b0c6dc5d78682f54a450785e075aa929cde768304cad363cd4efba5a82ac", size = 183103, upload-time = "2026-09-12T15:12:34.396Z" }, + { url = "https://files.pythonhosted.org/packages/01/9e/23065f8e2c7a4c372c1b6f6622e4cfab4dc786cb5150052b1527e6a6a840/jiter-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:00d783a779c5664e16dbad5e3a3c3a75e128b07dd5f4765159658d9210a50ca5", size = 292210, upload-time = "2026-09-12T15:12:35.613Z" }, + { url = "https://files.pythonhosted.org/packages/ea/81/67b58647560bc82a4490d722caa8561d7a86a9f45d4fa620b7e5fe282c7a/jiter-0.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0619d806e260ecf0c2a64521942c94af5d547c9ec99b55ae4f51b538b5576a76", size = 321512, upload-time = "2026-09-12T15:12:36.907Z" }, + { url = "https://files.pythonhosted.org/packages/c7/07/6658359a25f55927f7f8bf0e16465dee2ccd0b2a1a5208acc0df8972e074/jiter-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc0288ce39190ee33fe6e4ec73161eed34e7e2da509b525546ca061778d62b64", size = 343897, upload-time = "2026-09-12T15:12:38.189Z" }, + { url = "https://files.pythonhosted.org/packages/46/04/5d50a9f0319cbdc37fd53c27f8c313d46afc34f1b048219ae6d8ea068da4/jiter-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a52a430d04225ffde633e6840bf2381d34c019ff98526b5929755b9052fb199", size = 326519, upload-time = "2026-09-12T15:12:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c7/d02517832b29eb8275fdd0f4ce0f17b80f58cc4c3ebecd4d9ace990d633d/jiter-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37f33d327900bf2879613b3363fd48df97b4232d0c41f54bcf2e790c2fc40a71", size = 341369, upload-time = "2026-09-12T15:12:41.486Z" }, + { url = "https://files.pythonhosted.org/packages/3b/07/499b5f5603501cdd93a73a6a176dfad9c96555a3ae58ca9f8e3acba63dc9/jiter-0.17.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cf564d43c4388149ca58ee571d0f5ccf875e20d1fd4662fd94cc0d1ea3b10ef", size = 352160, upload-time = "2026-09-12T15:12:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/f5/75/b04013c7743269d4533ef4e746fc0ed678a143968dd7448658e3f51daad2/jiter-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:523c499235fb65add25d4bb01b1c4709ce695efdc7deb6c0a7bc515b5c44e0fb", size = 345018, upload-time = "2026-09-12T15:12:44.192Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/cbb6fd1e42a77c8412ec4643db95059b30cdfc635e387cc9193e098ce268/jiter-0.17.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:455e4ab35cb2a4a91a8404e08fd3c621bae433922e59bf1c494fe20a426b013b", size = 329244, upload-time = "2026-09-12T15:12:45.491Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/d3be402f398566a379bf40ae65be5c3505b14d9e95e0802a597ddde7ddee/jiter-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6871973bfbd4408f7f1c632b30bbb5bbd9671c1bc8650af6823e24b7be13709b", size = 335693, upload-time = "2026-09-12T15:12:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/98e2c4130b93d64f1d67c89060b928d04102549bf05e64451c9e6024f9ca/jiter-0.17.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:77f6aac0137309b31448c1bdcda4c6c77077664a6d018ece8d94019c68a5a5b9", size = 484329, upload-time = "2026-09-12T15:12:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/78/5e/8da91e49f0fbca37c3489fb4cf3ad6676d4965f00ae5468bca3a2513737a/jiter-0.17.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:93946d89fa04d5ba64dd323a8dd8d901676cb8a3c81d99ae4f6c051a9b4c3f2f", size = 521358, upload-time = "2026-09-12T15:12:49.856Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/5388684a5a38af3557cd9c2424b9827c71809cff24373c75ef9d0d3dfba9/jiter-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:70f19a2ca8429f91e82eeffb2f51cb87bc2d6e953b009b91a92d29c3a16ccb03", size = 110459, upload-time = "2026-09-12T15:12:51.747Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/58b3a93525d2ffca7f54d9dee441381990082bd1172fbeb8d6a3f72a4dc3/jiter-0.17.0-cp314-cp314-win32.whl", hash = "sha256:71dbd74314c5df52a1bccf7b8bca46d14e943af7a2012e73b23f49977ef194c8", size = 185043, upload-time = "2026-09-12T15:12:54.477Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/1aa520eb6c359b262c14ff995ca7283837208ddfb1202082ce9d73cf214d/jiter-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:ac3c6ee3264d6f5c44c617f90bc7e8b9e1587e7d6708c9d8f811cb65582ee312", size = 227163, upload-time = "2026-09-12T15:12:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e4/5997f648794bd9b499491d0ff480b096cc9a9c65bdba29f57568e6aa1705/jiter-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:6219adaf59711ba7063a52496e8ec6d3fa3e209d7827d83eee3b2abc780a1744", size = 183505, upload-time = "2026-09-12T15:12:58.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/84a5ec271d09f7590b6073af5ee4abb44eab4ccace453b7e2c5ce45234ca/jiter-0.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:59bddbe6f9ffecc68d641e1e2d619ce64cf8a9e9eeb74e5c518f74fc87abf1b0", size = 321527, upload-time = "2026-09-12T15:12:59.394Z" }, + { url = "https://files.pythonhosted.org/packages/39/71/9e1fd0045f5920b4c36be35c3f0f0dfd123668684f8ad352619d7aa44183/jiter-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cb41cd1432f1dc19a231cf70b54d42b2c9f05085155859263fce06fa4d41388", size = 340865, upload-time = "2026-09-12T15:13:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/14627fd2bc377f3dd09491bcace6b90e34b4d7fea2f1f3295031ff91f528/jiter-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd7790aa79c8b518e512ebcdfce9f11d8ef5f30efd43720c8a19a548b39fa489", size = 325412, upload-time = "2026-09-12T15:13:02.152Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/8d5808f7bf0f456bde79e6393587183a0cee5f83d179fe1f7f1eff2ba067/jiter-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbbfe4e3c21c8166980cddc5bee1a315df082454f007947dfb6fb73800768165", size = 340473, upload-time = "2026-09-12T15:13:03.485Z" }, + { url = "https://files.pythonhosted.org/packages/4f/da/1d8c7c6c4ae6b2423b94a81b6b907d37b28f87664e077427b531bf1b5313/jiter-0.17.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c286860abfe8b100cac1c02e225e5776eb9216edd71ba17cdb237da4af32bc9", size = 350757, upload-time = "2026-09-12T15:13:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/eb/96/c1813dcca15c5a370145a448aaea7d1f83f6f0228a5f1130e79340ee385f/jiter-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f753eb70b1474a29e635e7542ff7312e6d6b951e0b25e8a2e8c34eeb1ddcd478", size = 345203, upload-time = "2026-09-12T15:13:06.131Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f7/fc61cbcf2992d169ede13648fc3fd8e2d3171a3669dde43cd4db556549ac/jiter-0.17.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eae86b1f027031e39db2e0e9c4842221edb7b8cd474d23f87a79b3bd4b651768", size = 328322, upload-time = "2026-09-12T15:13:07.392Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/46418a3abbdffb7dc41b314200360f24f75faaeb35573e81c92de322cce9/jiter-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bf350452a43173e69e1fc74847c57a60e3d7515807287f29849baa2a85d8718", size = 336570, upload-time = "2026-09-12T15:13:08.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/28/b8a55b949be6306df8888e365a8df05441de8a7b11289f6957004302e41e/jiter-0.17.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:da139721f4b7cafdbff580a4f511ea24cb91f4909330c6b926a1ca53836c0a59", size = 482879, upload-time = "2026-09-12T15:13:10.037Z" }, + { url = "https://files.pythonhosted.org/packages/75/3b/21d0afa53ba0680962c39f3eb95ed2946f8793369ed44b0c82b490723081/jiter-0.17.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8079849db9a1371bfd90bad088458a8fb836261879df2233cc9632464ecf64e1", size = 520406, upload-time = "2026-09-12T15:13:11.456Z" }, + { url = "https://files.pythonhosted.org/packages/ef/03/bcbaf8b6b9ea23c2c074411f8ecfbb02d820abac5d0cb8f4e280209174a2/jiter-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:8f770b0c77e5fac482e1ba03ca1a7e18286bfb213d749932a00a7e4cd5de5e06", size = 184434, upload-time = "2026-09-12T15:13:13.037Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b5/5d6ce2c93ef6fe1241b37a9005547f9b6d58db1f07f39fe95807d4b98f51/jiter-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c4289293e5278d9314b00f15c37f2120fa51d3d68565292e715524c750e775a9", size = 227392, upload-time = "2026-09-12T15:13:14.933Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4b/1e52baf90187606e33a7b8cfa8f96f5829acd7f01870077eb01059ab76d0/jiter-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4dfbfe5a6e1e80a7082af559f66386405025ec278833e0c649f69cbc6e1004cc", size = 182776, upload-time = "2026-09-12T15:13:16.239Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3b/05a917204413e2e09906dfa35240c1021227aeb56c7305abea9562c598b4/jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:eaba834b72d573547b9d966465b3394b749d5e14208cc70acb63aca37619ab33", size = 288143, upload-time = "2026-09-12T15:14:02.998Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/a1cd3c0cf8f79945939e4f8caae9990529f67d7f77f671769f73956329b9/jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:51e1519d676a9f14dad9c2a411170d43b022ddb7989562df4e849b261ce127b2", size = 281847, upload-time = "2026-09-12T15:14:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/72/b4/9b797679e09f4a46c32986aeb3670bd9bc562fc0b373c7a0ee5c5dce1206/jiter-0.17.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ce4feb52493e3513335b2accdcd75605652e4632772d3c8c2f7b86954d7f39", size = 304797, upload-time = "2026-09-12T15:14:05.733Z" }, + { url = "https://files.pythonhosted.org/packages/25/4a/0d77415b27a00d970e4e710f7c1de62e96a11c4cab3ed1add0015af04626/jiter-0.17.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29f49b325e0234e4ad9ecca5b861ffbd09b95ccac9bd46fa55841b6e56eea5fe", size = 307815, upload-time = "2026-09-12T15:14:07.105Z" }, + { url = "https://files.pythonhosted.org/packages/17/31/4bb27f54333d3b9ef1e5bd3312dc0b4bbe59c68bb0885fdb40583a6b1567/jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:454c4997d73cc466c71fd565d91e603b0274e48ea0c6b0b7a7aee6967e4ceb7c", size = 288415, upload-time = "2026-09-12T15:14:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/879570ecf82574eaea77c5eb10309f4b630dece5f2a556e9814a90ba3f2d/jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:40d2c240f8f80b5b0f201b29f0ae129c81448c60c772227a41747b5e0026f6a2", size = 279113, upload-time = "2026-09-12T15:14:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/77/7a/1f0b8a35fbd079a4f1752c31a15dc99cf277f863747c459be0af39e900e5/jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e05f5adbf68c4bd11e1610f394034d984152988e84be6f8314235ce6f2139e5", size = 303708, upload-time = "2026-09-12T15:14:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/d76219ebdbcf3d4209d9d21a0810db4c8d0a6f88e3ee87d30bdea4e90d30/jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c0bf24c72fd0491405dce5d40194f2070e9021ce648c1a1d46234b93d848ff", size = 307147, upload-time = "2026-09-12T15:14:12.897Z" }, ] [[package]] @@ -6241,21 +6237,19 @@ wheels = [ [[package]] name = "openai" -version = "2.33.0" +version = "3.35.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, - { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/ee/d056c82f63c05f06baac0cffb4a90952d8274f90c49dfe244f20497b9bbd/openai-2.33.0.tar.gz", hash = "sha256:f850c435e2a4685bba3295bd54912dd26315d9c1b7733068186134d6e0599f9a", size = 693254, upload-time = "2026-04-28T14:04:42.428Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/4c/609b16f2a6e9a99f27a82ce93ff75868e7cb364fe6daff50cc0c1d620772/openai-3.35.1.tar.gz", hash = "sha256:b33e6561b90586e450def8a388e7d364755325da6884fc71348be5542e043bd0", size = 2250942, upload-time = "2026-10-18T01:06:38.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/32/37734d769bc8b42e4938785313cc05aade6cb0fa72479d3220a0d61a4e78/openai-2.33.0-py3-none-any.whl", hash = "sha256:03ac37d70e8c9e3a8124214e3afa785e2cbc12e627fbd98177a086ef2fd87ad5", size = 1162695, upload-time = "2026-04-28T14:04:40.482Z" }, + { url = "https://files.pythonhosted.org/packages/22/7b/6d7e70f0762d1b286051bd7cb2fd0c364c1830d0f8826738673aeec05c2b/openai-3.35.1-py3-none-any.whl", hash = "sha256:264fd0fafc889021f526b7c1e5a2df5ca3e8dea874738ecd31da666434f25967", size = 2280230, upload-time = "2026-10-18T01:06:36.16Z" }, ] [[package]] From 9dc0e71aedd3e5f0696a74769998e42d7f3851d0 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:22:23 -0700 Subject: [PATCH 03/23] test(streaming): cover SDK 3 transport errors on finished streams and file downloads A finished chat stream whose connection drops before [DONE] should end cleanly on both the httpx and httpx2 transports, a mid-stream timeout should fall back as a Timeout, and file_content(stream=True) should raise litellm errors instead of raw transport errors --- .../test_streaming_handler.py | 127 ++++++++++++++++++ tests/unit/llms/openai/test_openai.py | 81 ++++++++++- .../test_openai_file_content_streaming.py | 88 +++++++++++- 3 files changed, 293 insertions(+), 3 deletions(-) diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index 68aabc3c01e..c4a55408179 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -11,6 +11,7 @@ from typing import AsyncIterator, Callable, Final, Iterator, Literal, NoReturn, from unittest.mock import AsyncMock, MagicMock, Mock, patch import httpx +import openai import pytest from openai.types.chat import ChatCompletionChunk from pydantic import BaseModel @@ -4586,6 +4587,132 @@ async def test_transport_read_error_before_finish_reason_raises(logging_obj: Log assert fabricated_finish_reasons == [] +_SDK3_CHAT_REQUEST_URL: Final = "https://api.openai.com/v1/chat/completions" + + +def _raised_from(error: Exception, cause: Exception) -> Exception: + try: + raise error from cause + except Exception as raised: + return raised + + +def _httpx2_read_error() -> Exception: + return pytest.importorskip("httpx2").ReadError("peer closed connection") + + +def _httpx2_protocol_error() -> Exception: + return pytest.importorskip("httpx2").RemoteProtocolError("peer closed connection without sending complete body") + + +def _sdk_wrapped_read_error() -> Exception: + httpx2: Final = pytest.importorskip("httpx2") + return _raised_from( + openai.APIConnectionError(request=httpx2.Request("POST", _SDK3_CHAT_REQUEST_URL)), + httpx2.ReadError("peer closed connection"), + ) + + +def _httpx2_read_timeout() -> Exception: + return pytest.importorskip("httpx2").ReadTimeout("timed out") + + +def _sdk_wrapped_read_timeout() -> Exception: + httpx2: Final = pytest.importorskip("httpx2") + return _raised_from( + openai.APITimeoutError(request=httpx2.Request("POST", _SDK3_CHAT_REQUEST_URL)), + httpx2.ReadTimeout("timed out"), + ) + + +_SDK3_DROPPED_CONNECTIONS: Final = pytest.mark.parametrize( + "dropped_connection", + (_httpx2_read_error, _httpx2_protocol_error, _sdk_wrapped_read_error), + ids=("httpx2_read_error", "httpx2_protocol_error", "sdk_wrapped_read_error"), +) +_SYNC_AND_ASYNC: Final = pytest.mark.parametrize("sync_mode", (True, False), ids=("sync", "async")) + + +def _chunks_then_failure(chunks: Sequence[ModelResponseStream], failure: Exception) -> Iterator[ModelResponseStream]: + yield from chunks + raise failure + + +def _openai_stream_over( + chunks: Sequence[ModelResponseStream], failure: Exception, sync_mode: bool, logging_obj: Logging +) -> CustomStreamWrapper: + stream: Final = _chunks_then_failure(chunks, failure) + return CustomStreamWrapper( + completion_stream=stream if sync_mode else _as_async(stream), + model="openai/stub-model", + custom_llm_provider="openai", + logging_obj=logging_obj, + ) + + +async def _collect(wrapper: CustomStreamWrapper, sync_mode: bool) -> list[ModelResponseStream]: + if sync_mode: + return list(wrapper) + return [chunk async for chunk in wrapper] + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +@_SDK3_DROPPED_CONNECTIONS +async def test_sdk3_dropped_connection_after_finish_reason_ends_the_stream_cleanly( + logging_obj: Logging, sync_mode: bool, dropped_connection: Callable[[], Exception] +): + wrapper: Final = _openai_stream_over( + (_reset_test_chunk(content="Hello"), _reset_test_chunk(finish_reason="stop")), + dropped_connection(), + sync_mode, + logging_obj, + ) + + chunks: Final = await _collect(wrapper, sync_mode) + + assert "".join(chunk.choices[0].delta.content or "" for chunk in chunks if chunk.choices) == "Hello" + assert [chunk.choices[0].finish_reason for chunk in chunks if chunk.choices[0].finish_reason] == ["stop"] + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +@_SDK3_DROPPED_CONNECTIONS +async def test_sdk3_dropped_connection_before_finish_reason_raises_mid_stream_fallback( + logging_obj: Logging, sync_mode: bool, dropped_connection: Callable[[], Exception] +): + wrapper: Final = _openai_stream_over( + (_reset_test_chunk(content="Hel"),), dropped_connection(), sync_mode, logging_obj + ) + + with pytest.raises(MidStreamFallbackError) as excinfo: + await _collect(wrapper, sync_mode) + + assert excinfo.value.is_pre_first_chunk is False + assert excinfo.value.generated_content == "Hel" + assert isinstance(excinfo.value.original_exception, litellm.APIConnectionError) + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +@pytest.mark.parametrize( + "read_timeout", (_httpx2_read_timeout, _sdk_wrapped_read_timeout), ids=("httpx2", "sdk_wrapped_httpx2") +) +@pytest.mark.parametrize("finished", (False, True), ids=("before_finish_reason", "after_finish_reason")) +async def test_sdk3_read_timeout_mid_stream_falls_back_as_a_timeout( + logging_obj: Logging, sync_mode: bool, read_timeout: Callable[[], Exception], finished: bool +): + chunks: Final = (_reset_test_chunk(content="Hel"), _reset_test_chunk(finish_reason="stop")) + wrapper: Final = _openai_stream_over(chunks if finished else chunks[:1], read_timeout(), sync_mode, logging_obj) + + with pytest.raises(MidStreamFallbackError) as excinfo: + await _collect(wrapper, sync_mode) + + assert excinfo.value.is_pre_first_chunk is False + assert isinstance(excinfo.value.original_exception, litellm.Timeout) + assert excinfo.value.original_exception.status_code == 408 + + def test_openai_custom_tool_call_stream_deltas_survive_conversion(logging_obj: Logging): """ Regression test: OpenAI chat completions custom tool calls stream as diff --git a/tests/unit/llms/openai/test_openai.py b/tests/unit/llms/openai/test_openai.py index 11d7c7d6d3c..2fab1286e51 100644 --- a/tests/unit/llms/openai/test_openai.py +++ b/tests/unit/llms/openai/test_openai.py @@ -2,7 +2,7 @@ import asyncio import importlib import json import os -from collections.abc import Mapping +from collections.abc import AsyncIterator, Iterator, Mapping from types import ModuleType from typing import Final, Literal, Protocol from unittest.mock import MagicMock, Mock, patch @@ -32,6 +32,7 @@ from litellm.llms.openai.openai import ( from litellm.types.utils import ( ImageResponse, ModelResponse, + ModelResponseStream, PromptTokensDetails, ) from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER @@ -441,6 +442,84 @@ async def test_acompletion_streams_text_deltas_over_injected_transport( assert last_finish == "stop" +def _sse_event(delta: Mapping[str, str], finish: str | None) -> bytes: + body: Final = { + "id": "chatcmpl-dropped", + "object": "chat.completion.chunk", + "created": 0, + "model": "gpt-5.6", + "choices": [{"index": 0, "delta": delta, "finish_reason": finish}], + } + return f"data: {json.dumps(body)}\n\n".encode() + + +_FINISHED_EVENTS_WITHOUT_DONE: Final = ( + _sse_event({"role": "assistant", "content": "Hel"}, None), + _sse_event({"content": "lo"}, "stop"), +) + + +def _finished_then_dropped_transport(http_backend: ModuleType) -> object: + class FinishedThenDropped(http_backend.SyncByteStream, http_backend.AsyncByteStream): + def __iter__(self) -> Iterator[bytes]: + yield from _FINISHED_EVENTS_WITHOUT_DONE + raise http_backend.ReadError("peer closed the connection before [DONE]") + + async def __aiter__(self) -> AsyncIterator[bytes]: + for event in _FINISHED_EVENTS_WITHOUT_DONE: + yield event + raise http_backend.ReadError("peer closed the connection before [DONE]") + + return http_backend.MockTransport( + lambda _request: http_backend.Response( + 200, headers={"content-type": "text/event-stream"}, stream=FinishedThenDropped() + ) + ) + + +def _streamed_text_and_finish_reasons(chunks: list[ModelResponseStream]) -> tuple[str, list[str]]: + text: Final = "".join(part.choices[0].delta.content or "" for part in chunks if part.choices) + finish_reasons: Final = [part.choices[0].finish_reason for part in chunks if part.choices and part.choices[0].finish_reason] + return text, finish_reasons + + +@pytest.mark.asyncio +async def test_acompletion_ends_a_finished_stream_cleanly_when_the_connection_drops_before_done( + http_backend: ModuleType, +): + async with http_backend.AsyncClient(transport=_finished_then_dropped_transport(http_backend)) as http_client: + stream: Final = await litellm.acompletion( + model="openai/gpt-5.6", + client=AsyncOpenAI(api_key="transport-only", http_client=http_client), + messages=[{"role": "user", "content": "dropped-stream-request"}], + stream=True, + num_retries=0, + max_retries=0, + ) + + async def drain() -> list[ModelResponseStream]: + return [part async for part in stream] + + chunks: Final = await asyncio.wait_for(drain(), timeout=10) + + assert _streamed_text_and_finish_reasons(chunks) == ("Hello", ["stop"]) + + +def test_completion_ends_a_finished_stream_cleanly_when_the_connection_drops_before_done(http_backend: ModuleType): + with http_backend.Client(transport=_finished_then_dropped_transport(http_backend)) as http_client: + stream: Final = litellm.completion( + model="openai/gpt-5.6", + client=OpenAI(api_key="transport-only", http_client=http_client), + messages=[{"role": "user", "content": "dropped-stream-request"}], + stream=True, + num_retries=0, + max_retries=0, + ) + chunks: Final = list(stream) + + assert _streamed_text_and_finish_reasons(chunks) == ("Hello", ["stop"]) + + @pytest.mark.asyncio async def test_acompletion_streams_tool_call_arguments_over_injected_transport(http_backend: ModuleType): outbound: Final = asyncio.Queue() diff --git a/tests/unit/llms/openai/test_openai_file_content_streaming.py b/tests/unit/llms/openai/test_openai_file_content_streaming.py index 466918267f6..1455fd45e67 100644 --- a/tests/unit/llms/openai/test_openai_file_content_streaming.py +++ b/tests/unit/llms/openai/test_openai_file_content_streaming.py @@ -1,6 +1,13 @@ -import pytest -from typing import AsyncIterator, Iterator, cast +import importlib +from types import ModuleType +import pytest +from typing import AsyncIterator, Final, Iterator, cast + +from openai import AsyncOpenAI, OpenAI +from openai._types import Response as SDKResponse + +import litellm from litellm.files import main as files_main from litellm.files.streaming import FileContentStreamingResponse from litellm.files.types import FileContentStreamingResult @@ -323,3 +330,80 @@ def test_file_content_streaming_passes_exception_to_context_manager_exit(): assert response_cm.exc_info[0] is RuntimeError assert response_cm.exc_info[1] is exc_info.value assert response_cm.exc_info[2] is not None + + +@pytest.fixture(params=("httpx", "sdk")) +def http_backend(request: pytest.FixtureRequest) -> ModuleType: + return importlib.import_module("httpx" if request.param == "httpx" else SDKResponse.__module__) + + +_TRANSPORT_FAILURES: Final = pytest.mark.parametrize( + ("failure_name", "public_error"), + ( + ("ReadTimeout", litellm.Timeout), + ("ReadError", litellm.APIConnectionError), + ("RemoteProtocolError", litellm.APIConnectionError), + ), +) + + +def _body_then_failure_transport(http_backend: ModuleType, failure: Exception) -> object: + class BodyThenFailure(http_backend.SyncByteStream, http_backend.AsyncByteStream): + def __iter__(self) -> Iterator[bytes]: + yield b"hello" + raise failure + + async def __aiter__(self) -> AsyncIterator[bytes]: + yield b"hello" + raise failure + + return http_backend.MockTransport( + lambda _request: http_backend.Response( + 200, headers={"content-type": "application/octet-stream"}, stream=BodyThenFailure() + ) + ) + + +@_TRANSPORT_FAILURES +def test_file_content_stream_raises_litellm_errors_when_the_transport_fails_mid_body( + http_backend: ModuleType, failure_name: str, public_error: type[Exception] +): + failure: Final = getattr(http_backend, failure_name)("transport failed mid-body") + + with http_backend.Client(transport=_body_then_failure_transport(http_backend, failure)) as http_client: + result: Final = litellm.file_content( + file_id="file-abc123", + custom_llm_provider="openai", + client=OpenAI(api_key="transport-only", http_client=http_client), + stream=True, + chunk_size=len(b"hello"), + ) + stream: Final = cast(Iterator[bytes], result.stream_iterator) + assert next(stream) == b"hello" + with pytest.raises(public_error) as excinfo: + next(stream) + + assert excinfo.value.__cause__ is failure + + +@pytest.mark.asyncio +@_TRANSPORT_FAILURES +async def test_afile_content_stream_raises_litellm_errors_when_the_transport_fails_mid_body( + http_backend: ModuleType, failure_name: str, public_error: type[Exception] +): + failure: Final = getattr(http_backend, failure_name)("transport failed mid-body") + + async with http_backend.AsyncClient(transport=_body_then_failure_transport(http_backend, failure)) as http_client: + result: Final = await litellm.afile_content( + file_id="file-abc123", + custom_llm_provider="openai", + client=AsyncOpenAI(api_key="transport-only", http_client=http_client), + stream=True, + chunk_size=len(b"hello"), + ) + stream: Final = cast(AsyncIterator[bytes], result.stream_iterator) + assert await stream.__anext__() == b"hello" + with pytest.raises(public_error) as excinfo: + await stream.__anext__() + + assert excinfo.value.__cause__ is failure From 53312cb5f2d6876d0345d34b59e321cadb828706 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:44:47 -0700 Subject: [PATCH 04/23] fix(streaming): end finished streams cleanly on SDK 3 transport errors and map file download errors openai SDK 3 reads over httpx2 and, from 3.25, wraps transport errors in APIConnectionError or APITimeoutError. The stream wrapper only knew httpx, so a connection drop after finish_reason raised MidStreamFallbackError and a read timeout fell back as a 500. Transport errors are now classified across httpx, httpx2 and the SDK's explicit cause chain, a drop after finish_reason ends the stream on both the sync and async paths, and file_content(stream=True) raises litellm.Timeout or litellm.APIConnectionError instead of raw transport errors --- litellm/files/streaming.py | 33 ++++++- .../exception_mapping_utils.py | 3 +- .../litellm_core_utils/streaming_handler.py | 62 +++++++------ litellm/llms/custom_httpx/transport_errors.py | 54 ++++++++++++ .../custom_httpx/test_transport_errors.py | 86 +++++++++++++++++++ 5 files changed, 205 insertions(+), 33 deletions(-) create mode 100644 litellm/llms/custom_httpx/transport_errors.py create mode 100644 tests/unit/llms/custom_httpx/test_transport_errors.py diff --git a/litellm/files/streaming.py b/litellm/files/streaming.py index 460050872f4..6956d3baf32 100644 --- a/litellm/files/streaming.py +++ b/litellm/files/streaming.py @@ -5,7 +5,9 @@ from typing import TYPE_CHECKING, Any, Final, Optional, cast import anyio +from litellm.exceptions import APIConnectionError, Timeout from litellm.files.types import FileContentProvider +from litellm.llms.custom_httpx.transport_errors import TransportErrorKind, chained_transport_error_kind if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import ( @@ -59,8 +61,11 @@ class FileContentStreamingResponse: self._log_success_sync() raise except Exception as e: - self._log_failure_sync(e) - raise + public_error: Final = self._public_error(e) + self._log_failure_sync(public_error) + if public_error is e: + raise + raise public_error from e def __aiter__(self) -> "FileContentStreamingResponse": if not hasattr(self.stream_iterator, "__anext__"): @@ -77,8 +82,28 @@ class FileContentStreamingResponse: await self._log_success_async() raise except Exception as e: - await self._log_failure_async(e) - raise + public_error: Final = self._public_error(e) + await self._log_failure_async(public_error) + if public_error is e: + raise + raise public_error from e + + def _public_error(self, error: Exception) -> Exception: + match chained_transport_error_kind(error): + case TransportErrorKind.TIMEOUT: + return Timeout( + message=f"timed out reading file content: {error}", + model=self.model, + llm_provider=self.custom_llm_provider, + ) + case TransportErrorKind.DROPPED_CONNECTION: + return APIConnectionError( + message=f"connection dropped while reading file content: {error}", + llm_provider=self.custom_llm_provider, + model=self.model, + ) + case None: + return error async def aclose(self) -> None: if self._close_completed: diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index 1f49cc2c4bb..e7f2f5dc359 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -18,6 +18,7 @@ from litellm.litellm_core_utils.bug_report import ( ) from litellm.litellm_core_utils.core_helpers import as_str_mapping from litellm.litellm_core_utils.secret_redaction import redact_string as redact_secret_string +from litellm.llms.custom_httpx.transport_errors import TransportErrorKind, transport_error_classes from litellm.types.utils import LlmProviders from ..exceptions import ( @@ -2551,7 +2552,7 @@ def exception_type( ################################################################################ if ( - isinstance(original_exception, httpx.TimeoutException) + isinstance(original_exception, transport_error_classes(TransportErrorKind.TIMEOUT)) or "Request Timeout Error" in error_str or "Request timed out" in error_str or "Timed out generating response" in error_str diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 6ec426d6da5..045943fdd88 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -35,6 +35,7 @@ from litellm.litellm_core_utils.model_response_utils import ( ) from litellm.litellm_core_utils.redact_messages import LiteLLMLoggingObject from litellm.litellm_core_utils.thread_pool_executor import executor +from litellm.llms.custom_httpx.transport_errors import TransportErrorKind, chained_transport_error_kind from litellm.llms.ollama.common_utils import OllamaResponseIdNormalizer from litellm.types.llms.base import LiteLLMBaseModel from litellm.types.llms.openai import OpenAIChatCompletionChunk @@ -54,7 +55,7 @@ from litellm.types.utils import ( ) from litellm.types.utils import GenericStreamingChunk as GChunk -from ..exceptions import OpenAIError, Timeout +from ..exceptions import OpenAIError from .core_helpers import map_finish_reason, process_response_headers from .exception_mapping_utils import exception_type, with_deployment_api_base from .llm_response_utils.get_api_base import get_api_base @@ -122,12 +123,6 @@ def _delta_from_upstream(upstream_delta: BaseModel | None) -> Delta | None: return None -def _is_transport_timeout(error: BaseException) -> bool: - return isinstance(error, httpx.TimeoutException) or ( - isinstance(error, Timeout) and isinstance(error.__cause__, httpx.TimeoutException) - ) - - def _skips_stream_fallback(status_code: int | None) -> bool: return status_code is not None and 400 <= status_code < 500 and status_code != 429 @@ -2000,7 +1995,7 @@ class CustomStreamWrapper: ): chunk = self.completion_stream else: - chunk = next(self.completion_stream) + chunk = self._next_sync_chunk() if chunk is not None and chunk != b"": print_verbose( f"PROCESSED CHUNK PRE CHUNK CREATOR: {chunk.decode('utf-8', errors='replace') if isinstance(chunk, bytes) else chunk}; custom_llm_provider: {self.custom_llm_provider}" @@ -2162,6 +2157,17 @@ class CustomStreamWrapper: self.close() self._handle_stream_fallback_error(e) + def _next_sync_chunk(self) -> object: + try: + return next(self.completion_stream) + except Exception as e: + if ( + self.received_finish_reason is None + or chained_transport_error_kind(e) is not TransportErrorKind.DROPPED_CONNECTION + ): + raise + raise StopIteration from e + def fetch_sync_stream(self): if self.completion_stream is None and self.make_call is not None: # Call make_call to get the completion stream @@ -2282,26 +2288,26 @@ class CustomStreamWrapper: return processed_chunk except (StopAsyncIteration, StopIteration): return await self._finalize_completed_stream(cache_hit=cache_hit) - except httpx.TimeoutException as e: # if httpx read timeout error occues - traceback_exception = traceback.format_exc() - ## ADD DEBUG INFORMATION - E.G. LITELLM REQUEST TIMEOUT - traceback_exception += f"\nLiteLLM Default Request Timeout - {litellm.request_timeout}" - if self.logging_obj is not None: - self._record_partial_usage_for_failure() - ## LOGGING - self._failure_logging = asyncio.create_task( - self.logging_obj.dispatch_failure_handlers(e, traceback_exception, prefer_async_handlers=True) - ) - await self.aclose() - self._handle_stream_fallback_error(e) - except (httpx.ReadError, httpx.RemoteProtocolError) as e: - if self.received_finish_reason is None: - await self.aclose() - self._log_stream_failure_and_raise(e) - return await self._finalize_completed_stream(cache_hit=cache_hit) except Exception as e: - await self.aclose() - self._log_stream_failure_and_raise(e) + match chained_transport_error_kind(e): + case TransportErrorKind.DROPPED_CONNECTION if self.received_finish_reason is not None: + return await self._finalize_completed_stream(cache_hit=cache_hit) + case TransportErrorKind.TIMEOUT: + traceback_exception: Final = ( + f"{traceback.format_exc()}\nLiteLLM Default Request Timeout - {litellm.request_timeout}" + ) + if self.logging_obj is not None: + self._record_partial_usage_for_failure() + self._failure_logging = asyncio.create_task( + self.logging_obj.dispatch_failure_handlers( + e, traceback_exception, prefer_async_handlers=True + ) + ) + await self.aclose() + self._handle_stream_fallback_error(e) + case _: + await self.aclose() + self._log_stream_failure_and_raise(e) async def _finalize_completed_stream(self, cache_hit: bool) -> "ModelResponseStream": if self.sent_last_chunk is True: @@ -2556,7 +2562,7 @@ class CustomStreamWrapper: # Raise non-retriable client errors directly (skip fallback). # Exception: 429 (rate-limit) IS retriable/transient — allow it # through so the Router can switch to a different model group. - if not _is_transport_timeout(e) and ( + if chained_transport_error_kind(e) is not TransportErrorKind.TIMEOUT and ( _skips_stream_fallback(mapped_status_code) or _skips_stream_fallback(original_status_code) ): raise mapped_exception diff --git a/litellm/llms/custom_httpx/transport_errors.py b/litellm/llms/custom_httpx/transport_errors.py new file mode 100644 index 00000000000..f3a2b04a4ea --- /dev/null +++ b/litellm/llms/custom_httpx/transport_errors.py @@ -0,0 +1,54 @@ +import sys +from collections.abc import Iterator +from enum import Enum +from typing import Final, TypeAlias + +import httpx + + +class TransportErrorKind(Enum): + TIMEOUT = "timeout" + DROPPED_CONNECTION = "dropped_connection" + + +_ErrorTable: TypeAlias = tuple[tuple[type[BaseException], TransportErrorKind], ...] + +_HTTPX_ERRORS: Final[_ErrorTable] = ( + (httpx.TimeoutException, TransportErrorKind.TIMEOUT), + (httpx.ReadError, TransportErrorKind.DROPPED_CONNECTION), + (httpx.RemoteProtocolError, TransportErrorKind.DROPPED_CONNECTION), +) +_MAX_CAUSE_DEPTH: Final = 16 + + +def _transport_errors() -> _ErrorTable: + if "httpx2" not in sys.modules: + return _HTTPX_ERRORS + import httpx2 + + return ( + *_HTTPX_ERRORS, + (httpx2.TimeoutException, TransportErrorKind.TIMEOUT), + (httpx2.ReadError, TransportErrorKind.DROPPED_CONNECTION), + (httpx2.RemoteProtocolError, TransportErrorKind.DROPPED_CONNECTION), + ) + + +def transport_error_kind(error: object) -> TransportErrorKind | None: + return next((kind for error_class, kind in _transport_errors() if isinstance(error, error_class)), None) + + +def transport_error_classes(kind: TransportErrorKind) -> tuple[type[BaseException], ...]: + return tuple(error_class for error_class, error_kind in _transport_errors() if error_kind is kind) + + +def _explicit_causes(error: BaseException | None, depth: int) -> Iterator[BaseException]: + if error is None or depth == 0: + return + yield error + yield from _explicit_causes(error.__cause__, depth - 1) + + +def chained_transport_error_kind(error: BaseException) -> TransportErrorKind | None: + kinds: Final = (transport_error_kind(cause) for cause in _explicit_causes(error, _MAX_CAUSE_DEPTH)) + return next((kind for kind in kinds if kind is not None), None) diff --git a/tests/unit/llms/custom_httpx/test_transport_errors.py b/tests/unit/llms/custom_httpx/test_transport_errors.py new file mode 100644 index 00000000000..92aad0318c8 --- /dev/null +++ b/tests/unit/llms/custom_httpx/test_transport_errors.py @@ -0,0 +1,86 @@ +from typing import Final + +import httpx +import openai +import pytest + +from litellm.llms.custom_httpx.transport_errors import ( + TransportErrorKind, + chained_transport_error_kind, + transport_error_classes, + transport_error_kind, +) + +REQUEST: Final = httpx.Request("POST", "https://api.openai.com/v1/chat/completions") + + +def _raised_from(error: Exception, cause: Exception) -> Exception: + try: + raise error from cause + except Exception as raised: + return raised + + +def _raised_while_handling(error: Exception, handled: Exception) -> Exception: + try: + try: + raise handled + except Exception: + raise error + except Exception as raised: + return raised + + +@pytest.mark.parametrize("module_name", ("httpx", "httpx2")) +@pytest.mark.parametrize( + ("class_name", "expected"), + ( + ("ReadTimeout", TransportErrorKind.TIMEOUT), + ("ConnectTimeout", TransportErrorKind.TIMEOUT), + ("ReadError", TransportErrorKind.DROPPED_CONNECTION), + ("RemoteProtocolError", TransportErrorKind.DROPPED_CONNECTION), + ("ConnectError", None), + ("WriteError", None), + ), +) +def test_transport_error_kind_matches_httpx_and_httpx2_alike( + module_name: str, class_name: str, expected: TransportErrorKind | None +): + module: Final = pytest.importorskip(module_name) + error: Final = getattr(module, class_name)("transport failure") + + assert transport_error_kind(error) is expected + assert chained_transport_error_kind(error) is expected + assert all(isinstance(error, transport_error_classes(kind)) is (kind is expected) for kind in TransportErrorKind) + + +def test_transport_error_kind_ignores_values_that_are_not_transport_errors(): + assert transport_error_kind(ValueError("decoder failure")) is None + assert transport_error_kind({"error": "not an exception"}) is None + + +def test_chained_kind_reads_the_transport_error_an_sdk_error_was_raised_from(): + httpx2: Final = pytest.importorskip("httpx2") + dropped: Final = _raised_from(openai.APIConnectionError(request=REQUEST), httpx2.ReadError("peer closed")) + timed_out: Final = _raised_from(openai.APITimeoutError(request=REQUEST), httpx2.ReadTimeout("timed out")) + twice_wrapped: Final = _raised_from(ValueError("outer"), timed_out) + + assert transport_error_kind(dropped) is None + assert chained_transport_error_kind(dropped) is TransportErrorKind.DROPPED_CONNECTION + assert chained_transport_error_kind(timed_out) is TransportErrorKind.TIMEOUT + assert chained_transport_error_kind(twice_wrapped) is TransportErrorKind.TIMEOUT + assert chained_transport_error_kind(openai.APIConnectionError(request=REQUEST)) is None + + +def test_chained_kind_ignores_an_error_raised_while_handling_a_transport_error(): + contextual: Final = _raised_while_handling(ValueError("bug in a handler"), httpx.ReadError("connection reset")) + + assert contextual.__context__ is not None + assert chained_transport_error_kind(contextual) is None + + +def test_chained_kind_stops_on_a_cause_cycle(): + cyclic: Final = ValueError("caused by itself") + cyclic.__cause__ = cyclic + + assert chained_transport_error_kind(cyclic) is None From 2d862de42d866368191bacc980e66157f81ee742 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:22:23 -0700 Subject: [PATCH 05/23] test(streaming): cover SDK 3 transport errors on finished streams and file downloads A finished chat stream whose connection drops before [DONE] should end cleanly on both the httpx and httpx2 transports, a mid-stream timeout should fall back as a Timeout, and file_content(stream=True) should raise litellm errors instead of raw transport errors --- .../test_streaming_handler.py | 127 ++++++++++++++++++ tests/unit/llms/openai/test_openai.py | 81 ++++++++++- .../test_openai_file_content_streaming.py | 88 +++++++++++- 3 files changed, 293 insertions(+), 3 deletions(-) diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index 68aabc3c01e..c4a55408179 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -11,6 +11,7 @@ from typing import AsyncIterator, Callable, Final, Iterator, Literal, NoReturn, from unittest.mock import AsyncMock, MagicMock, Mock, patch import httpx +import openai import pytest from openai.types.chat import ChatCompletionChunk from pydantic import BaseModel @@ -4586,6 +4587,132 @@ async def test_transport_read_error_before_finish_reason_raises(logging_obj: Log assert fabricated_finish_reasons == [] +_SDK3_CHAT_REQUEST_URL: Final = "https://api.openai.com/v1/chat/completions" + + +def _raised_from(error: Exception, cause: Exception) -> Exception: + try: + raise error from cause + except Exception as raised: + return raised + + +def _httpx2_read_error() -> Exception: + return pytest.importorskip("httpx2").ReadError("peer closed connection") + + +def _httpx2_protocol_error() -> Exception: + return pytest.importorskip("httpx2").RemoteProtocolError("peer closed connection without sending complete body") + + +def _sdk_wrapped_read_error() -> Exception: + httpx2: Final = pytest.importorskip("httpx2") + return _raised_from( + openai.APIConnectionError(request=httpx2.Request("POST", _SDK3_CHAT_REQUEST_URL)), + httpx2.ReadError("peer closed connection"), + ) + + +def _httpx2_read_timeout() -> Exception: + return pytest.importorskip("httpx2").ReadTimeout("timed out") + + +def _sdk_wrapped_read_timeout() -> Exception: + httpx2: Final = pytest.importorskip("httpx2") + return _raised_from( + openai.APITimeoutError(request=httpx2.Request("POST", _SDK3_CHAT_REQUEST_URL)), + httpx2.ReadTimeout("timed out"), + ) + + +_SDK3_DROPPED_CONNECTIONS: Final = pytest.mark.parametrize( + "dropped_connection", + (_httpx2_read_error, _httpx2_protocol_error, _sdk_wrapped_read_error), + ids=("httpx2_read_error", "httpx2_protocol_error", "sdk_wrapped_read_error"), +) +_SYNC_AND_ASYNC: Final = pytest.mark.parametrize("sync_mode", (True, False), ids=("sync", "async")) + + +def _chunks_then_failure(chunks: Sequence[ModelResponseStream], failure: Exception) -> Iterator[ModelResponseStream]: + yield from chunks + raise failure + + +def _openai_stream_over( + chunks: Sequence[ModelResponseStream], failure: Exception, sync_mode: bool, logging_obj: Logging +) -> CustomStreamWrapper: + stream: Final = _chunks_then_failure(chunks, failure) + return CustomStreamWrapper( + completion_stream=stream if sync_mode else _as_async(stream), + model="openai/stub-model", + custom_llm_provider="openai", + logging_obj=logging_obj, + ) + + +async def _collect(wrapper: CustomStreamWrapper, sync_mode: bool) -> list[ModelResponseStream]: + if sync_mode: + return list(wrapper) + return [chunk async for chunk in wrapper] + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +@_SDK3_DROPPED_CONNECTIONS +async def test_sdk3_dropped_connection_after_finish_reason_ends_the_stream_cleanly( + logging_obj: Logging, sync_mode: bool, dropped_connection: Callable[[], Exception] +): + wrapper: Final = _openai_stream_over( + (_reset_test_chunk(content="Hello"), _reset_test_chunk(finish_reason="stop")), + dropped_connection(), + sync_mode, + logging_obj, + ) + + chunks: Final = await _collect(wrapper, sync_mode) + + assert "".join(chunk.choices[0].delta.content or "" for chunk in chunks if chunk.choices) == "Hello" + assert [chunk.choices[0].finish_reason for chunk in chunks if chunk.choices[0].finish_reason] == ["stop"] + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +@_SDK3_DROPPED_CONNECTIONS +async def test_sdk3_dropped_connection_before_finish_reason_raises_mid_stream_fallback( + logging_obj: Logging, sync_mode: bool, dropped_connection: Callable[[], Exception] +): + wrapper: Final = _openai_stream_over( + (_reset_test_chunk(content="Hel"),), dropped_connection(), sync_mode, logging_obj + ) + + with pytest.raises(MidStreamFallbackError) as excinfo: + await _collect(wrapper, sync_mode) + + assert excinfo.value.is_pre_first_chunk is False + assert excinfo.value.generated_content == "Hel" + assert isinstance(excinfo.value.original_exception, litellm.APIConnectionError) + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +@pytest.mark.parametrize( + "read_timeout", (_httpx2_read_timeout, _sdk_wrapped_read_timeout), ids=("httpx2", "sdk_wrapped_httpx2") +) +@pytest.mark.parametrize("finished", (False, True), ids=("before_finish_reason", "after_finish_reason")) +async def test_sdk3_read_timeout_mid_stream_falls_back_as_a_timeout( + logging_obj: Logging, sync_mode: bool, read_timeout: Callable[[], Exception], finished: bool +): + chunks: Final = (_reset_test_chunk(content="Hel"), _reset_test_chunk(finish_reason="stop")) + wrapper: Final = _openai_stream_over(chunks if finished else chunks[:1], read_timeout(), sync_mode, logging_obj) + + with pytest.raises(MidStreamFallbackError) as excinfo: + await _collect(wrapper, sync_mode) + + assert excinfo.value.is_pre_first_chunk is False + assert isinstance(excinfo.value.original_exception, litellm.Timeout) + assert excinfo.value.original_exception.status_code == 408 + + def test_openai_custom_tool_call_stream_deltas_survive_conversion(logging_obj: Logging): """ Regression test: OpenAI chat completions custom tool calls stream as diff --git a/tests/unit/llms/openai/test_openai.py b/tests/unit/llms/openai/test_openai.py index 11d7c7d6d3c..2fab1286e51 100644 --- a/tests/unit/llms/openai/test_openai.py +++ b/tests/unit/llms/openai/test_openai.py @@ -2,7 +2,7 @@ import asyncio import importlib import json import os -from collections.abc import Mapping +from collections.abc import AsyncIterator, Iterator, Mapping from types import ModuleType from typing import Final, Literal, Protocol from unittest.mock import MagicMock, Mock, patch @@ -32,6 +32,7 @@ from litellm.llms.openai.openai import ( from litellm.types.utils import ( ImageResponse, ModelResponse, + ModelResponseStream, PromptTokensDetails, ) from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER @@ -441,6 +442,84 @@ async def test_acompletion_streams_text_deltas_over_injected_transport( assert last_finish == "stop" +def _sse_event(delta: Mapping[str, str], finish: str | None) -> bytes: + body: Final = { + "id": "chatcmpl-dropped", + "object": "chat.completion.chunk", + "created": 0, + "model": "gpt-5.6", + "choices": [{"index": 0, "delta": delta, "finish_reason": finish}], + } + return f"data: {json.dumps(body)}\n\n".encode() + + +_FINISHED_EVENTS_WITHOUT_DONE: Final = ( + _sse_event({"role": "assistant", "content": "Hel"}, None), + _sse_event({"content": "lo"}, "stop"), +) + + +def _finished_then_dropped_transport(http_backend: ModuleType) -> object: + class FinishedThenDropped(http_backend.SyncByteStream, http_backend.AsyncByteStream): + def __iter__(self) -> Iterator[bytes]: + yield from _FINISHED_EVENTS_WITHOUT_DONE + raise http_backend.ReadError("peer closed the connection before [DONE]") + + async def __aiter__(self) -> AsyncIterator[bytes]: + for event in _FINISHED_EVENTS_WITHOUT_DONE: + yield event + raise http_backend.ReadError("peer closed the connection before [DONE]") + + return http_backend.MockTransport( + lambda _request: http_backend.Response( + 200, headers={"content-type": "text/event-stream"}, stream=FinishedThenDropped() + ) + ) + + +def _streamed_text_and_finish_reasons(chunks: list[ModelResponseStream]) -> tuple[str, list[str]]: + text: Final = "".join(part.choices[0].delta.content or "" for part in chunks if part.choices) + finish_reasons: Final = [part.choices[0].finish_reason for part in chunks if part.choices and part.choices[0].finish_reason] + return text, finish_reasons + + +@pytest.mark.asyncio +async def test_acompletion_ends_a_finished_stream_cleanly_when_the_connection_drops_before_done( + http_backend: ModuleType, +): + async with http_backend.AsyncClient(transport=_finished_then_dropped_transport(http_backend)) as http_client: + stream: Final = await litellm.acompletion( + model="openai/gpt-5.6", + client=AsyncOpenAI(api_key="transport-only", http_client=http_client), + messages=[{"role": "user", "content": "dropped-stream-request"}], + stream=True, + num_retries=0, + max_retries=0, + ) + + async def drain() -> list[ModelResponseStream]: + return [part async for part in stream] + + chunks: Final = await asyncio.wait_for(drain(), timeout=10) + + assert _streamed_text_and_finish_reasons(chunks) == ("Hello", ["stop"]) + + +def test_completion_ends_a_finished_stream_cleanly_when_the_connection_drops_before_done(http_backend: ModuleType): + with http_backend.Client(transport=_finished_then_dropped_transport(http_backend)) as http_client: + stream: Final = litellm.completion( + model="openai/gpt-5.6", + client=OpenAI(api_key="transport-only", http_client=http_client), + messages=[{"role": "user", "content": "dropped-stream-request"}], + stream=True, + num_retries=0, + max_retries=0, + ) + chunks: Final = list(stream) + + assert _streamed_text_and_finish_reasons(chunks) == ("Hello", ["stop"]) + + @pytest.mark.asyncio async def test_acompletion_streams_tool_call_arguments_over_injected_transport(http_backend: ModuleType): outbound: Final = asyncio.Queue() diff --git a/tests/unit/llms/openai/test_openai_file_content_streaming.py b/tests/unit/llms/openai/test_openai_file_content_streaming.py index 466918267f6..1455fd45e67 100644 --- a/tests/unit/llms/openai/test_openai_file_content_streaming.py +++ b/tests/unit/llms/openai/test_openai_file_content_streaming.py @@ -1,6 +1,13 @@ -import pytest -from typing import AsyncIterator, Iterator, cast +import importlib +from types import ModuleType +import pytest +from typing import AsyncIterator, Final, Iterator, cast + +from openai import AsyncOpenAI, OpenAI +from openai._types import Response as SDKResponse + +import litellm from litellm.files import main as files_main from litellm.files.streaming import FileContentStreamingResponse from litellm.files.types import FileContentStreamingResult @@ -323,3 +330,80 @@ def test_file_content_streaming_passes_exception_to_context_manager_exit(): assert response_cm.exc_info[0] is RuntimeError assert response_cm.exc_info[1] is exc_info.value assert response_cm.exc_info[2] is not None + + +@pytest.fixture(params=("httpx", "sdk")) +def http_backend(request: pytest.FixtureRequest) -> ModuleType: + return importlib.import_module("httpx" if request.param == "httpx" else SDKResponse.__module__) + + +_TRANSPORT_FAILURES: Final = pytest.mark.parametrize( + ("failure_name", "public_error"), + ( + ("ReadTimeout", litellm.Timeout), + ("ReadError", litellm.APIConnectionError), + ("RemoteProtocolError", litellm.APIConnectionError), + ), +) + + +def _body_then_failure_transport(http_backend: ModuleType, failure: Exception) -> object: + class BodyThenFailure(http_backend.SyncByteStream, http_backend.AsyncByteStream): + def __iter__(self) -> Iterator[bytes]: + yield b"hello" + raise failure + + async def __aiter__(self) -> AsyncIterator[bytes]: + yield b"hello" + raise failure + + return http_backend.MockTransport( + lambda _request: http_backend.Response( + 200, headers={"content-type": "application/octet-stream"}, stream=BodyThenFailure() + ) + ) + + +@_TRANSPORT_FAILURES +def test_file_content_stream_raises_litellm_errors_when_the_transport_fails_mid_body( + http_backend: ModuleType, failure_name: str, public_error: type[Exception] +): + failure: Final = getattr(http_backend, failure_name)("transport failed mid-body") + + with http_backend.Client(transport=_body_then_failure_transport(http_backend, failure)) as http_client: + result: Final = litellm.file_content( + file_id="file-abc123", + custom_llm_provider="openai", + client=OpenAI(api_key="transport-only", http_client=http_client), + stream=True, + chunk_size=len(b"hello"), + ) + stream: Final = cast(Iterator[bytes], result.stream_iterator) + assert next(stream) == b"hello" + with pytest.raises(public_error) as excinfo: + next(stream) + + assert excinfo.value.__cause__ is failure + + +@pytest.mark.asyncio +@_TRANSPORT_FAILURES +async def test_afile_content_stream_raises_litellm_errors_when_the_transport_fails_mid_body( + http_backend: ModuleType, failure_name: str, public_error: type[Exception] +): + failure: Final = getattr(http_backend, failure_name)("transport failed mid-body") + + async with http_backend.AsyncClient(transport=_body_then_failure_transport(http_backend, failure)) as http_client: + result: Final = await litellm.afile_content( + file_id="file-abc123", + custom_llm_provider="openai", + client=AsyncOpenAI(api_key="transport-only", http_client=http_client), + stream=True, + chunk_size=len(b"hello"), + ) + stream: Final = cast(AsyncIterator[bytes], result.stream_iterator) + assert await stream.__anext__() == b"hello" + with pytest.raises(public_error) as excinfo: + await stream.__anext__() + + assert excinfo.value.__cause__ is failure From 7cbead868843f6ae6950cb7447106773723869a4 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:46:03 -0700 Subject: [PATCH 06/23] ci(openai-compat): install the matrix's exact openai version past the lock's openai override --- .github/workflows/test-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index c593acbe8fb..aae3a607168 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -1340,7 +1340,7 @@ jobs: if: steps.changes.outputs.decision != 'skip' run: | .github/scripts/uv_sync_with_retries.sh --frozen --group dev --group proxy-dev --extra proxy --no-install-workspace - uv pip install "openai==${OPENAI_VERSION:?}" --exclude-newer-package openai=2026-10-07T00:00:00Z + uv pip install "openai==${OPENAI_VERSION:?}" --override <(echo "openai==${OPENAI_VERSION:?}") --exclude-newer-package openai=2026-10-07T00:00:00Z uv run --no-sync python -c 'import os, openai; assert openai.__version__ == os.environ["OPENAI_VERSION"], openai.__version__' - name: Run OpenAI SDK compatibility tests From 6517ef7d6421adfcec5d1eedb466785718f182ec Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:07:57 -0700 Subject: [PATCH 07/23] fix(openai): handle SDK 3 error shapes, audio deltas, Azure deployment URLs, and httpx2 token exchange errors OpenAI SDK 3 sends HTTP through httpx2, rejects an empty api_key, and ships new Responses error codes and types. This maps the new error codes and the SDK 3 missing-credentials message, keeps the raw upstream error code, copies audio deltas without unset fields, strips operation paths from full Azure deployment URLs, closes the websocket passthrough cleanly on httpx2 token exchange errors, and replaces the recursive cause walk with a bounded loop. Tests route the SDK 3 workload identity token exchange through respx and build router clients SDK 3 accepts. The lazy OpenAPI snapshot and dashboard API types are regenerated --- .../exception_mapping_utils.py | 9 +- .../litellm_core_utils/streaming_handler.py | 7 +- litellm/llms/azure/common_utils.py | 18 +- litellm/llms/custom_httpx/transport_errors.py | 17 +- litellm/proxy/_lazy_openapi_snapshot.json | 1151 ++++++++++++++++- .../common_utils/responses_stream_errors.py | 2 +- .../llm_passthrough_endpoints.py | 3 +- litellm/responses/streaming_iterator.py | 3 + tests/unit/conftest.py | 28 + .../test_exception_mapping_utils.py | 6 +- .../chat/test_azure_ai_transformation.py | 5 + .../openai/test_openai_workload_identity.py | 17 +- .../openai/test_moderations.py | 2 + .../test_llm_pass_through_endpoints.py | 2 +- .../test_openai_ws_passthrough_routes.py | 2 +- tests/unit/realtime_api/test_main.py | 4 +- .../test_streaming_iterator_error_events.py | 3 + tests/unit/test_router/test_router.py | 2 +- .../test_router_provider_endpoints.py | 9 +- ui/litellm-dashboard/src/lib/http/schema.d.ts | 929 +++++++++++-- 20 files changed, 2062 insertions(+), 157 deletions(-) diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index e7f2f5dc359..10db9c3b438 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -271,6 +271,10 @@ class _ProviderHTTPException(Protocol): _ERROR_BODY_ADAPTER: Final = TypeAdapter(dict[str, object]) +_OPENAI_CLIENT_MISSING_CREDENTIALS_MESSAGES: Final = ( + "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable", + "Missing credentials. Please pass an `api_key`", +) def _openai_cyber_policy_body(original_exception: _ProviderHTTPException) -> Mapping[str, object] | None: @@ -439,10 +443,7 @@ def _map_openai_exception( litellm_debug_info=extra_information, body=getattr(original_exception, "body", None), ) - elif ( - "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable" - in error_str - ): + elif any(marker in error_str for marker in _OPENAI_CLIENT_MISSING_CREDENTIALS_MESSAGES): raise AuthenticationError( message=f"AuthenticationError: {exception_provider} - {message}", llm_provider=custom_llm_provider, diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 045943fdd88..edb997173b7 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -1056,7 +1056,12 @@ class CustomStreamWrapper: """ Copy a specific attribute from source delta to target delta. """ - setattr(target_delta, attribute_name, getattr(source_delta, attribute_name)) + value: Final = stored_attribute(source_delta, attribute_name) + setattr( + target_delta, + attribute_name, + value.model_dump(exclude_unset=True) if isinstance(value, BaseModel) else value, + ) def _has_any_special_delta_attributes(self, delta: object) -> bool: """ diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index 52e3cdf380e..2d864fa7a08 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -8,6 +8,7 @@ from contextlib import closing from functools import lru_cache from types import MappingProxyType from typing import Final, Literal, NamedTuple, cast +from urllib.parse import urlsplit, urlunsplit import httpx from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI @@ -326,13 +327,26 @@ def get_azure_ad_token_from_oidc( return azure_ad_token_access_token +_DEPLOYMENT_OPERATION_PATH: Final = re.compile( + r"(?P.*/openai/deployments/[^/]+)" + r"/(?:chat/completions|completions|embeddings|audio/(?:transcriptions|translations|speech)|images/(?:generations|edits))/?" +) + + +def _azure_deployment_base_url(deployment_url: str) -> str: + parts: Final = urlsplit(deployment_url) + operation_path: Final = _DEPLOYMENT_OPERATION_PATH.fullmatch(parts.path) + deployment_path: Final = parts.path if operation_path is None else operation_path["deployment"] + return urlunsplit(parts._replace(path=deployment_path, query="", fragment="")) + + def select_azure_base_url_or_endpoint(azure_client_params: dict): azure_endpoint: Final = azure_client_params.get("azure_endpoint", None) - if azure_endpoint is not None: + if isinstance(azure_endpoint, str): # see : https://github.com/openai/openai-python/blob/3d61ed42aba652b547029095a7eb269ad4e1e957/src/openai/lib/azure.py#L192 if "/openai/deployments" in azure_endpoint: # this is base_url, not an azure_endpoint - azure_client_params["base_url"] = azure_endpoint + azure_client_params["base_url"] = _azure_deployment_base_url(azure_endpoint) azure_client_params.pop("azure_endpoint") return azure_client_params diff --git a/litellm/llms/custom_httpx/transport_errors.py b/litellm/llms/custom_httpx/transport_errors.py index f3a2b04a4ea..20c4c0aadc4 100644 --- a/litellm/llms/custom_httpx/transport_errors.py +++ b/litellm/llms/custom_httpx/transport_errors.py @@ -1,6 +1,9 @@ import sys from collections.abc import Iterator from enum import Enum +from functools import partial +from itertools import accumulate, takewhile +from operator import is_not from typing import Final, TypeAlias import httpx @@ -42,13 +45,15 @@ def transport_error_classes(kind: TransportErrorKind) -> tuple[type[BaseExceptio return tuple(error_class for error_class, error_kind in _transport_errors() if error_kind is kind) -def _explicit_causes(error: BaseException | None, depth: int) -> Iterator[BaseException]: - if error is None or depth == 0: - return - yield error - yield from _explicit_causes(error.__cause__, depth - 1) +def _next_cause(error: BaseException | None, _depth: int) -> BaseException | None: + return None if error is None else error.__cause__ + + +def _explicit_causes(error: BaseException) -> Iterator[BaseException | None]: + causes: Final = accumulate(range(1, _MAX_CAUSE_DEPTH), _next_cause, initial=error) + return takewhile(partial(is_not, None), causes) def chained_transport_error_kind(error: BaseException) -> TransportErrorKind | None: - kinds: Final = (transport_error_kind(cause) for cause in _explicit_causes(error, _MAX_CAUSE_DEPTH)) + kinds: Final = (transport_error_kind(cause) for cause in _explicit_causes(error)) return next((kind for kind in kinds if kind is not None), None) diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index 545b317055a..2334286dbde 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -22556,8 +22556,15 @@ "title": "Queries" }, "query": { - "title": "Query", - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Query" }, "sources": { "anyOf": [ @@ -22580,7 +22587,6 @@ } }, "required": [ - "query", "type" ], "title": "ActionSearch", @@ -22644,6 +22650,98 @@ "title": "ActionWait", "type": "object" }, + "AdditionalTools": { + "additionalProperties": true, + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "role": { + "enum": [ + "unknown", + "user", + "assistant", + "system", + "critic", + "discriminator", + "developer", + "tool" + ], + "title": "Role", + "type": "string" + }, + "tools": { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/FunctionTool" + }, + { + "$ref": "#/components/schemas/FileSearchTool" + }, + { + "$ref": "#/components/schemas/ComputerTool" + }, + { + "$ref": "#/components/schemas/ComputerUsePreviewTool" + }, + { + "$ref": "#/components/schemas/WebSearchTool" + }, + { + "$ref": "#/components/schemas/Mcp" + }, + { + "$ref": "#/components/schemas/CodeInterpreter" + }, + { + "$ref": "#/components/schemas/ProgrammaticToolCalling" + }, + { + "$ref": "#/components/schemas/ImageGeneration" + }, + { + "$ref": "#/components/schemas/LocalShell" + }, + { + "$ref": "#/components/schemas/FunctionShellTool" + }, + { + "$ref": "#/components/schemas/CustomTool" + }, + { + "$ref": "#/components/schemas/NamespaceTool" + }, + { + "$ref": "#/components/schemas/ToolSearchTool" + }, + { + "$ref": "#/components/schemas/WebSearchPreviewTool" + }, + { + "$ref": "#/components/schemas/ApplyPatchTool" + } + ] + }, + "title": "Tools", + "type": "array" + }, + "type": { + "const": "additional_tools", + "title": "Type", + "type": "string" + } + }, + "required": [ + "id", + "role", + "tools", + "type" + ], + "title": "AdditionalTools", + "type": "object" + }, "AnnotationContainerFileCitation": { "additionalProperties": true, "description": "A citation for a container file used to generate a model response.", @@ -22782,6 +22880,24 @@ "additionalProperties": true, "description": "Allows the assistant to create, delete, or update files using unified diffs.", "properties": { + "allowed_callers": { + "anyOf": [ + { + "items": { + "enum": [ + "direct", + "programmatic" + ], + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Callers" + }, "type": { "const": "apply_patch", "title": "Type", @@ -22899,6 +23015,41 @@ "title": "CachedTokensDetails", "type": "object" }, + "CallerDirect": { + "additionalProperties": true, + "properties": { + "type": { + "const": "direct", + "title": "Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "CallerDirect", + "type": "object" + }, + "CallerProgram": { + "additionalProperties": true, + "properties": { + "caller_id": { + "title": "Caller Id", + "type": "string" + }, + "type": { + "const": "program", + "title": "Type", + "type": "string" + } + }, + "required": [ + "caller_id", + "type" + ], + "title": "CallerProgram", + "type": "object" + }, "Click": { "additionalProperties": true, "description": "A click action.", @@ -22955,6 +23106,24 @@ "additionalProperties": true, "description": "A tool that runs Python code to help generate a response to a prompt.", "properties": { + "allowed_callers": { + "anyOf": [ + { + "items": { + "enum": [ + "direct", + "programmatic" + ], + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Callers" + }, "container": { "anyOf": [ { @@ -23132,7 +23301,7 @@ }, "ComputerTool": { "additionalProperties": true, - "description": "A tool that controls a virtual computer.\n\nLearn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use).", + "description": "A tool that controls a virtual computer.\n\nLearn more about the [computer tool](https://developers.openai.com/api/docs/guides/tools-computer-use).", "properties": { "type": { "const": "computer", @@ -23148,7 +23317,7 @@ }, "ComputerUsePreviewTool": { "additionalProperties": true, - "description": "A tool that controls a virtual computer.\n\nLearn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use).", + "description": "A tool that controls a virtual computer.\n\nLearn more about the [computer tool](https://developers.openai.com/api/docs/guides/tools-computer-use).", "properties": { "display_height": { "title": "Display Height", @@ -23384,8 +23553,37 @@ }, "CustomTool": { "additionalProperties": true, - "description": "A custom tool that processes input using a specified format.\n\nLearn more about [custom tools](https://platform.openai.com/docs/guides/function-calling#custom-tools)", + "description": "A custom tool that processes input using a specified format.\n\nLearn more about [custom tools](https://developers.openai.com/api/docs/guides/function-calling#custom-tools)", "properties": { + "allowed_callers": { + "anyOf": [ + { + "items": { + "enum": [ + "direct", + "programmatic" + ], + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Callers" + }, + "async": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Async" + }, "defer_loading": { "anyOf": [ { @@ -23665,7 +23863,7 @@ }, "FileSearchTool": { "additionalProperties": true, - "description": "A tool that searches for relevant content from uploaded files.\n\nLearn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search).", + "description": "A tool that searches for relevant content from uploaded files.\n\nLearn more about the [file search tool](https://developers.openai.com/api/docs/guides/tools-file-search).", "properties": { "filters": { "anyOf": [ @@ -23748,6 +23946,24 @@ "additionalProperties": true, "description": "A tool that allows the model to execute shell commands.", "properties": { + "allowed_callers": { + "anyOf": [ + { + "items": { + "enum": [ + "direct", + "programmatic" + ], + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Callers" + }, "environment": { "anyOf": [ { @@ -23779,8 +23995,37 @@ }, "FunctionTool": { "additionalProperties": true, - "description": "Defines a function in your own code the model can choose to call.\n\nLearn more about [function calling](https://platform.openai.com/docs/guides/function-calling).", + "description": "Defines a function in your own code the model can choose to call.\n\nLearn more about [function calling](https://developers.openai.com/api/docs/guides/function-calling).", "properties": { + "allowed_callers": { + "anyOf": [ + { + "items": { + "enum": [ + "direct", + "programmatic" + ], + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Callers" + }, + "async": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Async" + }, "defer_loading": { "anyOf": [ { @@ -23807,6 +24052,18 @@ "title": "Name", "type": "string" }, + "output_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Output Schema" + }, "parameters": { "anyOf": [ { @@ -23996,6 +24253,31 @@ "title": "Grammar", "type": "object" }, + "HTTPError": { + "additionalProperties": true, + "properties": { + "code": { + "title": "Code", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "type": { + "const": "http_error", + "title": "Type", + "type": "string" + } + }, + "required": [ + "code", + "message", + "type" + ], + "title": "HTTPError", + "type": "object" + }, "HTTPValidationError": { "properties": { "detail": { @@ -24079,7 +24361,14 @@ "enum": [ "gpt-image-1", "gpt-image-1-mini", - "gpt-image-1.5" + "gpt-image-2", + "gpt-image-2-2026-04-21", + "gpt-image-2.5-sunburst", + "gpt-image-2.5-sunburst-2026-09-08", + "gpt-image-2.5-flare", + "gpt-image-2.5-flare-2026-09-08", + "gpt-image-1.5", + "chatgpt-image-latest" ], "type": "string" }, @@ -24149,6 +24438,8 @@ "low", "medium", "high", + "xhigh", + "max", "auto" ], "type": "string" @@ -24161,6 +24452,9 @@ }, "size": { "anyOf": [ + { + "type": "string" + }, { "enum": [ "1024x1024", @@ -24192,10 +24486,87 @@ "additionalProperties": true, "description": "An image generation request made by the model.", "properties": { + "action": { + "anyOf": [ + { + "enum": [ + "generate", + "edit", + "auto" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Action" + }, + "background": { + "anyOf": [ + { + "enum": [ + "transparent", + "opaque", + "auto" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Background" + }, + "error": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageGenerationCallError" + }, + { + "type": "null" + } + ] + }, "id": { "title": "Id", "type": "string" }, + "output_format": { + "anyOf": [ + { + "enum": [ + "png", + "webp", + "jpeg" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Output Format" + }, + "quality": { + "anyOf": [ + { + "enum": [ + "low", + "medium", + "high", + "xhigh", + "max", + "auto" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Quality" + }, "result": { "anyOf": [ { @@ -24207,6 +24578,36 @@ ], "title": "Result" }, + "revised_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Revised Prompt" + }, + "size": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": [ + "1024x1024", + "1024x1536", + "1536x1024" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Size" + }, "status": { "enum": [ "in_progress", @@ -24231,6 +24632,86 @@ "title": "ImageGenerationCall", "type": "object" }, + "ImageGenerationCallError": { + "additionalProperties": true, + "description": "Details of a moderation failure, when available.", + "properties": { + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Code" + }, + "message": { + "title": "Message", + "type": "string" + }, + "moderation_details": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageGenerationCallErrorModerationDetails" + }, + { + "type": "null" + } + ] + }, + "param": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Param" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "message", + "type" + ], + "title": "ImageGenerationCallError", + "type": "object" + }, + "ImageGenerationCallErrorModerationDetails": { + "additionalProperties": true, + "description": "Public details for a `moderation_blocked` error, when available.", + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Categories", + "type": "array" + }, + "moderation_stage": { + "enum": [ + "input", + "output", + "unknown" + ], + "title": "Moderation Stage", + "type": "string" + } + }, + "required": [ + "categories", + "moderation_stage" + ], + "title": "ImageGenerationCallErrorModerationDetails", + "type": "object" + }, "ImageGenerationInputImageMask": { "additionalProperties": true, "description": "Optional mask for inpainting.\n\nContains `image_url`\n(string, optional) and `file_id` (string, optional).", @@ -24270,7 +24751,9 @@ { "enum": [ "max_output_tokens", - "content_filter" + "max_messages", + "content_filter", + "steered" ], "type": "string" }, @@ -24712,8 +25195,26 @@ }, "Mcp": { "additionalProperties": true, - "description": "Give the model access to additional tools via remote Model Context Protocol\n(MCP) servers. [Learn more about MCP](https://platform.openai.com/docs/guides/tools-remote-mcp).", + "description": "Give the model access to additional tools via remote Model Context Protocol\n(MCP) servers. [Learn more about MCP](https://developers.openai.com/api/docs/guides/tools-connectors-mcp).", "properties": { + "allowed_callers": { + "anyOf": [ + { + "items": { + "enum": [ + "direct", + "programmatic" + ], + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Callers" + }, "allowed_tools": { "anyOf": [ { @@ -24832,6 +25333,17 @@ ], "title": "Server Url" }, + "tunnel_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tunnel Id" + }, "type": { "const": "mcp", "title": "Type", @@ -24978,7 +25490,13 @@ "error": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/McpProtocolError" + }, + { + "$ref": "#/components/schemas/McpToolExecutionError" + }, + { + "$ref": "#/components/schemas/HTTPError" }, { "type": "null" @@ -25127,6 +25645,31 @@ "title": "McpListToolsTool", "type": "object" }, + "McpProtocolError": { + "additionalProperties": true, + "properties": { + "code": { + "title": "Code", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "type": { + "const": "mcp_protocol_error", + "title": "Type", + "type": "string" + } + }, + "required": [ + "code", + "message", + "type" + ], + "title": "McpProtocolError", + "type": "object" + }, "McpRequireApprovalMcpToolApprovalFilter": { "additionalProperties": true, "description": "Specify which of the MCP server's tools require approval.\n\nCan be\n`always`, `never`, or a filter object associated with tools\nthat require approval.", @@ -25221,6 +25764,25 @@ "title": "McpRequireApprovalMcpToolApprovalFilterNever", "type": "object" }, + "McpToolExecutionError": { + "additionalProperties": true, + "properties": { + "content": { + "title": "Content" + }, + "type": { + "const": "mcp_tool_execution_error", + "title": "Type", + "type": "string" + } + }, + "required": [ + "content", + "type" + ], + "title": "McpToolExecutionError", + "type": "object" + }, "Move": { "additionalProperties": true, "description": "A mouse move action.", @@ -25908,6 +26470,111 @@ "title": "PendingSafetyCheck", "type": "object" }, + "Program": { + "additionalProperties": true, + "properties": { + "call_id": { + "title": "Call Id", + "type": "string" + }, + "code": { + "title": "Code", + "type": "string" + }, + "fingerprint": { + "title": "Fingerprint", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "type": { + "const": "program", + "title": "Type", + "type": "string" + } + }, + "required": [ + "id", + "call_id", + "code", + "fingerprint", + "type" + ], + "title": "Program", + "type": "object" + }, + "ProgramOutput": { + "additionalProperties": true, + "properties": { + "call_id": { + "title": "Call Id", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "result": { + "title": "Result", + "type": "string" + }, + "status": { + "enum": [ + "completed", + "incomplete" + ], + "title": "Status", + "type": "string" + }, + "type": { + "const": "program_output", + "title": "Type", + "type": "string" + } + }, + "required": [ + "id", + "call_id", + "result", + "status", + "type" + ], + "title": "ProgramOutput", + "type": "object" + }, + "ProgrammaticToolCalling": { + "additionalProperties": true, + "properties": { + "type": { + "const": "programmatic_tool_calling", + "title": "Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ProgrammaticToolCalling", + "type": "object" + }, + "PromptCacheBreakpoint": { + "additionalProperties": true, + "description": "Marks the exact end of a reusable prompt prefix.\n\nThe breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block.", + "properties": { + "mode": { + "const": "explicit", + "title": "Mode", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "PromptCacheBreakpoint", + "type": "object" + }, "RankingOptions": { "additionalProperties": true, "description": "Ranking options for search.", @@ -26029,6 +26696,34 @@ "title": "RealtimeTranscriptionSessionResponse", "type": "object" }, + "Reasoning": { + "additionalProperties": true, + "description": "The reasoning configuration applied by this update.", + "properties": { + "effort": { + "anyOf": [ + { + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Effort" + } + }, + "title": "Reasoning", + "type": "object" + }, "ResponseAPIUsage": { "additionalProperties": true, "properties": { @@ -26092,6 +26787,20 @@ "title": "Call Id", "type": "string" }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" + }, "created_by": { "anyOf": [ { @@ -26153,6 +26862,20 @@ "title": "Call Id", "type": "string" }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" + }, "created_by": { "anyOf": [ { @@ -26274,7 +26997,7 @@ }, "ResponseCompactionItem": { "additionalProperties": true, - "description": "A compaction item generated by the [`v1/responses/compact` API](https://platform.openai.com/docs/api-reference/responses/compact).", + "description": "A compaction item generated by the [`v1/responses/compact` API](https://developers.openai.com/api/reference/resources/responses/methods/compact).", "properties": { "created_by": { "anyOf": [ @@ -26311,7 +27034,7 @@ }, "ResponseComputerToolCall": { "additionalProperties": true, - "description": "A tool call to a computer use tool.\n\nSee the\n[computer use guide](https://platform.openai.com/docs/guides/tools-computer-use) for more information.", + "description": "A tool call to a computer use tool.\n\nSee the\n[computer use guide](https://developers.openai.com/api/docs/guides/tools-computer-use) for more information.", "properties": { "action": { "anyOf": [ @@ -26533,6 +27256,37 @@ "title": "ResponseComputerToolCallOutputScreenshot", "type": "object" }, + "ResponseConfigurationUpdateItem": { + "additionalProperties": true, + "description": "A configuration update that applies to subsequent responses until it is\nreplaced by another configuration update.", + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "reasoning": { + "anyOf": [ + { + "$ref": "#/components/schemas/Reasoning" + }, + { + "type": "null" + } + ] + }, + "type": { + "const": "configuration_update", + "title": "Type", + "type": "string" + } + }, + "required": [ + "id", + "type" + ], + "title": "ResponseConfigurationUpdateItem", + "type": "object" + }, "ResponseContainerReference": { "additionalProperties": true, "description": "Represents a container created with /v1/containers.", @@ -26558,10 +27312,35 @@ "additionalProperties": true, "description": "A call to a custom tool created by the model.", "properties": { + "async": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Async" + }, "call_id": { "title": "Call Id", "type": "string" }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" + }, "id": { "anyOf": [ { @@ -26611,10 +27390,35 @@ "additionalProperties": true, "description": "A call to a custom tool created by the model.", "properties": { + "async": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Async" + }, "call_id": { "title": "Call Id", "type": "string" }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" + }, "created_by": { "anyOf": [ { @@ -26683,6 +27487,20 @@ "title": "Call Id", "type": "string" }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" + }, "created_by": { "anyOf": [ { @@ -26749,7 +27567,7 @@ }, "ResponseFileSearchToolCall": { "additionalProperties": true, - "description": "The results of a file search tool call.\n\nSee the\n[file search guide](https://platform.openai.com/docs/guides/tools-file-search) for more information.", + "description": "The results of a file search tool call.\n\nSee the\n[file search guide](https://developers.openai.com/api/docs/guides/tools-file-search) for more information.", "properties": { "id": { "title": "Id", @@ -26836,7 +27654,7 @@ }, "ResponseFormatTextJSONSchemaConfigParam": { "additionalProperties": true, - "description": "JSON Schema response format.\n\nUsed to generate structured JSON responses.\nLearn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs).", + "description": "JSON Schema response format.\n\nUsed to generate structured JSON responses.\nLearn more about [Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs).", "properties": { "description": { "title": "Description", @@ -26887,6 +27705,20 @@ "title": "Call Id", "type": "string" }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" + }, "created_by": { "anyOf": [ { @@ -26949,6 +27781,20 @@ "title": "Call Id", "type": "string" }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" + }, "created_by": { "anyOf": [ { @@ -27009,16 +27855,41 @@ }, "ResponseFunctionToolCall": { "additionalProperties": true, - "description": "A tool call to run a function.\n\nSee the\n[function calling guide](https://platform.openai.com/docs/guides/function-calling) for more information.", + "description": "A tool call to run a function.\n\nSee the\n[function calling guide](https://developers.openai.com/api/docs/guides/function-calling) for more information.", "properties": { "arguments": { "title": "Arguments", "type": "string" }, + "async": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Async" + }, "call_id": { "title": "Call Id", "type": "string" }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" + }, "id": { "anyOf": [ { @@ -27078,16 +27949,41 @@ }, "ResponseFunctionToolCallItem": { "additionalProperties": true, - "description": "A tool call to run a function.\n\nSee the\n[function calling guide](https://platform.openai.com/docs/guides/function-calling) for more information.", + "description": "A tool call to run a function.\n\nSee the\n[function calling guide](https://developers.openai.com/api/docs/guides/function-calling) for more information.", "properties": { "arguments": { "title": "Arguments", "type": "string" }, + "async": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Async" + }, "call_id": { "title": "Call Id", "type": "string" }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" + }, "created_by": { "anyOf": [ { @@ -27148,8 +28044,29 @@ "additionalProperties": true, "properties": { "call_id": { - "title": "Call Id", - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Call Id" + }, + "caller": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallerDirect" + }, + { + "$ref": "#/components/schemas/CallerProgram" + }, + { + "type": "null" + } + ], + "title": "Caller" }, "created_by": { "anyOf": [ @@ -27166,6 +28083,28 @@ "title": "Id", "type": "string" }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Namespace" + }, "output": { "anyOf": [ { @@ -27207,7 +28146,6 @@ }, "required": [ "id", - "call_id", "output", "status", "type" @@ -27217,7 +28155,7 @@ }, "ResponseFunctionWebSearch": { "additionalProperties": true, - "description": "The results of a web search tool call.\n\nSee the\n[web search guide](https://platform.openai.com/docs/guides/tools-web-search) for more information.", + "description": "The results of a web search tool call.\n\nSee the\n[web search guide](https://developers.openai.com/api/docs/guides/tools-web-search) for more information.", "properties": { "action": { "anyOf": [ @@ -27242,7 +28180,8 @@ "in_progress", "searching", "completed", - "failed" + "failed", + "incomplete" ], "title": "Status", "type": "string" @@ -27270,8 +28209,9 @@ "anyOf": [ { "enum": [ - "high", - "low" + "auto", + "low", + "high" ], "type": "string" }, @@ -27325,6 +28265,16 @@ ], "title": "Filename" }, + "prompt_cache_breakpoint": { + "anyOf": [ + { + "$ref": "#/components/schemas/PromptCacheBreakpoint" + }, + { + "type": "null" + } + ] + }, "type": { "const": "input_file", "title": "Type", @@ -27339,7 +28289,7 @@ }, "ResponseInputImage": { "additionalProperties": true, - "description": "An image input to the model.\n\nLearn about [image inputs](https://platform.openai.com/docs/guides/vision).", + "description": "An image input to the model.\n\nLearn about [image inputs](https://developers.openai.com/api/docs/guides/images-vision).", "properties": { "detail": { "enum": [ @@ -27373,6 +28323,16 @@ ], "title": "Image Url" }, + "prompt_cache_breakpoint": { + "anyOf": [ + { + "$ref": "#/components/schemas/PromptCacheBreakpoint" + }, + { + "type": "null" + } + ] + }, "type": { "const": "input_image", "title": "Type", @@ -27454,6 +28414,16 @@ "additionalProperties": true, "description": "A text input to the model.", "properties": { + "prompt_cache_breakpoint": { + "anyOf": [ + { + "$ref": "#/components/schemas/PromptCacheBreakpoint" + }, + { + "type": "null" + } + ] + }, "text": { "title": "Text", "type": "string" @@ -27508,9 +28478,21 @@ { "$ref": "#/components/schemas/ResponseToolSearchOutputItem" }, + { + "$ref": "#/components/schemas/AdditionalTools" + }, + { + "$ref": "#/components/schemas/ResponseConfigurationUpdateItem" + }, { "$ref": "#/components/schemas/ResponseReasoningItem" }, + { + "$ref": "#/components/schemas/Program" + }, + { + "$ref": "#/components/schemas/ProgramOutput" + }, { "$ref": "#/components/schemas/ResponseCompactionItem" }, @@ -27751,7 +28733,7 @@ }, "ResponseReasoningItem": { "additionalProperties": true, - "description": "A description of the chain of thought used by a reasoning model while generating\na response. Be sure to include these items in your `input` to the Responses API\nfor subsequent turns of a conversation if you are manually\n[managing context](https://platform.openai.com/docs/guides/conversation-state).", + "description": "A description of the chain of thought used by a reasoning model while generating\na response. Be sure to include these items in your `input` to the Responses API\nfor subsequent turns of a conversation if you are manually\n[managing context](https://developers.openai.com/api/docs/guides/conversation-state).", "properties": { "content": { "anyOf": [ @@ -27821,7 +28803,7 @@ }, "ResponseTextConfigParam": { "additionalProperties": true, - "description": "Configuration options for a text response from the model.\n\nCan be plain\ntext or structured JSON data. Learn more:\n- [Text inputs and outputs](https://platform.openai.com/docs/guides/text)\n- [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs)", + "description": "Configuration options for a text response from the model.\n\nCan be plain\ntext or structured JSON data. Learn more:\n- [Text inputs and outputs](https://developers.openai.com/api/docs/guides/text)\n- [Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs)", "properties": { "format": { "anyOf": [ @@ -27992,6 +28974,9 @@ { "$ref": "#/components/schemas/CodeInterpreter" }, + { + "$ref": "#/components/schemas/ProgrammaticToolCalling" + }, { "$ref": "#/components/schemas/ImageGeneration" }, @@ -28155,12 +29140,21 @@ { "$ref": "#/components/schemas/ResponseReasoningItem" }, + { + "$ref": "#/components/schemas/Program" + }, + { + "$ref": "#/components/schemas/ProgramOutput" + }, { "$ref": "#/components/schemas/ResponseToolSearchCall" }, { "$ref": "#/components/schemas/ResponseToolSearchOutputItem" }, + { + "$ref": "#/components/schemas/AdditionalTools" + }, { "$ref": "#/components/schemas/ResponseCompactionItem" }, @@ -28355,6 +29349,9 @@ { "$ref": "#/components/schemas/ToolChoiceCustomParam" }, + { + "$ref": "#/components/schemas/ToolChoiceSpecificProgrammaticToolCallingParam" + }, { "$ref": "#/components/schemas/ToolChoiceApplyPatchParam" }, @@ -28393,6 +29390,9 @@ { "$ref": "#/components/schemas/CodeInterpreter" }, + { + "$ref": "#/components/schemas/ProgrammaticToolCalling" + }, { "$ref": "#/components/schemas/ImageGeneration" }, @@ -28845,9 +29845,24 @@ "title": "ToolChoiceShellParam", "type": "object" }, + "ToolChoiceSpecificProgrammaticToolCallingParam": { + "additionalProperties": true, + "properties": { + "type": { + "const": "programmatic_tool_calling", + "title": "Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ToolChoiceSpecificProgrammaticToolCallingParam", + "type": "object" + }, "ToolChoiceTypesParam": { "additionalProperties": true, - "description": "Indicates that the model should use a built-in tool to generate a response.\n[Learn more about built-in tools](https://platform.openai.com/docs/guides/tools).", + "description": "Indicates that the model should use a built-in tool to generate a response.\n[Learn more about built-in tools](https://developers.openai.com/api/docs/guides/tools).", "properties": { "type": { "enum": [ @@ -28873,6 +29888,35 @@ "ToolFunction": { "additionalProperties": true, "properties": { + "allowed_callers": { + "anyOf": [ + { + "items": { + "enum": [ + "direct", + "programmatic" + ], + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Callers" + }, + "async": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Async" + }, "defer_loading": { "anyOf": [ { @@ -28899,6 +29943,18 @@ "title": "Name", "type": "string" }, + "output_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Output Schema" + }, "parameters": { "anyOf": [ {}, @@ -29062,7 +30118,7 @@ }, "WebSearchPreviewTool": { "additionalProperties": true, - "description": "This tool searches the web for relevant results to use in a response.\n\nLearn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search).", + "description": "This tool searches the web for relevant results to use in a response.\n\nLearn more about the [web search tool](https://developers.openai.com/api/docs/guides/tools-web-search).", "properties": { "search_content_types": { "anyOf": [ @@ -29125,8 +30181,19 @@ }, "WebSearchTool": { "additionalProperties": true, - "description": "Search the Internet for sources related to the prompt.\n\nLearn more about the\n[web search tool](https://platform.openai.com/docs/guides/tools-web-search).", + "description": "Search the Internet for sources related to the prompt.\n\nLearn more about the\n[web search tool](https://developers.openai.com/api/docs/guides/tools-web-search).", "properties": { + "external_web_access": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "External Web Access" + }, "filters": { "anyOf": [ { @@ -29180,7 +30247,7 @@ }, "openai__types__responses__web_search_preview_tool__UserLocation": { "additionalProperties": true, - "description": "The user's location.", + "description": "The approximate location of the user.\n\nIf omitted or null, defaults to the United States. To avoid this fallback, pass `{\"type\": \"approximate\"}` without location fields. To localize results, provide the relevant location fields.", "properties": { "city": { "anyOf": [ @@ -29240,7 +30307,7 @@ }, "openai__types__responses__web_search_tool__UserLocation": { "additionalProperties": true, - "description": "The approximate location of the user.", + "description": "The approximate location of the user.\n\nIf omitted or null, defaults to the\nUnited States. To avoid this fallback, pass `{\"type\": \"approximate\"}` without\nlocation fields. To localize results, provide the relevant location fields.", "properties": { "city": { "anyOf": [ @@ -47554,6 +48621,9 @@ "input_audio": { "$ref": "#/components/schemas/InputAudio" }, + "prompt_cache_breakpoint": { + "$ref": "#/components/schemas/PromptCacheBreakpoint" + }, "type": { "const": "input_audio", "title": "Type", @@ -48939,6 +50009,21 @@ "title": "PolicyValidationResponse", "type": "object" }, + "PromptCacheBreakpoint": { + "description": "Marks the exact end of a reusable prompt prefix.\n\nThe breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block.", + "properties": { + "mode": { + "const": "explicit", + "title": "Mode", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "PromptCacheBreakpoint", + "type": "object" + }, "SuggestTemplatesRequest": { "properties": { "attack_examples": { diff --git a/litellm/proxy/common_utils/responses_stream_errors.py b/litellm/proxy/common_utils/responses_stream_errors.py index 3409b4b6657..2b57618ea5f 100644 --- a/litellm/proxy/common_utils/responses_stream_errors.py +++ b/litellm/proxy/common_utils/responses_stream_errors.py @@ -72,7 +72,7 @@ def _failure_details(original: Exception) -> _FailureDetails: upstream: Final = _FailureDetails.model_validate(body) return _FailureDetails( message=upstream.message or mapped.message, - code=upstream.code if upstream.code is not None else mapped.code, + code=upstream.code if "code" in body and body["code"] is not None else mapped.code, type=upstream.type or mapped.type, status_code=mapped.status_code, ) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 16fbd1de376..7bc404c6eca 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -22,6 +22,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING, Annotated, Final, Literal, Protocol, cast import httpx +import httpx2 import openai from fastapi import APIRouter, Depends, HTTPException, Request, Response, WebSocket from fastapi.responses import StreamingResponse @@ -3398,7 +3399,7 @@ async def openai_websocket_proxy_route( base_target_url: Final = os.getenv("OPENAI_API_BASE") or "https://api.openai.com/" try: openai_api_key: Final = await _openai_passthrough_credential(base_target_url) - except (openai.OpenAIError, httpx.HTTPError, LiteLLMOpenAIError): + except (openai.OpenAIError, httpx.HTTPError, httpx2.HTTPError, LiteLLMOpenAIError): verbose_proxy_logger.exception("OpenAI workload identity token exchange failed for websocket passthrough") await websocket.close(code=1011, reason=_OPENAI_WS_TOKEN_EXCHANGE_FAILED_REASON) return diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index c5dc45566a9..dd6a554cefa 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -226,6 +226,9 @@ _ERROR_CODE_HTTP_STATUS: Final[Mapping[str, int]] = MappingProxyType( "insufficient_quota": 429, "vector_store_timeout": 504, "invalid_prompt": 400, + "data_residency_mismatch": 400, + "bio_policy": 400, + "misalignment_policy_violation": 403, "invalid_image": 400, "invalid_image_format": 400, "invalid_base64_image": 400, diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index c96e3e2c031..d9ec2cece9e 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -2,8 +2,10 @@ import asyncio import base64 import importlib import os +import sys from collections.abc import Callable, Coroutine, Iterator from dataclasses import dataclass, field +from functools import partial from pathlib import Path from types import MappingProxyType from typing import Final, Literal, TypeAlias @@ -427,6 +429,32 @@ def fake_provider_credentials(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv(name, value) +@pytest.fixture +def openai_token_exchange_via_httpx(monkeypatch: pytest.MonkeyPatch) -> None: + workload_module: Final = sys.modules.get("openai.auth._workload") + native_client_factory: Final = getattr(workload_module, "DefaultHttpx2Client", None) + if native_client_factory is None: + return + import httpx2 + + def send_through_httpx(request: httpx2.Request) -> httpx2.Response: + with httpx.Client(trust_env=False) as client: + try: + response: Final = client.send( + httpx.Request(request.method, str(request.url), headers=dict(request.headers), content=request.content) + ) + except httpx.TransportError as error: + native_error: Final = getattr(httpx2, type(error).__name__, httpx2.TransportError) + raise native_error(str(error), request=request) from error + return httpx2.Response(response.status_code, headers=dict(response.headers), content=response.content) + + monkeypatch.setattr( + workload_module, + "DefaultHttpx2Client", + partial(native_client_factory, transport=httpx2.MockTransport(send_through_httpx), trust_env=False), + ) + + @pytest.fixture async def drained_logging_worker() -> None: await asyncio.wait_for(GLOBAL_LOGGING_WORKER.clear_queue(), timeout=10) diff --git a/tests/unit/litellm_core_utils/test_exception_mapping_utils.py b/tests/unit/litellm_core_utils/test_exception_mapping_utils.py index 5ee14ff530d..365e319e34b 100644 --- a/tests/unit/litellm_core_utils/test_exception_mapping_utils.py +++ b/tests/unit/litellm_core_utils/test_exception_mapping_utils.py @@ -2235,10 +2235,8 @@ def test_completion_perplexity_exception_on_openai_client(monkeypatch): model="perplexity/mistral-7b-instruct", messages=[{"role": "user", "content": "hello"}], ) - assert ( - "The api_key client option must be set either by passing api_key to the client or by setting the PERPLEXITY_API_KEY environment variable" - in str(exc_info.value) - ) + assert "PERPLEXITY_API_KEY" in str(exc_info.value) + assert "OPENAI_API_KEY" not in str(exc_info.value) @pytest.mark.asyncio diff --git a/tests/unit/llms/azure_ai/chat/test_azure_ai_transformation.py b/tests/unit/llms/azure_ai/chat/test_azure_ai_transformation.py index 5186541a62b..1a6d206dbc8 100644 --- a/tests/unit/llms/azure_ai/chat/test_azure_ai_transformation.py +++ b/tests/unit/llms/azure_ai/chat/test_azure_ai_transformation.py @@ -160,6 +160,11 @@ async def test_azure_ai_acompletion_posts_the_conversation_to_the_models_endpoin "/openai/deployments/gpt-4.1-mini/chat/completions", id="full-deployment-url", ), + pytest.param( + "https://example-resource.services.ai.azure.com/openai/deployments/gpt-4.1-mini/chat/completions", + "/openai/deployments/gpt-4.1-mini/chat/completions", + id="full-deployment-url-without-query", + ), ], ) def test_azure_ai_completion_routes_flexible_api_base( diff --git a/tests/unit/llms/openai/test_openai_workload_identity.py b/tests/unit/llms/openai/test_openai_workload_identity.py index 1a1a4ca7047..c85f132e9fd 100644 --- a/tests/unit/llms/openai/test_openai_workload_identity.py +++ b/tests/unit/llms/openai/test_openai_workload_identity.py @@ -56,22 +56,7 @@ CHAT_COMPLETION_BODY: Final = { } -@pytest.fixture(autouse=True) -def mock_sdk_token_exchange_transport(monkeypatch: pytest.MonkeyPatch) -> None: - native_client_factory: Final = getattr(sys.modules.get("openai.auth._workload"), "DefaultHttpx2Client", None) - if native_client_factory is not None: - import httpx2 - - def handle_exchange(request: httpx2.Request) -> httpx2.Response: - response: Final = respx.mock.handler( - httpx.Request(request.method, str(request.url), headers=dict(request.headers), content=request.content) - ) - return httpx2.Response(response.status_code, headers=dict(response.headers), content=response.content) - - monkeypatch.setattr( - "openai.auth._workload.DefaultHttpx2Client", - partial(native_client_factory, transport=httpx2.MockTransport(handle_exchange), trust_env=False), - ) +pytestmark = pytest.mark.usefixtures("openai_token_exchange_via_httpx") @pytest.fixture diff --git a/tests/unit/proxy/guardrails/guardrail_hooks/openai/test_moderations.py b/tests/unit/proxy/guardrails/guardrail_hooks/openai/test_moderations.py index 930309d62ed..8412e72ca6a 100644 --- a/tests/unit/proxy/guardrails/guardrail_hooks/openai/test_moderations.py +++ b/tests/unit/proxy/guardrails/guardrail_hooks/openai/test_moderations.py @@ -1119,6 +1119,7 @@ class _ModerationsEndpointThatRecordsTheAuthorization: @pytest.mark.asyncio +@pytest.mark.usefixtures("openai_token_exchange_via_httpx") @respx.mock async def test_openai_moderation_sends_the_federated_bearer_when_only_workload_identity_is_set( monkeypatch: pytest.MonkeyPatch, tmp_path: Path @@ -1149,6 +1150,7 @@ async def test_openai_moderation_sends_the_federated_bearer_when_only_workload_i @pytest.mark.asyncio +@pytest.mark.usefixtures("openai_token_exchange_via_httpx") @respx.mock async def test_openai_moderation_federates_with_the_identity_in_its_own_guardrail_config( monkeypatch: pytest.MonkeyPatch, tmp_path: Path diff --git a/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 0186a33dbc4..c5626df405e 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -4058,7 +4058,7 @@ def test_openai_passthrough_forwards_verbatim_to_openai( @pytest.fixture -def openai_wif_env(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: +def openai_wif_env(monkeypatch: pytest.MonkeyPatch, tmp_path, openai_token_exchange_via_httpx: None) -> None: from litellm.llms.openai.workload_identity import _workload_identity_auth token_file: Final = tmp_path / "subject_token.jwt" diff --git a/tests/unit/proxy/test_openai_ws_passthrough_routes.py b/tests/unit/proxy/test_openai_ws_passthrough_routes.py index 6a9cd972dc2..3c785af2437 100644 --- a/tests/unit/proxy/test_openai_ws_passthrough_routes.py +++ b/tests/unit/proxy/test_openai_ws_passthrough_routes.py @@ -182,7 +182,7 @@ TOKEN_EXCHANGE_URL: Final = "https://auth.openai.com/oauth/token" @pytest.fixture -def openai_wif_token_file(monkeypatch, tmp_path): +def openai_wif_token_file(monkeypatch, tmp_path, openai_token_exchange_via_httpx: None): token_file = tmp_path / "subject_token.jwt" token_file.write_text("subject-token-from-file") monkeypatch.delenv("OPENAI_API_BASE", raising=False) diff --git a/tests/unit/realtime_api/test_main.py b/tests/unit/realtime_api/test_main.py index 4d89ee497a5..4cc181ea41a 100644 --- a/tests/unit/realtime_api/test_main.py +++ b/tests/unit/realtime_api/test_main.py @@ -1081,7 +1081,9 @@ async def test_voice_transcript_blocked_by_guardrail(): @pytest.fixture -def federated_deployment(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> dict[str, str]: +def federated_deployment( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, openai_token_exchange_via_httpx: None +) -> dict[str, str]: token_file: Final = tmp_path / "subject_token.jwt" token_file.write_text("subject-token") for name in ( diff --git a/tests/unit/responses/test_streaming_iterator_error_events.py b/tests/unit/responses/test_streaming_iterator_error_events.py index 04d2b9ddf81..373c562066d 100644 --- a/tests/unit/responses/test_streaming_iterator_error_events.py +++ b/tests/unit/responses/test_streaming_iterator_error_events.py @@ -608,6 +608,9 @@ def test_every_openai_sdk_response_error_code_has_explicit_status_mapping(): ("empty_image_file", 400), ("failed_to_download_image", 400), ("image_file_not_found", 400), + ("data_residency_mismatch", 400), + ("bio_policy", 400), + ("misalignment_policy_violation", 403), ("totally_unknown_future_code", 500), ("429", 429), ("503", 503), diff --git a/tests/unit/test_router/test_router.py b/tests/unit/test_router/test_router.py index 13ac55fa937..12c057c87d3 100644 --- a/tests/unit/test_router/test_router.py +++ b/tests/unit/test_router/test_router.py @@ -26971,7 +26971,7 @@ def test_router_dynamic_cooldown_correct_retry_after_time(): ] ) - openai_client = openai.OpenAI(api_key="") + openai_client = openai.OpenAI(api_key="sk-fake") cooldown_time = 30 diff --git a/tests/unit/test_router/test_router_provider_endpoints.py b/tests/unit/test_router/test_router_provider_endpoints.py index f8b57410872..cf0b0a5e187 100644 --- a/tests/unit/test_router/test_router_provider_endpoints.py +++ b/tests/unit/test_router/test_router_provider_endpoints.py @@ -95,11 +95,16 @@ def _ratelimit_headers(response: ModelResponse | CustomStreamWrapper) -> dict[st def _openai_router_client() -> AsyncOpenAI: - return AsyncOpenAI(api_key="sk-fake") + return AsyncOpenAI(api_key="sk-fake", http_client=httpx.AsyncClient()) def _azure_router_client() -> AsyncAzureOpenAI: - return AsyncAzureOpenAI(api_key="sk-fake", azure_endpoint="https://azure.test", api_version="2025-02-01-preview") + return AsyncAzureOpenAI( + api_key="sk-fake", + azure_endpoint="https://azure.test", + api_version="2025-02-01-preview", + http_client=httpx.AsyncClient(), + ) @pytest.mark.asyncio diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 2b174a1141a..165a66866d4 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -25543,7 +25543,7 @@ export interface components { /** Queries */ queries?: string[] | null; /** Query */ - query: string; + query?: string | null; /** Sources */ sources?: components["schemas"]["ActionSearchSource"][] | null; /** @@ -25632,6 +25632,25 @@ export interface components { [key: string]: string; }; }; + /** AdditionalTools */ + AdditionalTools: { + /** Id */ + id: string; + /** + * Role + * @enum {string} + */ + role: "unknown" | "user" | "assistant" | "system" | "critic" | "discriminator" | "developer" | "tool"; + /** Tools */ + tools: (components["schemas"]["FunctionTool"] | components["schemas"]["FileSearchTool"] | components["schemas"]["ComputerTool"] | components["schemas"]["ComputerUsePreviewTool"] | components["schemas"]["WebSearchTool"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ProgrammaticToolCalling"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellTool"] | components["schemas"]["CustomTool"] | components["schemas"]["NamespaceTool"] | components["schemas"]["ToolSearchTool"] | components["schemas"]["WebSearchPreviewTool"] | components["schemas"]["ApplyPatchTool"])[]; + /** + * Type + * @constant + */ + type: "additional_tools"; + } & { + [key: string]: unknown; + }; /** AdminIgnore */ AdminIgnore: { /** @@ -26277,6 +26296,8 @@ export interface components { ApplyPatchCall: { /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["ApplyPatchCallCallerDirect"] | components["schemas"]["ApplyPatchCallCallerProgram"] | null; /** Id */ id?: string | null; /** Operation */ @@ -26292,6 +26313,24 @@ export interface components { */ type: "apply_patch_call"; }; + /** ApplyPatchCallCallerDirect */ + ApplyPatchCallCallerDirect: { + /** + * Type + * @constant + */ + type: "direct"; + }; + /** ApplyPatchCallCallerProgram */ + ApplyPatchCallCallerProgram: { + /** Caller Id */ + caller_id: string; + /** + * Type + * @constant + */ + type: "program"; + }; /** * ApplyPatchCallOperationCreateFile * @description Instruction for creating a new file via the apply_patch tool. @@ -26342,6 +26381,8 @@ export interface components { ApplyPatchCallOutput: { /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["ApplyPatchCallOutputCallerDirect"] | components["schemas"]["ApplyPatchCallOutputCallerProgram"] | null; /** Id */ id?: string | null; /** Output */ @@ -26357,11 +26398,31 @@ export interface components { */ type: "apply_patch_call_output"; }; + /** ApplyPatchCallOutputCallerDirect */ + ApplyPatchCallOutputCallerDirect: { + /** + * Type + * @constant + */ + type: "direct"; + }; + /** ApplyPatchCallOutputCallerProgram */ + ApplyPatchCallOutputCallerProgram: { + /** Caller Id */ + caller_id: string; + /** + * Type + * @constant + */ + type: "program"; + }; /** * ApplyPatchTool * @description Allows the assistant to create, delete, or update files using unified diffs. */ ApplyPatchTool: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; /** * Type * @constant @@ -26375,6 +26436,8 @@ export interface components { * @description Allows the assistant to create, delete, or update files using unified diffs. */ ApplyPatchToolParam: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; /** * Type * @constant @@ -28528,6 +28591,28 @@ export interface components { /** Success And Failure */ success_and_failure: string[]; }; + /** CallerDirect */ + CallerDirect: { + /** + * Type + * @constant + */ + type: "direct"; + } & { + [key: string]: unknown; + }; + /** CallerProgram */ + CallerProgram: { + /** Caller Id */ + caller_id: string; + /** + * Type + * @constant + */ + type: "program"; + } & { + [key: string]: unknown; + }; /** * CancelEvalResponse * @description Response from cancelling an evaluation @@ -28724,6 +28809,7 @@ export interface components { /** ChatCompletionAudioObject */ ChatCompletionAudioObject: { input_audio: components["schemas"]["InputAudio"]; + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"]; /** * Type * @constant @@ -29453,6 +29539,8 @@ export interface components { * @description A tool that runs Python code to help generate a response to a prompt. */ CodeInterpreter: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; /** Container */ container: string | components["schemas"]["CodeInterpreterContainerCodeInterpreterToolAuto"]; /** @@ -29484,6 +29572,17 @@ export interface components { } & { [key: string]: unknown; }; + /** + * CompactionTrigger + * @description Compacts the current context. Must be the final input item. + */ + CompactionTrigger: { + /** + * Type + * @constant + */ + type: "compaction_trigger"; + }; /** * ComparisonFilter * @description A filter used to compare a specified attribute key to a given value using a defined comparison operation. @@ -29657,7 +29756,7 @@ export interface components { * ComputerTool * @description A tool that controls a virtual computer. * - * Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use). + * Learn more about the [computer tool](https://developers.openai.com/api/docs/guides/tools-computer-use). */ ComputerTool: { /** @@ -29672,7 +29771,7 @@ export interface components { * ComputerUsePreviewTool * @description A tool that controls a virtual computer. * - * Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use). + * Learn more about the [computer tool](https://developers.openai.com/api/docs/guides/tools-computer-use). */ ComputerUsePreviewTool: { /** Display Height */ @@ -29696,7 +29795,7 @@ export interface components { * ComputerUsePreviewToolParam * @description A tool that controls a virtual computer. * - * Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use). + * Learn more about the [computer tool](https://developers.openai.com/api/docs/guides/tools-computer-use). */ ComputerUsePreviewToolParam: { /** Display Height */ @@ -31233,9 +31332,13 @@ export interface components { * CustomTool * @description A custom tool that processes input using a specified format. * - * Learn more about [custom tools](https://platform.openai.com/docs/guides/function-calling#custom-tools) + * Learn more about [custom tools](https://developers.openai.com/api/docs/guides/function-calling#custom-tools) */ CustomTool: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; + /** Async */ + async?: boolean | null; /** Defer Loading */ defer_loading?: boolean | null; /** Description */ @@ -31283,9 +31386,13 @@ export interface components { * CustomToolParam * @description A custom tool that processes input using a specified format. * - * Learn more about [custom tools](https://platform.openai.com/docs/guides/function-calling#custom-tools) + * Learn more about [custom tools](https://developers.openai.com/api/docs/guides/function-calling#custom-tools) */ CustomToolParam: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; + /** Async */ + async?: boolean; /** Defer Loading */ defer_loading?: boolean; /** Description */ @@ -32464,7 +32571,7 @@ export interface components { * FileSearchTool * @description A tool that searches for relevant content from uploaded files. * - * Learn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search). + * Learn more about the [file search tool](https://developers.openai.com/api/docs/guides/tools-file-search). */ FileSearchTool: { /** Filters */ @@ -32486,7 +32593,7 @@ export interface components { * FileSearchToolParam * @description A tool that searches for relevant content from uploaded files. * - * Learn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search). + * Learn more about the [file search tool](https://developers.openai.com/api/docs/guides/tools-file-search). */ FileSearchToolParam: { /** Filters */ @@ -32527,9 +32634,15 @@ export interface components { */ FunctionCallOutput: { /** Call Id */ - call_id: string; + call_id?: string | null; + /** Caller */ + caller?: components["schemas"]["FunctionCallOutputCallerDirect"] | components["schemas"]["FunctionCallOutputCallerProgram"] | null; /** Id */ id?: string | null; + /** Name */ + name?: string | null; + /** Namespace */ + namespace?: string | null; /** Output */ output: string | (components["schemas"]["ResponseInputTextContentParam"] | components["schemas"]["ResponseInputImageContentParam"] | components["schemas"]["ResponseInputFileContentParam"])[]; /** Status */ @@ -32540,11 +32653,31 @@ export interface components { */ type: "function_call_output"; }; + /** FunctionCallOutputCallerDirect */ + FunctionCallOutputCallerDirect: { + /** + * Type + * @constant + */ + type: "direct"; + }; + /** FunctionCallOutputCallerProgram */ + FunctionCallOutputCallerProgram: { + /** Caller Id */ + caller_id: string; + /** + * Type + * @constant + */ + type: "program"; + }; /** * FunctionShellTool * @description A tool that allows the model to execute shell commands. */ FunctionShellTool: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; /** Environment */ environment?: components["schemas"]["ContainerAuto"] | components["schemas"]["LocalEnvironment"] | components["schemas"]["ContainerReference"] | null; /** @@ -32560,6 +32693,8 @@ export interface components { * @description A tool that allows the model to execute shell commands. */ FunctionShellToolParam: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; /** Environment */ environment?: components["schemas"]["ContainerAutoParam"] | components["schemas"]["LocalEnvironmentParam"] | components["schemas"]["ContainerReferenceParam"] | null; /** @@ -32572,15 +32707,23 @@ export interface components { * FunctionTool * @description Defines a function in your own code the model can choose to call. * - * Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling). + * Learn more about [function calling](https://developers.openai.com/api/docs/guides/function-calling). */ FunctionTool: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; + /** Async */ + async?: boolean | null; /** Defer Loading */ defer_loading?: boolean | null; /** Description */ description?: string | null; /** Name */ name: string; + /** Output Schema */ + output_schema?: { + [key: string]: unknown; + } | null; /** Parameters */ parameters?: { [key: string]: unknown; @@ -32599,15 +32742,23 @@ export interface components { * FunctionToolParam * @description Defines a function in your own code the model can choose to call. * - * Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling). + * Learn more about [function calling](https://developers.openai.com/api/docs/guides/function-calling). */ FunctionToolParam: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; + /** Async */ + async?: boolean; /** Defer Loading */ defer_loading?: boolean; /** Description */ description?: string | null; /** Name */ name: string; + /** Output Schema */ + output_schema?: { + [key: string]: unknown; + } | null; /** Parameters */ parameters: { [key: string]: unknown; @@ -33304,6 +33455,20 @@ export interface components { */ type: "http"; }; + /** HTTPError */ + HTTPError: { + /** Code */ + code: number; + /** Message */ + message: string; + /** + * Type + * @constant + */ + type: "http_error"; + } & { + [key: string]: unknown; + }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -33409,7 +33574,7 @@ export interface components { input_fidelity?: ("high" | "low") | null; input_image_mask?: components["schemas"]["ImageGenerationInputImageMask"] | null; /** Model */ - model?: string | ("gpt-image-1" | "gpt-image-1-mini" | "gpt-image-1.5") | null; + model?: string | ("gpt-image-1" | "gpt-image-1-mini" | "gpt-image-2" | "gpt-image-2-2026-04-21" | "gpt-image-2.5-sunburst" | "gpt-image-2.5-sunburst-2026-09-08" | "gpt-image-2.5-flare" | "gpt-image-2.5-flare-2026-09-08" | "gpt-image-1.5" | "chatgpt-image-latest") | null; /** Moderation */ moderation?: ("auto" | "low") | null; /** Output Compression */ @@ -33419,9 +33584,9 @@ export interface components { /** Partial Images */ partial_images?: number | null; /** Quality */ - quality?: ("low" | "medium" | "high" | "auto") | null; + quality?: ("low" | "medium" | "high" | "xhigh" | "max" | "auto") | null; /** Size */ - size?: ("1024x1024" | "1024x1536" | "1536x1024" | "auto") | null; + size?: string | ("1024x1024" | "1024x1536" | "1536x1024" | "auto") | null; /** * Type * @constant @@ -33435,10 +33600,23 @@ export interface components { * @description An image generation request made by the model. */ ImageGenerationCall: { + /** Action */ + action?: ("generate" | "edit" | "auto") | null; + /** Background */ + background?: ("transparent" | "opaque" | "auto") | null; + error?: components["schemas"]["ImageGenerationCallError"] | null; /** Id */ id: string; + /** Output Format */ + output_format?: ("png" | "webp" | "jpeg") | null; + /** Quality */ + quality?: ("low" | "medium" | "high" | "xhigh" | "max" | "auto") | null; /** Result */ result?: string | null; + /** Revised Prompt */ + revised_prompt?: string | null; + /** Size */ + size?: string | ("1024x1024" | "1024x1536" | "1536x1024") | null; /** * Status * @enum {string} @@ -33452,6 +33630,38 @@ export interface components { } & { [key: string]: unknown; }; + /** + * ImageGenerationCallError + * @description Details of a moderation failure, when available. + */ + ImageGenerationCallError: { + /** Code */ + code?: string | null; + /** Message */ + message: string; + moderation_details?: components["schemas"]["ImageGenerationCallErrorModerationDetails"] | null; + /** Param */ + param?: string | null; + /** Type */ + type: string; + } & { + [key: string]: unknown; + }; + /** + * ImageGenerationCallErrorModerationDetails + * @description Public details for a `moderation_blocked` error, when available. + */ + ImageGenerationCallErrorModerationDetails: { + /** Categories */ + categories: string[]; + /** + * Moderation Stage + * @enum {string} + */ + moderation_stage: "input" | "output" | "unknown"; + } & { + [key: string]: unknown; + }; /** * ImageGenerationInputImageMask * @description Optional mask for inpainting. @@ -33495,7 +33705,7 @@ export interface components { */ IncompleteDetails: { /** Reason */ - reason?: ("max_output_tokens" | "content_filter") | null; + reason?: ("max_output_tokens" | "max_messages" | "content_filter" | "steered") | null; } & { [key: string]: unknown; }; @@ -35592,7 +35802,7 @@ export interface components { [key: string]: unknown; }[] | null; /** Timeout */ - timeout?: number | string | null; + timeout?: number | string | components["schemas"]["Timeout"] | null; /** Timeout By Service Tier */ timeout_by_service_tier?: { [key: string]: number; @@ -37964,9 +38174,11 @@ export interface components { /** * Mcp * @description Give the model access to additional tools via remote Model Context Protocol - * (MCP) servers. [Learn more about MCP](https://platform.openai.com/docs/guides/tools-remote-mcp). + * (MCP) servers. [Learn more about MCP](https://developers.openai.com/api/docs/guides/tools-connectors-mcp). */ Mcp: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; /** Allowed Tools */ allowed_tools?: string[] | components["schemas"]["McpAllowedToolsMcpToolFilter"] | null; /** Authorization */ @@ -37987,6 +38199,8 @@ export interface components { server_label: string; /** Server Url */ server_url?: string | null; + /** Tunnel Id */ + tunnel_id?: string | null; /** * Type * @constant @@ -38059,7 +38273,7 @@ export interface components { /** Arguments */ arguments: string; /** Error */ - error?: string | null; + error?: components["schemas"]["McpProtocolError"] | components["schemas"]["McpToolExecutionError"] | components["schemas"]["HTTPError"] | null; /** Id */ id: string; /** Name */ @@ -38115,6 +38329,20 @@ export interface components { } & { [key: string]: unknown; }; + /** McpProtocolError */ + McpProtocolError: { + /** Code */ + code: number; + /** Message */ + message: string; + /** + * Type + * @constant + */ + type: "mcp_protocol_error"; + } & { + [key: string]: unknown; + }; /** * McpRequireApprovalMcpToolApprovalFilter * @description Specify which of the MCP server's tools require approval. @@ -38153,6 +38381,18 @@ export interface components { } & { [key: string]: unknown; }; + /** McpToolExecutionError */ + McpToolExecutionError: { + /** Content */ + content: unknown; + /** + * Type + * @constant + */ + type: "mcp_tool_execution_error"; + } & { + [key: string]: unknown; + }; /** Member */ Member: { /** @@ -41508,6 +41748,55 @@ export interface components { */ version_status: string; }; + /** Program */ + Program: { + /** Call Id */ + call_id: string; + /** Code */ + code: string; + /** Fingerprint */ + fingerprint: string; + /** Id */ + id: string; + /** + * Type + * @constant + */ + type: "program"; + } & { + [key: string]: unknown; + }; + /** ProgramOutput */ + ProgramOutput: { + /** Call Id */ + call_id: string; + /** Id */ + id: string; + /** Result */ + result: string; + /** + * Status + * @enum {string} + */ + status: "completed" | "incomplete"; + /** + * Type + * @constant + */ + type: "program_output"; + } & { + [key: string]: unknown; + }; + /** ProgrammaticToolCalling */ + ProgrammaticToolCalling: { + /** + * Type + * @constant + */ + type: "programmatic_tool_calling"; + } & { + [key: string]: unknown; + }; /** Prompt */ Prompt: { litellm_params: components["schemas"]["PromptLiteLLMParams"]; @@ -41515,6 +41804,21 @@ export interface components { prompt_id: string; prompt_info?: components["schemas"]["PromptInfo"] | null; }; + /** + * PromptCacheBreakpoint + * @description Marks the exact end of a reusable prompt prefix. + * + * The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + */ + PromptCacheBreakpoint: { + /** + * Mode + * @constant + */ + mode: "explicit"; + } & { + [key: string]: unknown; + }; /** PromptCacheOptions */ PromptCacheOptions: { /** @@ -42000,18 +42304,13 @@ export interface components { }; /** * Reasoning - * @description **gpt-5 and o-series models only** - * - * Configuration options for - * [reasoning models](https://platform.openai.com/docs/guides/reasoning). + * @description The reasoning configuration applied by this update. */ Reasoning: { /** Effort */ - effort?: ("none" | "minimal" | "low" | "medium" | "high" | "xhigh") | null; - /** Generate Summary */ - generate_summary?: ("auto" | "concise" | "detailed") | null; - /** Summary */ - summary?: ("auto" | "concise" | "detailed") | null; + effort?: ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max") | null; + } & { + [key: string]: unknown; }; /** RegenerateKeyRequest */ RegenerateKeyRequest: { @@ -42875,6 +43174,8 @@ export interface components { ResponseApplyPatchToolCall: { /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Created By */ created_by?: string | null; /** Id */ @@ -42901,6 +43202,8 @@ export interface components { ResponseApplyPatchToolCallOutput: { /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Created By */ created_by?: string | null; /** Id */ @@ -42972,7 +43275,7 @@ export interface components { }; /** * ResponseCompactionItem - * @description A compaction item generated by the [`v1/responses/compact` API](https://platform.openai.com/docs/api-reference/responses/compact). + * @description A compaction item generated by the [`v1/responses/compact` API](https://developers.openai.com/api/reference/resources/responses/methods/compact). */ ResponseCompactionItem: { /** Created By */ @@ -42991,7 +43294,7 @@ export interface components { }; /** * ResponseCompactionItemParamParam - * @description A compaction item generated by the [`v1/responses/compact` API](https://platform.openai.com/docs/api-reference/responses/compact). + * @description A compaction item generated by the [`v1/responses/compact` API](https://developers.openai.com/api/reference/resources/responses/methods/compact). */ ResponseCompactionItemParamParam: { /** Encrypted Content */ @@ -43009,7 +43312,7 @@ export interface components { * @description A tool call to a computer use tool. * * See the - * [computer use guide](https://platform.openai.com/docs/guides/tools-computer-use) for more information. + * [computer use guide](https://developers.openai.com/api/docs/guides/tools-computer-use) for more information. */ ResponseComputerToolCall: { /** Action */ @@ -43096,7 +43399,7 @@ export interface components { * @description A tool call to a computer use tool. * * See the - * [computer use guide](https://platform.openai.com/docs/guides/tools-computer-use) for more information. + * [computer use guide](https://developers.openai.com/api/docs/guides/tools-computer-use) for more information. */ ResponseComputerToolCallParam: { /** Action */ @@ -43120,6 +43423,41 @@ export interface components { */ type: "computer_call"; }; + /** + * ResponseConfigurationUpdateItem + * @description A configuration update that applies to subsequent responses until it is + * replaced by another configuration update. + */ + ResponseConfigurationUpdateItem: { + /** Id */ + id: string; + reasoning?: components["schemas"]["Reasoning"] | null; + /** + * Type + * @constant + */ + type: "configuration_update"; + } & { + [key: string]: unknown; + }; + /** + * ResponseConfigurationUpdateItemParamParam + * @description An update to the conversation's response configuration. + * + * The configuration + * remains in effect for subsequent responses until it is replaced by another + * configuration update. + */ + ResponseConfigurationUpdateItemParamParam: { + /** Id */ + id?: string | null; + reasoning?: components["schemas"]["openai__types__responses__response_configuration_update_item_param_param__Reasoning"]; + /** + * Type + * @constant + */ + type: "configuration_update"; + }; /** * ResponseContainerReference * @description Represents a container created with /v1/containers. @@ -43140,8 +43478,12 @@ export interface components { * @description A call to a custom tool created by the model. */ ResponseCustomToolCall: { + /** Async */ + async?: boolean | null; /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Id */ id?: string | null; /** Input */ @@ -43163,8 +43505,12 @@ export interface components { * @description A call to a custom tool created by the model. */ ResponseCustomToolCallItem: { + /** Async */ + async?: boolean | null; /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Created By */ created_by?: string | null; /** Id */ @@ -43195,6 +43541,8 @@ export interface components { ResponseCustomToolCallOutputItem: { /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Created By */ created_by?: string | null; /** Id */ @@ -43221,6 +43569,8 @@ export interface components { ResponseCustomToolCallOutputParam: { /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Id */ id?: string; /** Output */ @@ -43236,8 +43586,12 @@ export interface components { * @description A call to a custom tool created by the model. */ ResponseCustomToolCallParam: { + /** Async */ + async?: boolean; /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Id */ id?: string; /** Input */ @@ -43257,7 +43611,7 @@ export interface components { * @description The results of a file search tool call. * * See the - * [file search guide](https://platform.openai.com/docs/guides/tools-file-search) for more information. + * [file search guide](https://developers.openai.com/api/docs/guides/tools-file-search) for more information. */ ResponseFileSearchToolCall: { /** Id */ @@ -43284,7 +43638,7 @@ export interface components { * @description The results of a file search tool call. * * See the - * [file search guide](https://platform.openai.com/docs/guides/tools-file-search) for more information. + * [file search guide](https://developers.openai.com/api/docs/guides/tools-file-search) for more information. */ ResponseFileSearchToolCallParam: { /** Id */ @@ -43340,7 +43694,7 @@ export interface components { * @description JSON Schema response format. * * Used to generate structured JSON responses. - * Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs). + * Learn more about [Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs). */ ResponseFormatTextJSONSchemaConfigParam: { /** Description */ @@ -43381,6 +43735,8 @@ export interface components { action: components["schemas"]["Action"]; /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Created By */ created_by?: string | null; /** Environment */ @@ -43407,6 +43763,8 @@ export interface components { ResponseFunctionShellToolCallOutput: { /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Created By */ created_by?: string | null; /** Id */ @@ -43433,13 +43791,17 @@ export interface components { * @description A tool call to run a function. * * See the - * [function calling guide](https://platform.openai.com/docs/guides/function-calling) for more information. + * [function calling guide](https://developers.openai.com/api/docs/guides/function-calling) for more information. */ ResponseFunctionToolCall: { /** Arguments */ arguments: string; + /** Async */ + async?: boolean | null; /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Id */ id?: string | null; /** Name */ @@ -43461,13 +43823,17 @@ export interface components { * @description A tool call to run a function. * * See the - * [function calling guide](https://platform.openai.com/docs/guides/function-calling) for more information. + * [function calling guide](https://developers.openai.com/api/docs/guides/function-calling) for more information. */ ResponseFunctionToolCallItem: { /** Arguments */ arguments: string; + /** Async */ + async?: boolean | null; /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Created By */ created_by?: string | null; /** Id */ @@ -43492,11 +43858,17 @@ export interface components { /** ResponseFunctionToolCallOutputItem */ ResponseFunctionToolCallOutputItem: { /** Call Id */ - call_id: string; + call_id?: string | null; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Created By */ created_by?: string | null; /** Id */ id: string; + /** Name */ + name?: string | null; + /** Namespace */ + namespace?: string | null; /** Output */ output: string | (components["schemas"]["ResponseInputText"] | components["schemas"]["ResponseInputImage"] | components["schemas"]["ResponseInputFile"])[]; /** @@ -43517,13 +43889,17 @@ export interface components { * @description A tool call to run a function. * * See the - * [function calling guide](https://platform.openai.com/docs/guides/function-calling) for more information. + * [function calling guide](https://developers.openai.com/api/docs/guides/function-calling) for more information. */ ResponseFunctionToolCallParam: { /** Arguments */ arguments: string; + /** Async */ + async?: boolean; /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["CallerDirect"] | components["schemas"]["CallerProgram"] | null; /** Id */ id?: string; /** Name */ @@ -43546,7 +43922,7 @@ export interface components { * @description The results of a web search tool call. * * See the - * [web search guide](https://platform.openai.com/docs/guides/tools-web-search) for more information. + * [web search guide](https://developers.openai.com/api/docs/guides/tools-web-search) for more information. */ ResponseFunctionWebSearch: { /** Action */ @@ -43557,7 +43933,7 @@ export interface components { * Status * @enum {string} */ - status: "in_progress" | "searching" | "completed" | "failed"; + status: "in_progress" | "searching" | "completed" | "failed" | "incomplete"; /** * Type * @constant @@ -43571,7 +43947,7 @@ export interface components { * @description The results of a web search tool call. * * See the - * [web search guide](https://platform.openai.com/docs/guides/tools-web-search) for more information. + * [web search guide](https://developers.openai.com/api/docs/guides/tools-web-search) for more information. */ ResponseFunctionWebSearchParam: { /** Action */ @@ -43582,7 +43958,7 @@ export interface components { * Status * @enum {string} */ - status: "in_progress" | "searching" | "completed" | "failed"; + status: "in_progress" | "searching" | "completed" | "incomplete" | "failed"; /** * Type * @constant @@ -43595,7 +43971,7 @@ export interface components { */ ResponseInputFile: { /** Detail */ - detail?: ("high" | "low") | null; + detail?: ("auto" | "low" | "high") | null; /** File Data */ file_data?: string | null; /** File Id */ @@ -43604,6 +43980,7 @@ export interface components { file_url?: string | null; /** Filename */ filename?: string | null; + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"] | null; /** * Type * @constant @@ -43621,7 +43998,7 @@ export interface components { * Detail * @enum {string} */ - detail?: "low" | "high"; + detail?: "auto" | "low" | "high"; /** File Data */ file_data?: string | null; /** File Id */ @@ -43630,6 +44007,7 @@ export interface components { file_url?: string | null; /** Filename */ filename?: string | null; + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"] | null; /** * Type * @constant @@ -43645,7 +44023,7 @@ export interface components { * Detail * @enum {string} */ - detail?: "low" | "high"; + detail?: "auto" | "low" | "high"; /** File Data */ file_data?: string; /** File Id */ @@ -43654,6 +44032,7 @@ export interface components { file_url?: string; /** Filename */ filename?: string; + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"]; /** * Type * @constant @@ -43664,7 +44043,7 @@ export interface components { * ResponseInputImage * @description An image input to the model. * - * Learn about [image inputs](https://platform.openai.com/docs/guides/vision). + * Learn about [image inputs](https://developers.openai.com/api/docs/guides/images-vision). */ ResponseInputImage: { /** @@ -43676,6 +44055,7 @@ export interface components { file_id?: string | null; /** Image Url */ image_url?: string | null; + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"] | null; /** * Type * @constant @@ -43688,7 +44068,7 @@ export interface components { * ResponseInputImageContentParam * @description An image input to the model. * - * Learn about [image inputs](https://platform.openai.com/docs/guides/vision) + * Learn about [image inputs](https://developers.openai.com/api/docs/guides/images-vision) */ ResponseInputImageContentParam: { /** Detail */ @@ -43697,6 +44077,7 @@ export interface components { file_id?: string | null; /** Image Url */ image_url?: string | null; + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"] | null; /** * Type * @constant @@ -43707,7 +44088,7 @@ export interface components { * ResponseInputImageParam * @description An image input to the model. * - * Learn about [image inputs](https://platform.openai.com/docs/guides/vision). + * Learn about [image inputs](https://developers.openai.com/api/docs/guides/images-vision). */ ResponseInputImageParam: { /** @@ -43719,6 +44100,7 @@ export interface components { file_id?: string | null; /** Image Url */ image_url?: string | null; + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"]; /** * Type * @constant @@ -43751,6 +44133,7 @@ export interface components { * @description A text input to the model. */ ResponseInputText: { + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"] | null; /** Text */ text: string; /** @@ -43766,6 +44149,7 @@ export interface components { * @description A text input to the model. */ ResponseInputTextContentParam: { + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"] | null; /** Text */ text: string; /** @@ -43779,6 +44163,7 @@ export interface components { * @description A text input to the model. */ ResponseInputTextParam: { + prompt_cache_breakpoint?: components["schemas"]["PromptCacheBreakpoint"]; /** Text */ text: string; /** @@ -43793,7 +44178,7 @@ export interface components { */ ResponseItemList: { /** Data */ - data: (components["schemas"]["ResponseInputMessageItem"] | components["schemas"]["ResponseOutputMessage"] | components["schemas"]["ResponseFileSearchToolCall"] | components["schemas"]["ResponseComputerToolCall"] | components["schemas"]["ResponseComputerToolCallOutputItem"] | components["schemas"]["ResponseFunctionWebSearch"] | components["schemas"]["ResponseFunctionToolCallItem"] | components["schemas"]["ResponseFunctionToolCallOutputItem"] | components["schemas"]["ResponseToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItem"] | components["schemas"]["ResponseReasoningItem"] | components["schemas"]["ResponseCompactionItem"] | components["schemas"]["ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCall"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ResponseFunctionShellToolCall"] | components["schemas"]["ResponseFunctionShellToolCallOutput"] | components["schemas"]["ResponseApplyPatchToolCall"] | components["schemas"]["ResponseApplyPatchToolCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallItem"] | components["schemas"]["ResponseCustomToolCallOutputItem"])[]; + data: (components["schemas"]["ResponseInputMessageItem"] | components["schemas"]["ResponseOutputMessage"] | components["schemas"]["ResponseFileSearchToolCall"] | components["schemas"]["ResponseComputerToolCall"] | components["schemas"]["ResponseComputerToolCallOutputItem"] | components["schemas"]["ResponseFunctionWebSearch"] | components["schemas"]["ResponseFunctionToolCallItem"] | components["schemas"]["ResponseFunctionToolCallOutputItem"] | components["schemas"]["ResponseToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItem"] | components["schemas"]["AdditionalTools"] | components["schemas"]["ResponseConfigurationUpdateItem"] | components["schemas"]["ResponseReasoningItem"] | components["schemas"]["Program"] | components["schemas"]["ProgramOutput"] | components["schemas"]["ResponseCompactionItem"] | components["schemas"]["ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCall"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ResponseFunctionShellToolCall"] | components["schemas"]["ResponseFunctionShellToolCallOutput"] | components["schemas"]["ResponseApplyPatchToolCall"] | components["schemas"]["ResponseApplyPatchToolCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallItem"] | components["schemas"]["ResponseCustomToolCallOutputItem"])[]; /** First Id */ first_id: string; /** Has More */ @@ -43950,7 +44335,7 @@ export interface components { * @description A description of the chain of thought used by a reasoning model while generating * a response. Be sure to include these items in your `input` to the Responses API * for subsequent turns of a conversation if you are manually - * [managing context](https://platform.openai.com/docs/guides/conversation-state). + * [managing context](https://developers.openai.com/api/docs/guides/conversation-state). */ ResponseReasoningItem: { /** Content */ @@ -43976,7 +44361,7 @@ export interface components { * @description A description of the chain of thought used by a reasoning model while generating * a response. Be sure to include these items in your `input` to the Responses API * for subsequent turns of a conversation if you are manually - * [managing context](https://platform.openai.com/docs/guides/conversation-state). + * [managing context](https://developers.openai.com/api/docs/guides/conversation-state). */ ResponseReasoningItemParam: { /** Content */ @@ -44004,8 +44389,8 @@ export interface components { * * Can be plain * text or structured JSON data. Learn more: - * - [Text inputs and outputs](https://platform.openai.com/docs/guides/text) - * - [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs) + * - [Text inputs and outputs](https://developers.openai.com/api/docs/guides/text) + * - [Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs) */ ResponseTextConfigParam: { /** Format */ @@ -44062,7 +44447,7 @@ export interface components { */ status: "in_progress" | "completed" | "incomplete"; /** Tools */ - tools: (components["schemas"]["FunctionTool"] | components["schemas"]["FileSearchTool"] | components["schemas"]["ComputerTool"] | components["schemas"]["ComputerUsePreviewTool"] | components["schemas"]["WebSearchTool"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellTool"] | components["schemas"]["CustomTool"] | components["schemas"]["NamespaceTool"] | components["schemas"]["ToolSearchTool"] | components["schemas"]["WebSearchPreviewTool"] | components["schemas"]["ApplyPatchTool"])[]; + tools: (components["schemas"]["FunctionTool"] | components["schemas"]["FileSearchTool"] | components["schemas"]["ComputerTool"] | components["schemas"]["ComputerUsePreviewTool"] | components["schemas"]["WebSearchTool"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ProgrammaticToolCalling"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellTool"] | components["schemas"]["CustomTool"] | components["schemas"]["NamespaceTool"] | components["schemas"]["ToolSearchTool"] | components["schemas"]["WebSearchPreviewTool"] | components["schemas"]["ApplyPatchTool"])[]; /** * Type * @constant @@ -44085,7 +44470,7 @@ export interface components { /** Status */ status?: ("in_progress" | "completed" | "incomplete") | null; /** Tools */ - tools: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"])[]; + tools: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["ToolMcp"] | components["schemas"]["ToolCodeInterpreter"] | components["schemas"]["ToolProgrammaticToolCalling"] | components["schemas"]["ToolImageGeneration"] | components["schemas"]["ToolLocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["ToolSearchOutputNamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"])[]; /** * Type * @constant @@ -44104,7 +44489,7 @@ export interface components { /** Include */ include?: ("file_search_call.results" | "web_search_call.results" | "web_search_call.action.sources" | "message.input_image.image_url" | "computer_call_output.output.image_url" | "code_interpreter_call.outputs" | "reasoning.encrypted_content" | "message.output_text.logprobs")[] | null; /** Input */ - input: string | (components["schemas"]["EasyInputMessageParam"] | components["schemas"]["ResponsesAPIRequestParams_Message"] | components["schemas"]["ResponseOutputMessageParam"] | components["schemas"]["ResponseFileSearchToolCallParam"] | components["schemas"]["ResponseComputerToolCallParam"] | components["schemas"]["ComputerCallOutput"] | components["schemas"]["ResponseFunctionWebSearchParam"] | components["schemas"]["ResponseFunctionToolCallParam"] | components["schemas"]["FunctionCallOutput"] | components["schemas"]["ToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItemParamParam"] | components["schemas"]["ResponseReasoningItemParam"] | components["schemas"]["ResponseCompactionItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCallParam"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ShellCall"] | components["schemas"]["ShellCallOutput"] | components["schemas"]["ApplyPatchCall"] | components["schemas"]["ApplyPatchCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["ResponsesAPIRequestParams_McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallOutputParam"] | components["schemas"]["ResponseCustomToolCallParam"] | components["schemas"]["ItemReference"])[]; + input: string | (components["schemas"]["EasyInputMessageParam"] | components["schemas"]["ResponsesAPIRequestParams_Message"] | components["schemas"]["ResponseOutputMessageParam"] | components["schemas"]["ResponseFileSearchToolCallParam"] | components["schemas"]["ResponseComputerToolCallParam"] | components["schemas"]["ComputerCallOutput"] | components["schemas"]["ResponseFunctionWebSearchParam"] | components["schemas"]["ResponseFunctionToolCallParam"] | components["schemas"]["FunctionCallOutput"] | components["schemas"]["ToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_AdditionalTools"] | components["schemas"]["ResponseConfigurationUpdateItemParamParam"] | components["schemas"]["ResponseReasoningItemParam"] | components["schemas"]["ResponseCompactionItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCallParam"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ShellCall"] | components["schemas"]["ShellCallOutput"] | components["schemas"]["ApplyPatchCall"] | components["schemas"]["ApplyPatchCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["ResponsesAPIRequestParams_McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallOutputParam"] | components["schemas"]["ResponseCustomToolCallParam"] | components["schemas"]["CompactionTrigger"] | components["schemas"]["ItemReference"] | components["schemas"]["Program"] | components["schemas"]["ProgramOutput"])[]; /** Instructions */ instructions?: string | null; /** Max Output Tokens */ @@ -44129,7 +44514,7 @@ export interface components { prompt_cache_options?: components["schemas"]["PromptCacheOptions"] | null; /** Prompt Cache Retention */ prompt_cache_retention?: string | null; - reasoning?: components["schemas"]["Reasoning"] | null; + reasoning?: components["schemas"]["openai__types__shared_params__reasoning__Reasoning"] | null; /** Safety Identifier */ safety_identifier?: string | null; /** Service Tier */ @@ -44143,9 +44528,9 @@ export interface components { temperature?: number | null; text?: components["schemas"]["ResponseTextConfigParam"] | null; /** Tool Choice */ - tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; + tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceSpecificProgrammaticToolCallingParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; /** Tools */ - tools?: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"] | components["schemas"]["litellm__types__llms__openai__ComputerToolParam"] | components["schemas"]["ShellToolParam"] | components["schemas"]["OpenAIToolSearchToolParam"])[] | null; + tools?: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ProgrammaticToolCalling"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"] | components["schemas"]["litellm__types__llms__openai__ComputerToolParam"] | components["schemas"]["ShellToolParam"] | components["schemas"]["OpenAIToolSearchToolParam"])[] | null; /** Top Logprobs */ top_logprobs?: number | null; /** Top P */ @@ -44172,6 +44557,23 @@ export interface components { /** Y */ y: number; }; + /** AdditionalTools */ + ResponsesAPIRequestParams_AdditionalTools: { + /** Id */ + id?: string | null; + /** + * Role + * @constant + */ + role: "developer"; + /** Tools */ + tools: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ProgrammaticToolCalling"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"])[]; + /** + * Type + * @constant + */ + type: "additional_tools"; + }; /** * DoubleClick * @description A double click action. @@ -44194,10 +44596,23 @@ export interface components { * @description An image generation request made by the model. */ ResponsesAPIRequestParams_ImageGenerationCall: { + /** Action */ + action?: ("generate" | "edit" | "auto") | null; + /** Background */ + background?: ("transparent" | "opaque" | "auto") | null; + error?: components["schemas"]["ResponsesAPIRequestParams_ImageGenerationCallError"]; /** Id */ id: string; + /** Output Format */ + output_format?: ("png" | "webp" | "jpeg") | null; + /** Quality */ + quality?: ("low" | "medium" | "high" | "xhigh" | "max" | "auto") | null; /** Result */ result: string | null; + /** Revised Prompt */ + revised_prompt?: string | null; + /** Size */ + size?: string | ("1024x1024" | "1024x1536" | "1536x1024") | null; /** * Status * @enum {string} @@ -44209,6 +44624,21 @@ export interface components { */ type: "image_generation_call"; }; + /** + * ImageGenerationCallError + * @description Details of a moderation failure, when available. + */ + ResponsesAPIRequestParams_ImageGenerationCallError: { + /** Code */ + code: string | null; + /** Message */ + message: string; + moderation_details?: components["schemas"]["ImageGenerationCallErrorModerationDetails"]; + /** Param */ + param: string | null; + /** Type */ + type: string; + }; /** * McpApprovalResponse * @description A response to an MCP approval request. @@ -44277,7 +44707,7 @@ export interface components { /** Object */ object?: string | null; /** Output */ - output: (components["schemas"]["ResponseOutputMessage"] | components["schemas"]["ResponseFileSearchToolCall"] | components["schemas"]["ResponseFunctionToolCall"] | components["schemas"]["ResponseFunctionToolCallOutputItem"] | components["schemas"]["ResponseFunctionWebSearch"] | components["schemas"]["ResponseComputerToolCall"] | components["schemas"]["ResponseComputerToolCallOutputItem"] | components["schemas"]["ResponseReasoningItem"] | components["schemas"]["ResponseToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItem"] | components["schemas"]["ResponseCompactionItem"] | components["schemas"]["ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCall"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ResponseFunctionShellToolCall"] | components["schemas"]["ResponseFunctionShellToolCallOutput"] | components["schemas"]["ResponseApplyPatchToolCall"] | components["schemas"]["ResponseApplyPatchToolCallOutput"] | components["schemas"]["McpCall"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["McpApprovalResponse"] | components["schemas"]["ResponseCustomToolCall"] | components["schemas"]["ResponseCustomToolCallOutputItem"] | { + output: (components["schemas"]["ResponseOutputMessage"] | components["schemas"]["ResponseFileSearchToolCall"] | components["schemas"]["ResponseFunctionToolCall"] | components["schemas"]["ResponseFunctionToolCallOutputItem"] | components["schemas"]["ResponseFunctionWebSearch"] | components["schemas"]["ResponseComputerToolCall"] | components["schemas"]["ResponseComputerToolCallOutputItem"] | components["schemas"]["ResponseReasoningItem"] | components["schemas"]["Program"] | components["schemas"]["ProgramOutput"] | components["schemas"]["ResponseToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItem"] | components["schemas"]["AdditionalTools"] | components["schemas"]["ResponseCompactionItem"] | components["schemas"]["ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCall"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ResponseFunctionShellToolCall"] | components["schemas"]["ResponseFunctionShellToolCallOutput"] | components["schemas"]["ResponseApplyPatchToolCall"] | components["schemas"]["ResponseApplyPatchToolCallOutput"] | components["schemas"]["McpCall"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["McpApprovalResponse"] | components["schemas"]["ResponseCustomToolCall"] | components["schemas"]["ResponseCustomToolCallOutputItem"] | { [key: string]: unknown; })[] | (components["schemas"]["GenericResponseOutputItem"] | components["schemas"]["OutputCodeInterpreterCall"] | components["schemas"]["OutputFunctionToolCall"] | components["schemas"]["OutputImageGenerationCall"] | components["schemas"]["ResponseFunctionToolCall"] | components["schemas"]["ResponseFunctionWebSearch"] | components["schemas"]["CustomToolCallOutputItem"] | components["schemas"]["OutputToolSearchCall"])[]; /** Parallel Tool Calls */ @@ -44299,9 +44729,9 @@ export interface components { [key: string]: unknown; } | null; /** Tool Choice */ - tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; + tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceSpecificProgrammaticToolCallingParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; /** Tools */ - tools?: (components["schemas"]["FunctionTool"] | components["schemas"]["FileSearchTool"] | components["schemas"]["ComputerTool"] | components["schemas"]["ComputerUsePreviewTool"] | components["schemas"]["WebSearchTool"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellTool"] | components["schemas"]["CustomTool"] | components["schemas"]["NamespaceTool"] | components["schemas"]["ToolSearchTool"] | components["schemas"]["WebSearchPreviewTool"] | components["schemas"]["ApplyPatchTool"])[] | components["schemas"]["ResponseFunctionToolCall"][] | { + tools?: (components["schemas"]["FunctionTool"] | components["schemas"]["FileSearchTool"] | components["schemas"]["ComputerTool"] | components["schemas"]["ComputerUsePreviewTool"] | components["schemas"]["WebSearchTool"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ProgrammaticToolCalling"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellTool"] | components["schemas"]["CustomTool"] | components["schemas"]["NamespaceTool"] | components["schemas"]["ToolSearchTool"] | components["schemas"]["WebSearchPreviewTool"] | components["schemas"]["ApplyPatchTool"])[] | components["schemas"]["ResponseFunctionToolCall"][] | { [key: string]: unknown; }[] | null; /** Top P */ @@ -45450,6 +45880,8 @@ export interface components { action: components["schemas"]["ShellCallAction"]; /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["ShellCallCallerDirect"] | components["schemas"]["ShellCallCallerProgram"] | null; /** Environment */ environment?: components["schemas"]["LocalEnvironmentParam"] | components["schemas"]["ContainerReferenceParam"] | null; /** Id */ @@ -45474,6 +45906,24 @@ export interface components { /** Timeout Ms */ timeout_ms?: number | null; }; + /** ShellCallCallerDirect */ + ShellCallCallerDirect: { + /** + * Type + * @constant + */ + type: "direct"; + }; + /** ShellCallCallerProgram */ + ShellCallCallerProgram: { + /** Caller Id */ + caller_id: string; + /** + * Type + * @constant + */ + type: "program"; + }; /** * ShellCallOutput * @description The streamed output items emitted by a shell tool call. @@ -45481,6 +45931,8 @@ export interface components { ShellCallOutput: { /** Call Id */ call_id: string; + /** Caller */ + caller?: components["schemas"]["ShellCallOutputCallerDirect"] | components["schemas"]["ShellCallOutputCallerProgram"] | null; /** Id */ id?: string | null; /** Max Output Length */ @@ -45495,6 +45947,24 @@ export interface components { */ type: "shell_call_output"; }; + /** ShellCallOutputCallerDirect */ + ShellCallOutputCallerDirect: { + /** + * Type + * @constant + */ + type: "direct"; + }; + /** ShellCallOutputCallerProgram */ + ShellCallOutputCallerProgram: { + /** Caller Id */ + caller_id: string; + /** + * Type + * @constant + */ + type: "program"; + }; /** * ShellToolParam * @description Shell tool for Responses API: run commands in hosted containers or local runtime. @@ -47083,6 +47553,29 @@ export interface components { /** Tier */ tier: number; }; + /** + * Timeout + * @description Timeout configuration. + * + * **Usage**: + * + * Timeout(None) # No timeouts. + * Timeout(5.0) # 5s timeout on all operations. + * Timeout(None, connect=5.0) # 5s timeout on connect, no other timeouts. + * Timeout(5.0, connect=10.0) # 10s timeout on connect. 5s timeout elsewhere. + * Timeout(5.0, pool=None) # No timeout on acquiring connection from pool. + * # 5s timeout elsewhere. + */ + Timeout: { + /** Connect */ + connect: number | null; + /** Pool */ + pool: number | null; + /** Read */ + read: number | null; + /** Write */ + write: number | null; + }; /** * TokenCountDetailsResponse * @description Response structure for token count details with modality breakdown. @@ -47246,10 +47739,20 @@ export interface components { } & { [key: string]: unknown; }; + /** ToolChoiceSpecificProgrammaticToolCallingParam */ + ToolChoiceSpecificProgrammaticToolCallingParam: { + /** + * Type + * @constant + */ + type: "programmatic_tool_calling"; + } & { + [key: string]: unknown; + }; /** * ToolChoiceTypesParam * @description Indicates that the model should use a built-in tool to generate a response. - * [Learn more about built-in tools](https://platform.openai.com/docs/guides/tools). + * [Learn more about built-in tools](https://developers.openai.com/api/docs/guides/tools). */ ToolChoiceTypesParam: { /** @@ -47260,6 +47763,40 @@ export interface components { } & { [key: string]: unknown; }; + /** + * ToolCodeInterpreter + * @description A tool that runs Python code to help generate a response to a prompt. + */ + ToolCodeInterpreter: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; + /** Container */ + container: string | components["schemas"]["ToolCodeInterpreterContainerCodeInterpreterToolAuto"]; + /** + * Type + * @constant + */ + type: "code_interpreter"; + }; + /** + * ToolCodeInterpreterContainerCodeInterpreterToolAuto + * @description Configuration for a code interpreter container. + * + * Optionally specify the IDs of the files to run the code on. + */ + ToolCodeInterpreterContainerCodeInterpreterToolAuto: { + /** File Ids */ + file_ids?: string[]; + /** Memory Limit */ + memory_limit?: ("1g" | "4g" | "16g" | "64g") | null; + /** Network Policy */ + network_policy?: components["schemas"]["ContainerNetworkPolicyDisabledParam"] | components["schemas"]["ContainerNetworkPolicyAllowlistParam"]; + /** + * Type + * @constant + */ + type: "auto"; + }; /** ToolDetailResponse */ ToolDetailResponse: { /** Overrides */ @@ -47277,12 +47814,20 @@ export interface components { }; /** ToolFunction */ ToolFunction: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; + /** Async */ + async?: boolean | null; /** Defer Loading */ defer_loading?: boolean | null; /** Description */ description?: string | null; /** Name */ name: string; + /** Output Schema */ + output_schema?: { + [key: string]: unknown; + } | null; /** Parameters */ parameters?: unknown | null; /** Strict */ @@ -47295,6 +47840,66 @@ export interface components { } & { [key: string]: unknown; }; + /** + * ToolImageGeneration + * @description A tool that generates images using the GPT image models. + */ + ToolImageGeneration: { + /** + * Action + * @enum {string} + */ + action?: "generate" | "edit" | "auto"; + /** + * Background + * @enum {string} + */ + background?: "transparent" | "opaque" | "auto"; + /** Input Fidelity */ + input_fidelity?: ("high" | "low") | null; + input_image_mask?: components["schemas"]["ToolImageGenerationInputImageMask"]; + /** Model */ + model?: string | ("gpt-image-1" | "gpt-image-1-mini" | "gpt-image-2" | "gpt-image-2-2026-04-21" | "gpt-image-2.5-sunburst" | "gpt-image-2.5-sunburst-2026-09-08" | "gpt-image-2.5-flare" | "gpt-image-2.5-flare-2026-09-08" | "gpt-image-1.5" | "chatgpt-image-latest"); + /** + * Moderation + * @enum {string} + */ + moderation?: "auto" | "low"; + /** Output Compression */ + output_compression?: number; + /** + * Output Format + * @enum {string} + */ + output_format?: "png" | "webp" | "jpeg"; + /** Partial Images */ + partial_images?: number; + /** + * Quality + * @enum {string} + */ + quality?: "low" | "medium" | "high" | "xhigh" | "max" | "auto"; + /** Size */ + size?: string | ("1024x1024" | "1024x1536" | "1536x1024" | "auto"); + /** + * Type + * @constant + */ + type: "image_generation"; + }; + /** + * ToolImageGenerationInputImageMask + * @description Optional mask for inpainting. + * + * Contains `image_url` + * (string, optional) and `file_id` (string, optional). + */ + ToolImageGenerationInputImageMask: { + /** File Id */ + file_id?: string; + /** Image Url */ + image_url?: string; + }; /** ToolListResponse */ ToolListResponse: { /** Tools */ @@ -47302,6 +47907,98 @@ export interface components { /** Total */ total: number; }; + /** + * ToolLocalShell + * @description A tool that allows the model to execute shell commands in a local environment. + */ + ToolLocalShell: { + /** + * Type + * @constant + */ + type: "local_shell"; + }; + /** + * ToolMcp + * @description Give the model access to additional tools via remote Model Context Protocol + * (MCP) servers. [Learn more about MCP](https://developers.openai.com/api/docs/guides/tools-connectors-mcp). + */ + ToolMcp: { + /** Allowed Callers */ + allowed_callers?: ("direct" | "programmatic")[] | null; + /** Allowed Tools */ + allowed_tools?: string[] | components["schemas"]["ToolMcpAllowedToolsMcpToolFilter"] | null; + /** Authorization */ + authorization?: string; + /** + * Connector Id + * @enum {string} + */ + connector_id?: "connector_dropbox" | "connector_gmail" | "connector_googlecalendar" | "connector_googledrive" | "connector_microsoftteams" | "connector_outlookcalendar" | "connector_outlookemail" | "connector_sharepoint"; + /** Defer Loading */ + defer_loading?: boolean; + /** Headers */ + headers?: { + [key: string]: string; + } | null; + /** Require Approval */ + require_approval?: components["schemas"]["ToolMcpRequireApprovalMcpToolApprovalFilter"] | ("always" | "never") | null; + /** Server Description */ + server_description?: string; + /** Server Label */ + server_label: string; + /** Server Url */ + server_url?: string; + /** Tunnel Id */ + tunnel_id?: string; + /** + * Type + * @constant + */ + type: "mcp"; + }; + /** + * ToolMcpAllowedToolsMcpToolFilter + * @description A filter object to specify which tools are allowed. + */ + ToolMcpAllowedToolsMcpToolFilter: { + /** Read Only */ + read_only?: boolean; + /** Tool Names */ + tool_names?: string[]; + }; + /** + * ToolMcpRequireApprovalMcpToolApprovalFilter + * @description Specify which of the MCP server's tools require approval. + * + * Can be + * `always`, `never`, or a filter object associated with tools + * that require approval. + */ + ToolMcpRequireApprovalMcpToolApprovalFilter: { + always?: components["schemas"]["ToolMcpRequireApprovalMcpToolApprovalFilterAlways"]; + never?: components["schemas"]["ToolMcpRequireApprovalMcpToolApprovalFilterNever"]; + }; + /** + * ToolMcpRequireApprovalMcpToolApprovalFilterAlways + * @description A filter object to specify which tools are allowed. + */ + ToolMcpRequireApprovalMcpToolApprovalFilterAlways: { + /** Read Only */ + read_only?: boolean; + /** Tool Names */ + tool_names?: string[]; + }; + /** + * ToolMcpRequireApprovalMcpToolApprovalFilterNever + * @description A filter object to specify which tools are allowed. + */ + ToolMcpRequireApprovalMcpToolApprovalFilterNever: { + /** Read Only */ + read_only?: boolean; + /** Tool Names */ + tool_names?: string[]; + }; /** * ToolPermissionRule * @description A rule defining permission for a specific tool or tool pattern @@ -47405,6 +48102,14 @@ export interface components { /** Updated */ updated: boolean; }; + /** ToolProgrammaticToolCalling */ + ToolProgrammaticToolCalling: { + /** + * Type + * @constant + */ + type: "programmatic_tool_calling"; + }; /** ToolSearchCall */ ToolSearchCall: { /** Arguments */ @@ -47426,6 +48131,23 @@ export interface components { */ type: "tool_search_call"; }; + /** + * ToolSearchOutputNamespaceToolParam + * @description Groups function/custom tools under a shared namespace. + */ + ToolSearchOutputNamespaceToolParam: { + /** Description */ + description: string; + /** Name */ + name: string; + /** Tools */ + tools: (components["schemas"]["ToolFunction"] | components["schemas"]["CustomToolParam"])[]; + /** + * Type + * @constant + */ + type: "namespace"; + }; /** * ToolSearchTool * @description Hosted or BYOT tool search configuration for deferred tools. @@ -50046,7 +50768,7 @@ export interface components { * WebSearchPreviewTool * @description This tool searches the web for relevant results to use in a response. * - * Learn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search). + * Learn more about the [web search tool](https://developers.openai.com/api/docs/guides/tools-web-search). */ WebSearchPreviewTool: { /** Search Content Types */ @@ -50066,7 +50788,7 @@ export interface components { * WebSearchPreviewToolParam * @description This tool searches the web for relevant results to use in a response. * - * Learn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search). + * Learn more about the [web search tool](https://developers.openai.com/api/docs/guides/tools-web-search). */ WebSearchPreviewToolParam: { /** Search Content Types */ @@ -50088,9 +50810,11 @@ export interface components { * @description Search the Internet for sources related to the prompt. * * Learn more about the - * [web search tool](https://platform.openai.com/docs/guides/tools-web-search). + * [web search tool](https://developers.openai.com/api/docs/guides/tools-web-search). */ WebSearchTool: { + /** External Web Access */ + external_web_access?: boolean | null; filters?: components["schemas"]["Filters"] | null; /** Search Context Size */ search_context_size?: ("low" | "medium" | "high") | null; @@ -50108,9 +50832,11 @@ export interface components { * @description Search the Internet for sources related to the prompt. * * Learn more about the - * [web search tool](https://platform.openai.com/docs/guides/tools-web-search). + * [web search tool](https://developers.openai.com/api/docs/guides/tools-web-search). */ WebSearchToolParam: { + /** External Web Access */ + external_web_access?: boolean; filters?: components["schemas"]["Filters"] | null; /** * Search Context Size @@ -50303,7 +51029,7 @@ export interface components { * ComputerToolParam * @description A tool that controls a virtual computer. * - * Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use). + * Learn more about the [computer tool](https://developers.openai.com/api/docs/guides/tools-computer-use). */ openai__types__responses__computer_tool_param__ComputerToolParam: { /** @@ -50312,9 +51038,19 @@ export interface components { */ type: "computer"; }; + /** + * Reasoning + * @description Updates to reasoning configuration. Only effort is supported. + */ + openai__types__responses__response_configuration_update_item_param_param__Reasoning: { + /** Effort */ + effort?: ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max") | null; + }; /** * UserLocation - * @description The user's location. + * @description The approximate location of the user. + * + * If omitted or null, defaults to the United States. To avoid this fallback, pass `{"type": "approximate"}` without location fields. To localize results, provide the relevant location fields. */ openai__types__responses__web_search_preview_tool__UserLocation: { /** City */ @@ -50335,7 +51071,9 @@ export interface components { }; /** * UserLocation - * @description The user's location. + * @description The approximate location of the user. + * + * If omitted or null, defaults to the United States. To avoid this fallback, pass `{"type": "approximate"}` without location fields. To localize results, provide the relevant location fields. */ openai__types__responses__web_search_preview_tool_param__UserLocation: { /** City */ @@ -50355,6 +51093,10 @@ export interface components { /** * UserLocation * @description The approximate location of the user. + * + * If omitted or null, defaults to the + * United States. To avoid this fallback, pass `{"type": "approximate"}` without + * location fields. To localize results, provide the relevant location fields. */ openai__types__responses__web_search_tool__UserLocation: { /** City */ @@ -50373,6 +51115,10 @@ export interface components { /** * UserLocation * @description The approximate location of the user. + * + * If omitted or null, defaults to the + * United States. To avoid this fallback, pass `{"type": "approximate"}` without + * location fields. To localize results, provide the relevant location fields. */ openai__types__responses__web_search_tool_param__UserLocation: { /** City */ @@ -50389,6 +51135,23 @@ export interface components { */ type?: "approximate"; }; + /** + * Reasoning + * @description Configuration options for + * [reasoning models](https://developers.openai.com/api/docs/guides/reasoning). + */ + openai__types__shared_params__reasoning__Reasoning: { + /** Context */ + context?: ("auto" | "current_turn" | "all_turns") | null; + /** Effort */ + effort?: ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max") | null; + /** Generate Summary */ + generate_summary?: ("auto" | "concise" | "detailed") | null; + /** Mode */ + mode?: string | ("standard" | "pro"); + /** Summary */ + summary?: ("auto" | "concise" | "detailed") | null; + }; /** updateDeployment */ updateDeployment: { /** Blocked */ @@ -50890,7 +51653,7 @@ export interface components { [key: string]: unknown; }[] | null; /** Timeout */ - timeout?: number | string | null; + timeout?: number | string | components["schemas"]["Timeout"] | null; /** Timeout By Service Tier */ timeout_by_service_tier?: { [key: string]: number; @@ -66715,7 +67478,7 @@ export interface operations { /** Include */ include?: ("file_search_call.results" | "web_search_call.results" | "web_search_call.action.sources" | "message.input_image.image_url" | "computer_call_output.output.image_url" | "code_interpreter_call.outputs" | "reasoning.encrypted_content" | "message.output_text.logprobs")[] | null; /** Input */ - input: string | (components["schemas"]["EasyInputMessageParam"] | components["schemas"]["ResponsesAPIRequestParams_Message"] | components["schemas"]["ResponseOutputMessageParam"] | components["schemas"]["ResponseFileSearchToolCallParam"] | components["schemas"]["ResponseComputerToolCallParam"] | components["schemas"]["ComputerCallOutput"] | components["schemas"]["ResponseFunctionWebSearchParam"] | components["schemas"]["ResponseFunctionToolCallParam"] | components["schemas"]["FunctionCallOutput"] | components["schemas"]["ToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItemParamParam"] | components["schemas"]["ResponseReasoningItemParam"] | components["schemas"]["ResponseCompactionItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCallParam"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ShellCall"] | components["schemas"]["ShellCallOutput"] | components["schemas"]["ApplyPatchCall"] | components["schemas"]["ApplyPatchCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["ResponsesAPIRequestParams_McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallOutputParam"] | components["schemas"]["ResponseCustomToolCallParam"] | components["schemas"]["ItemReference"])[]; + input: string | (components["schemas"]["EasyInputMessageParam"] | components["schemas"]["ResponsesAPIRequestParams_Message"] | components["schemas"]["ResponseOutputMessageParam"] | components["schemas"]["ResponseFileSearchToolCallParam"] | components["schemas"]["ResponseComputerToolCallParam"] | components["schemas"]["ComputerCallOutput"] | components["schemas"]["ResponseFunctionWebSearchParam"] | components["schemas"]["ResponseFunctionToolCallParam"] | components["schemas"]["FunctionCallOutput"] | components["schemas"]["ToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_AdditionalTools"] | components["schemas"]["ResponseConfigurationUpdateItemParamParam"] | components["schemas"]["ResponseReasoningItemParam"] | components["schemas"]["ResponseCompactionItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCallParam"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ShellCall"] | components["schemas"]["ShellCallOutput"] | components["schemas"]["ApplyPatchCall"] | components["schemas"]["ApplyPatchCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["ResponsesAPIRequestParams_McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallOutputParam"] | components["schemas"]["ResponseCustomToolCallParam"] | components["schemas"]["CompactionTrigger"] | components["schemas"]["ItemReference"] | components["schemas"]["Program"] | components["schemas"]["ProgramOutput"])[]; /** Instructions */ instructions?: string | null; /** Max Output Tokens */ @@ -66740,7 +67503,7 @@ export interface operations { prompt_cache_options?: components["schemas"]["PromptCacheOptions"] | null; /** Prompt Cache Retention */ prompt_cache_retention?: string | null; - reasoning?: components["schemas"]["Reasoning"] | null; + reasoning?: components["schemas"]["openai__types__shared_params__reasoning__Reasoning"] | null; /** Safety Identifier */ safety_identifier?: string | null; /** Service Tier */ @@ -66754,9 +67517,9 @@ export interface operations { temperature?: number | null; text?: components["schemas"]["ResponseTextConfigParam"] | null; /** Tool Choice */ - tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; + tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceSpecificProgrammaticToolCallingParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; /** Tools */ - tools?: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"] | components["schemas"]["litellm__types__llms__openai__ComputerToolParam"] | components["schemas"]["ShellToolParam"] | components["schemas"]["OpenAIToolSearchToolParam"])[] | null; + tools?: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ProgrammaticToolCalling"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"] | components["schemas"]["litellm__types__llms__openai__ComputerToolParam"] | components["schemas"]["ShellToolParam"] | components["schemas"]["OpenAIToolSearchToolParam"])[] | null; /** Top Logprobs */ top_logprobs?: number | null; /** Top P */ @@ -70179,7 +70942,7 @@ export interface operations { /** Include */ include?: ("file_search_call.results" | "web_search_call.results" | "web_search_call.action.sources" | "message.input_image.image_url" | "computer_call_output.output.image_url" | "code_interpreter_call.outputs" | "reasoning.encrypted_content" | "message.output_text.logprobs")[] | null; /** Input */ - input: string | (components["schemas"]["EasyInputMessageParam"] | components["schemas"]["ResponsesAPIRequestParams_Message"] | components["schemas"]["ResponseOutputMessageParam"] | components["schemas"]["ResponseFileSearchToolCallParam"] | components["schemas"]["ResponseComputerToolCallParam"] | components["schemas"]["ComputerCallOutput"] | components["schemas"]["ResponseFunctionWebSearchParam"] | components["schemas"]["ResponseFunctionToolCallParam"] | components["schemas"]["FunctionCallOutput"] | components["schemas"]["ToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItemParamParam"] | components["schemas"]["ResponseReasoningItemParam"] | components["schemas"]["ResponseCompactionItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCallParam"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ShellCall"] | components["schemas"]["ShellCallOutput"] | components["schemas"]["ApplyPatchCall"] | components["schemas"]["ApplyPatchCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["ResponsesAPIRequestParams_McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallOutputParam"] | components["schemas"]["ResponseCustomToolCallParam"] | components["schemas"]["ItemReference"])[]; + input: string | (components["schemas"]["EasyInputMessageParam"] | components["schemas"]["ResponsesAPIRequestParams_Message"] | components["schemas"]["ResponseOutputMessageParam"] | components["schemas"]["ResponseFileSearchToolCallParam"] | components["schemas"]["ResponseComputerToolCallParam"] | components["schemas"]["ComputerCallOutput"] | components["schemas"]["ResponseFunctionWebSearchParam"] | components["schemas"]["ResponseFunctionToolCallParam"] | components["schemas"]["FunctionCallOutput"] | components["schemas"]["ToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_AdditionalTools"] | components["schemas"]["ResponseConfigurationUpdateItemParamParam"] | components["schemas"]["ResponseReasoningItemParam"] | components["schemas"]["ResponseCompactionItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCallParam"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ShellCall"] | components["schemas"]["ShellCallOutput"] | components["schemas"]["ApplyPatchCall"] | components["schemas"]["ApplyPatchCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["ResponsesAPIRequestParams_McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallOutputParam"] | components["schemas"]["ResponseCustomToolCallParam"] | components["schemas"]["CompactionTrigger"] | components["schemas"]["ItemReference"] | components["schemas"]["Program"] | components["schemas"]["ProgramOutput"])[]; /** Instructions */ instructions?: string | null; /** Max Output Tokens */ @@ -70204,7 +70967,7 @@ export interface operations { prompt_cache_options?: components["schemas"]["PromptCacheOptions"] | null; /** Prompt Cache Retention */ prompt_cache_retention?: string | null; - reasoning?: components["schemas"]["Reasoning"] | null; + reasoning?: components["schemas"]["openai__types__shared_params__reasoning__Reasoning"] | null; /** Safety Identifier */ safety_identifier?: string | null; /** Service Tier */ @@ -70218,9 +70981,9 @@ export interface operations { temperature?: number | null; text?: components["schemas"]["ResponseTextConfigParam"] | null; /** Tool Choice */ - tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; + tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceSpecificProgrammaticToolCallingParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; /** Tools */ - tools?: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"] | components["schemas"]["litellm__types__llms__openai__ComputerToolParam"] | components["schemas"]["ShellToolParam"] | components["schemas"]["OpenAIToolSearchToolParam"])[] | null; + tools?: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ProgrammaticToolCalling"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"] | components["schemas"]["litellm__types__llms__openai__ComputerToolParam"] | components["schemas"]["ShellToolParam"] | components["schemas"]["OpenAIToolSearchToolParam"])[] | null; /** Top Logprobs */ top_logprobs?: number | null; /** Top P */ @@ -81010,7 +81773,7 @@ export interface operations { /** Include */ include?: ("file_search_call.results" | "web_search_call.results" | "web_search_call.action.sources" | "message.input_image.image_url" | "computer_call_output.output.image_url" | "code_interpreter_call.outputs" | "reasoning.encrypted_content" | "message.output_text.logprobs")[] | null; /** Input */ - input: string | (components["schemas"]["EasyInputMessageParam"] | components["schemas"]["ResponsesAPIRequestParams_Message"] | components["schemas"]["ResponseOutputMessageParam"] | components["schemas"]["ResponseFileSearchToolCallParam"] | components["schemas"]["ResponseComputerToolCallParam"] | components["schemas"]["ComputerCallOutput"] | components["schemas"]["ResponseFunctionWebSearchParam"] | components["schemas"]["ResponseFunctionToolCallParam"] | components["schemas"]["FunctionCallOutput"] | components["schemas"]["ToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItemParamParam"] | components["schemas"]["ResponseReasoningItemParam"] | components["schemas"]["ResponseCompactionItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCallParam"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ShellCall"] | components["schemas"]["ShellCallOutput"] | components["schemas"]["ApplyPatchCall"] | components["schemas"]["ApplyPatchCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["ResponsesAPIRequestParams_McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallOutputParam"] | components["schemas"]["ResponseCustomToolCallParam"] | components["schemas"]["ItemReference"])[]; + input: string | (components["schemas"]["EasyInputMessageParam"] | components["schemas"]["ResponsesAPIRequestParams_Message"] | components["schemas"]["ResponseOutputMessageParam"] | components["schemas"]["ResponseFileSearchToolCallParam"] | components["schemas"]["ResponseComputerToolCallParam"] | components["schemas"]["ComputerCallOutput"] | components["schemas"]["ResponseFunctionWebSearchParam"] | components["schemas"]["ResponseFunctionToolCallParam"] | components["schemas"]["FunctionCallOutput"] | components["schemas"]["ToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_AdditionalTools"] | components["schemas"]["ResponseConfigurationUpdateItemParamParam"] | components["schemas"]["ResponseReasoningItemParam"] | components["schemas"]["ResponseCompactionItemParamParam"] | components["schemas"]["ResponsesAPIRequestParams_ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCallParam"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ShellCall"] | components["schemas"]["ShellCallOutput"] | components["schemas"]["ApplyPatchCall"] | components["schemas"]["ApplyPatchCallOutput"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["ResponsesAPIRequestParams_McpApprovalResponse"] | components["schemas"]["McpCall"] | components["schemas"]["ResponseCustomToolCallOutputParam"] | components["schemas"]["ResponseCustomToolCallParam"] | components["schemas"]["CompactionTrigger"] | components["schemas"]["ItemReference"] | components["schemas"]["Program"] | components["schemas"]["ProgramOutput"])[]; /** Instructions */ instructions?: string | null; /** Max Output Tokens */ @@ -81035,7 +81798,7 @@ export interface operations { prompt_cache_options?: components["schemas"]["PromptCacheOptions"] | null; /** Prompt Cache Retention */ prompt_cache_retention?: string | null; - reasoning?: components["schemas"]["Reasoning"] | null; + reasoning?: components["schemas"]["openai__types__shared_params__reasoning__Reasoning"] | null; /** Safety Identifier */ safety_identifier?: string | null; /** Service Tier */ @@ -81049,9 +81812,9 @@ export interface operations { temperature?: number | null; text?: components["schemas"]["ResponseTextConfigParam"] | null; /** Tool Choice */ - tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; + tool_choice?: ("none" | "auto" | "required") | components["schemas"]["ToolChoiceAllowedParam"] | components["schemas"]["ToolChoiceTypesParam"] | components["schemas"]["ToolChoiceFunctionParam"] | components["schemas"]["ToolChoiceMcpParam"] | components["schemas"]["ToolChoiceCustomParam"] | components["schemas"]["ToolChoiceSpecificProgrammaticToolCallingParam"] | components["schemas"]["ToolChoiceApplyPatchParam"] | components["schemas"]["ToolChoiceShellParam"] | null; /** Tools */ - tools?: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"] | components["schemas"]["litellm__types__llms__openai__ComputerToolParam"] | components["schemas"]["ShellToolParam"] | components["schemas"]["OpenAIToolSearchToolParam"])[] | null; + tools?: (components["schemas"]["FunctionToolParam"] | components["schemas"]["FileSearchToolParam"] | components["schemas"]["openai__types__responses__computer_tool_param__ComputerToolParam"] | components["schemas"]["ComputerUsePreviewToolParam"] | components["schemas"]["WebSearchToolParam"] | components["schemas"]["Mcp"] | components["schemas"]["CodeInterpreter"] | components["schemas"]["ProgrammaticToolCalling"] | components["schemas"]["ImageGeneration"] | components["schemas"]["LocalShell"] | components["schemas"]["FunctionShellToolParam"] | components["schemas"]["CustomToolParam"] | components["schemas"]["NamespaceToolParam"] | components["schemas"]["ToolSearchToolParam"] | components["schemas"]["WebSearchPreviewToolParam"] | components["schemas"]["ApplyPatchToolParam"] | components["schemas"]["litellm__types__llms__openai__ComputerToolParam"] | components["schemas"]["ShellToolParam"] | components["schemas"]["OpenAIToolSearchToolParam"])[] | null; /** Top Logprobs */ top_logprobs?: number | null; /** Top P */ From 79f1531aa07410bb30fc69c1811a65d9cef565d0 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:07:58 -0700 Subject: [PATCH 08/23] fix(aiohttp): yield bytes already received before raising on a dropped stream aiohttp read methods raise the stored payload error before returning buffered bytes, so a stream whose connection dropped after the final SSE events lost them. The transport now drains the reader's buffer once and yields it before raising httpx.ReadError, and falls back to the old behavior if aiohttp's internals change --- .../llms/custom_httpx/aiohttp_transport.py | 41 ++++-- .../custom_httpx/test_aiohttp_transport.py | 127 ++++++++++++++++-- 2 files changed, 148 insertions(+), 20 deletions(-) diff --git a/litellm/llms/custom_httpx/aiohttp_transport.py b/litellm/llms/custom_httpx/aiohttp_transport.py index b8cb183c02f..57860d4332e 100644 --- a/litellm/llms/custom_httpx/aiohttp_transport.py +++ b/litellm/llms/custom_httpx/aiohttp_transport.py @@ -109,6 +109,32 @@ def map_aiohttp_exceptions() -> Generator[None, None, None]: raise mapped_exc(message) from exc +@contextlib.contextmanager +def _map_stream_read_errors() -> Generator[None, None, None]: + try: + yield + except RuntimeError as e: + if "Connection closed" not in str(e): + raise + raise httpx.ReadError(str(e)) from e + except aiohttp.http_exceptions.TransferEncodingError as e: + raise httpx.ReadError(str(e)) from e + except Exception: + with map_aiohttp_exceptions(): + raise + + +def _received_unread_bytes(content: aiohttp.StreamReader) -> bytes: + read_buffer: Final[Callable[[int], object] | None] = getattr(content, "_read_nowait", None) + if read_buffer is None: + return b"" + try: + unread: Final = read_buffer(-1) + except (AttributeError, TypeError, TimeoutError): + return b"" + return unread if isinstance(unread, bytes) else b"" + + class AiohttpResponseStream(httpx.AsyncByteStream): CHUNK_SIZE = 1024 * 16 @@ -116,18 +142,15 @@ class AiohttpResponseStream(httpx.AsyncByteStream): self._aiohttp_response = aiohttp_response async def __aiter__(self) -> typing.AsyncIterator[bytes]: + content: Final = self._aiohttp_response.content try: - async for chunk in self._aiohttp_response.content.iter_chunked(self.CHUNK_SIZE): + async for chunk in content.iter_chunked(self.CHUNK_SIZE): yield chunk - except RuntimeError as e: - if "Connection closed" not in str(e): - raise - raise httpx.ReadError(str(e)) from e - except aiohttp.http_exceptions.TransferEncodingError as e: - raise httpx.ReadError(str(e)) from e except Exception: - # For other exceptions, use the normal mapping - with map_aiohttp_exceptions(): + unread: Final = _received_unread_bytes(content) + if unread: + yield unread + with _map_stream_read_errors(): raise finally: # Release the aiohttp connection when iteration ends for any diff --git a/tests/unit/llms/custom_httpx/test_aiohttp_transport.py b/tests/unit/llms/custom_httpx/test_aiohttp_transport.py index 0765371dc4a..5696afcf89e 100644 --- a/tests/unit/llms/custom_httpx/test_aiohttp_transport.py +++ b/tests/unit/llms/custom_httpx/test_aiohttp_transport.py @@ -1276,6 +1276,20 @@ async def test_refused_connection_maps_to_httpx_connect_error( assert (cause.host, cause.port) == ("127.0.0.1", port) +async def _read_request_headers(reader: asyncio.StreamReader) -> bytes: + headers: Final = await reader.readuntil(b"\r\n\r\n") + content_length: Final = next( + ( + int(header.split(b":", 1)[1]) + for header in headers.split(b"\r\n") + if header.lower().startswith(b"content-length:") + ), + 0, + ) + await reader.readexactly(content_length) + return headers + + @pytest.mark.asyncio async def test_reset_after_request_is_not_replayed(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("NO_PROXY", "127.0.0.1") @@ -1283,17 +1297,7 @@ async def test_reset_after_request_is_not_replayed(monkeypatch: pytest.MonkeyPat received_requests: Final[list[bytes]] = [] # mutable-ok: local server captures requests async def reset_after_request(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: - headers: Final = await reader.readuntil(b"\r\n\r\n") - content_length: Final = next( - ( - int(header.split(b":", 1)[1]) - for header in headers.split(b"\r\n") - if header.lower().startswith(b"content-length:") - ), - 0, - ) - await reader.readexactly(content_length) - received_requests.append(headers) + received_requests.append(await _read_request_headers(reader)) transport_socket: Final = writer.get_extra_info("socket") assert transport_socket is not None transport_socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) @@ -1310,3 +1314,104 @@ async def test_reset_after_request_is_not_replayed(monkeypatch: pytest.MonkeyPat assert len(received_requests) == 1 finally: await handler.close() + + +_SSE_EVENTS_BEFORE_DROP: Final = ( + b'data: {"id":"chatcmpl-drop","object":"chat.completion.chunk","created":1,"model":"gpt-5.6",' + b'"choices":[{"index":0,"delta":{"role":"assistant","content":"1 2 "},"finish_reason":null}]}\n\n', + b'data: {"id":"chatcmpl-drop","object":"chat.completion.chunk","created":1,"model":"gpt-5.6",' + b'"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n', +) + + +async def _stream_events_then_drop_before_done(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + await _read_request_headers(reader) + chunked_events: Final = b"".join(b"%x\r\n%s\r\n" % (len(event), event) for event in _SSE_EVENTS_BEFORE_DROP) + writer.write( + b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nTransfer-Encoding: chunked\r\n\r\n" + chunked_events + ) + await writer.drain() + writer.close() + await writer.wait_closed() + + +async def _until_connection_drop_is_received(content: aiohttp.StreamReader) -> None: + while content.exception() is None: + await asyncio.sleep(0) + + +async def _read_until_error(stream: httpx.AsyncByteStream) -> tuple[bytes, Exception | None]: + chunks: Final[list[bytes]] = [] # mutable-ok: keeps the chunks read before the stream raises + try: + async for chunk in stream: + chunks.append(chunk) + except Exception as error: + return b"".join(chunks), error + return b"".join(chunks), None + + +@pytest.mark.asyncio +async def test_bytes_received_before_a_connection_drop_are_yielded_before_read_error() -> None: + server: Final = await asyncio.start_server(_stream_events_then_drop_before_done, "127.0.0.1", 0) + port: Final = server.sockets[0].getsockname()[1] + async with server, aiohttp.ClientSession() as session: + aiohttp_response: Final = await session.post(f"http://127.0.0.1:{port}/v1/chat/completions", data=b"{}") + await _until_connection_drop_is_received(aiohttp_response.content) + received, error = await _read_until_error(AiohttpResponseStream(aiohttp_response)) + + assert received == b"".join(_SSE_EVENTS_BEFORE_DROP) + assert isinstance(error, httpx.ReadError) + + +@pytest.mark.asyncio +async def test_finished_stream_dropped_before_done_ends_cleanly_through_acompletion( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("NO_PROXY", "127.0.0.1") + monkeypatch.setenv("no_proxy", "127.0.0.1") + upstream_responses: Final[list[aiohttp.ClientResponse]] = [] # mutable-ok: trace hook captures the response + + async def capture_response( + _session: ClientSession, _context: object, params: aiohttp.TraceRequestEndParams + ) -> None: + upstream_responses.append(params.response) + + trace_config: Final = aiohttp.TraceConfig() + trace_config.on_request_end.append(capture_response) + server: Final = await asyncio.start_server(_stream_events_then_drop_before_done, "127.0.0.1", 0) + port: Final = server.sockets[0].getsockname()[1] + async with server, aiohttp.ClientSession(trace_configs=[trace_config]) as session: + stream: Final = await litellm.acompletion( + model="openai/gpt-5.6", + api_base=f"http://127.0.0.1:{port}/v1", + api_key="sk-local-test", + messages=[{"role": "user", "content": "count to two"}], + stream=True, + shared_session=session, + ) + await _until_connection_drop_is_received(upstream_responses[0].content) + chunks: Final = [chunk async for chunk in stream] + + assert "".join(chunk.choices[0].delta.content or "" for chunk in chunks) == "1 2 " + assert chunks[-1].choices[0].finish_reason == "stop" + + +class _ContentWithChangedBufferInternals(MockContent): + def _read_nowait(self, n: int) -> bytes: + raise TypeError(f"_read_nowait({n}) no longer drains the buffer") + + +@pytest.mark.asyncio +async def test_payload_error_still_maps_to_read_error_when_buffer_drain_breaks() -> None: + mock_response: Final = MockAiohttpResponse() + mock_response.content = _ContentWithChangedBufferInternals( + chunks=[b"chunk1", b"chunk2"], + exception_to_raise=aiohttp.ClientPayloadError("Response payload is not completed"), + exception_at_chunk=1, + ) + + received, error = await _read_until_error(AiohttpResponseStream(mock_response)) # pyright: ignore[reportArgumentType] # duck-typed aiohttp response double + + assert received == b"chunk1" + assert isinstance(error, httpx.ReadError) + assert mock_response.closed is True From 924ac590b18ec96ca487304eaf0762da57ebc2f3 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:42:15 -0700 Subject: [PATCH 09/23] fix(openai): end a dropped stream only after every requested choice finished, keep Azure deployment URL query params, and narrow scope A dropped connection now ends an n>1 stream cleanly only once each requested choice has sent its finish_reason. A pasted Azure deployment URL keeps its extra query params on SDK requests. The new SDK 3 refusal codes map to 500, and the file download change goes back to main. --- litellm/files/streaming.py | 33 +------ .../litellm_core_utils/streaming_handler.py | 33 ++++++- litellm/llms/azure/common_utils.py | 35 ++++---- litellm/responses/streaming_iterator.py | 6 +- .../test_streaming_handler.py | 47 +++++++++- .../llms/azure/test_azure_common_utils.py | 32 +++++++ .../test_openai_file_content_streaming.py | 86 +------------------ .../test_streaming_iterator_error_events.py | 6 +- 8 files changed, 138 insertions(+), 140 deletions(-) diff --git a/litellm/files/streaming.py b/litellm/files/streaming.py index e7139589840..e9a030e0e32 100644 --- a/litellm/files/streaming.py +++ b/litellm/files/streaming.py @@ -5,9 +5,7 @@ from typing import TYPE_CHECKING, Final, Optional, cast import anyio -from litellm.exceptions import APIConnectionError, Timeout from litellm.files.types import FileContentProvider -from litellm.llms.custom_httpx.transport_errors import TransportErrorKind, chained_transport_error_kind if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import ( @@ -61,11 +59,8 @@ class FileContentStreamingResponse: self._log_success_sync() raise except Exception as e: - public_error: Final = self._public_error(e) - self._log_failure_sync(public_error) - if public_error is e: - raise - raise public_error from e + self._log_failure_sync(e) + raise def __aiter__(self) -> "FileContentStreamingResponse": if not hasattr(self.stream_iterator, "__anext__"): @@ -82,28 +77,8 @@ class FileContentStreamingResponse: await self._log_success_async() raise except Exception as e: - public_error: Final = self._public_error(e) - await self._log_failure_async(public_error) - if public_error is e: - raise - raise public_error from e - - def _public_error(self, error: Exception) -> Exception: - match chained_transport_error_kind(error): - case TransportErrorKind.TIMEOUT: - return Timeout( - message=f"timed out reading file content: {error}", - model=self.model, - llm_provider=self.custom_llm_provider, - ) - case TransportErrorKind.DROPPED_CONNECTION: - return APIConnectionError( - message=f"connection dropped while reading file content: {error}", - llm_provider=self.custom_llm_provider, - model=self.model, - ) - case None: - return error + await self._log_failure_async(e) + raise async def aclose(self) -> None: if self._close_completed: diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 32421f8926b..214f4803f1a 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -93,6 +93,16 @@ def _next_sync_or_exhausted(it: Iterator[object]) -> object: return _SYNC_ITER_EXHAUSTED +def _first_choice_index(chunk: ChatCompletionChunk | ModelResponseStream) -> int: + match chunk: + case ChatCompletionChunk(choices=[first, *_]): + return first.index + case ModelResponseStream(choices=[first, *_]): + return first.index + case _: + return 0 + + def _stamp_served_service_tier(response: ModelResponseStream, complete_streaming_response: ModelResponse) -> None: served_tier: Final = complete_streaming_response.model_dump().get("service_tier") if isinstance(served_tier, str) and served_tier: @@ -333,6 +343,7 @@ class CustomStreamWrapper: self._provider_response_model: str | None = None self.received_finish_reason: str | None = None self.intermittent_finish_reason: str | None = None # finish reasons that show up mid-stream + self._finished_choice_indices: frozenset[int] = frozenset() self.special_tokens = [ "<|assistant|>", "<|system|>", @@ -1091,7 +1102,7 @@ class CustomStreamWrapper: """ return delta is not None and stored_attribute(delta, attribute_name) is not None - def _copy_delta_attribute(self, source_delta, target_delta, attribute_name: str) -> None: + def _copy_delta_attribute(self, source_delta: object, target_delta: object, attribute_name: str) -> None: """ Copy a specific attribute from source delta to target delta. """ @@ -1575,6 +1586,9 @@ class CustomStreamWrapper: f"{self.custom_llm_provider} raised a streaming error - finish_reason: error, no content string given. Received Chunk={response_obj}" ) self.received_finish_reason = response_obj["finish_reason"] + self._finished_choice_indices = self._finished_choice_indices | { + _first_choice_index(chunk) if isinstance(chunk, (ChatCompletionChunk, ModelResponseStream)) else 0 + } if response_obj.get("original_chunk", None) is not None: if hasattr(response_obj["original_chunk"], "id"): model_response = self.set_model_id(response_obj["original_chunk"].id, model_response) @@ -2205,12 +2219,25 @@ class CustomStreamWrapper: return next(self.completion_stream) except Exception as e: if ( - self.received_finish_reason is None + not self._every_choice_finished() or chained_transport_error_kind(e) is not TransportErrorKind.DROPPED_CONNECTION ): raise raise StopIteration from e + def _every_choice_finished(self) -> bool: + if self.received_finish_reason is None: + return False + requested_choices: Final = self._requested_choice_count() + return requested_choices <= 1 or len(self._finished_choice_indices) >= requested_choices + + def _requested_choice_count(self) -> int: + match getattr(self.logging_obj, "optional_params", None): + case {"n": int() as requested}: + return requested + case _: + return 1 + def fetch_sync_stream(self): if self.completion_stream is None and self.make_call is not None: # Call make_call to get the completion stream @@ -2321,7 +2348,7 @@ class CustomStreamWrapper: return await self._finalize_completed_stream(cache_hit=cache_hit) except Exception as e: match chained_transport_error_kind(e): - case TransportErrorKind.DROPPED_CONNECTION if self.received_finish_reason is not None: + case TransportErrorKind.DROPPED_CONNECTION if self._every_choice_finished(): return await self._finalize_completed_stream(cache_hit=cache_hit) case TransportErrorKind.TIMEOUT: traceback_exception: Final = ( diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index c7127db88dc..b1f0065cec4 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -8,7 +8,7 @@ from contextlib import closing from functools import lru_cache from types import MappingProxyType from typing import Final, Literal, NamedTuple, cast -from urllib.parse import urlsplit, urlunsplit +from urllib.parse import SplitResult, parse_qsl, urlsplit, urlunsplit import httpx from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI @@ -334,23 +334,28 @@ _DEPLOYMENT_OPERATION_PATH: Final = re.compile( ) -def _azure_deployment_base_url(deployment_url: str) -> str: - parts: Final = urlsplit(deployment_url) - operation_path: Final = _DEPLOYMENT_OPERATION_PATH.fullmatch(parts.path) - deployment_path: Final = parts.path if operation_path is None else operation_path["deployment"] - return urlunsplit(parts._replace(path=deployment_path, query="", fragment="")) +def _azure_deployment_base_url(deployment_url: SplitResult) -> str: + operation_path: Final = _DEPLOYMENT_OPERATION_PATH.fullmatch(deployment_url.path) + deployment_path: Final = deployment_url.path if operation_path is None else operation_path["deployment"] + return urlunsplit(deployment_url._replace(path=deployment_path, query="", fragment="")) -def select_azure_base_url_or_endpoint(azure_client_params: dict): +def _azure_deployment_extra_query(deployment_url: SplitResult) -> Mapping[str, str]: + return {name: value for name, value in parse_qsl(deployment_url.query) if name != "api-version"} + + +def select_azure_base_url_or_endpoint(azure_client_params: Mapping[str, object]): azure_endpoint: Final = azure_client_params.get("azure_endpoint", None) - if isinstance(azure_endpoint, str): - # see : https://github.com/openai/openai-python/blob/3d61ed42aba652b547029095a7eb269ad4e1e957/src/openai/lib/azure.py#L192 - if "/openai/deployments" in azure_endpoint: - # this is base_url, not an azure_endpoint - azure_client_params["base_url"] = _azure_deployment_base_url(azure_endpoint) - azure_client_params.pop("azure_endpoint") - - return azure_client_params + # see : https://github.com/openai/openai-python/blob/3d61ed42aba652b547029095a7eb269ad4e1e957/src/openai/lib/azure.py#L192 + if not isinstance(azure_endpoint, str) or "/openai/deployments" not in azure_endpoint: + return dict(azure_client_params) + deployment_url: Final = urlsplit(azure_endpoint) + extra_query: Final = _azure_deployment_extra_query(deployment_url) + return { + **{name: value for name, value in azure_client_params.items() if name != "azure_endpoint"}, + "base_url": _azure_deployment_base_url(deployment_url), + **({"default_query": extra_query} if extra_query else {}), + } def get_azure_ad_token( diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index e2376f4ad71..723e0695af2 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -228,9 +228,9 @@ _ERROR_CODE_HTTP_STATUS: Final[Mapping[str, int]] = MappingProxyType( "insufficient_quota": 429, "vector_store_timeout": 504, "invalid_prompt": 400, - "data_residency_mismatch": 400, - "bio_policy": 400, - "misalignment_policy_violation": 403, + "data_residency_mismatch": 500, + "bio_policy": 500, + "misalignment_policy_violation": 500, "invalid_image": 400, "invalid_image_format": 400, "invalid_base64_image": 400, diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index 1c31f2c77f4..6915aaf4edc 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -4760,7 +4760,9 @@ class TransportErrorAfterChunksIterator: raise StopAsyncIteration -def _reset_test_chunk(content: Optional[str] = None, finish_reason: Optional[str] = None) -> ModelResponseStream: +def _reset_test_chunk( + content: Optional[str] = None, finish_reason: Optional[str] = None, index: int = 0 +) -> ModelResponseStream: return ModelResponseStream( id="chatcmpl-reset-test", created=1783458104, @@ -4768,7 +4770,7 @@ def _reset_test_chunk(content: Optional[str] = None, finish_reason: Optional[str object="chat.completion.chunk", choices=[ StreamingChoices( - index=0, + index=index, delta=Delta(content=content), finish_reason=finish_reason, ) @@ -4940,6 +4942,47 @@ async def test_sdk3_dropped_connection_after_finish_reason_ends_the_stream_clean assert [chunk.choices[0].finish_reason for chunk in chunks if chunk.choices[0].finish_reason] == ["stop"] +def _two_choice_stream( + finished_choices: Sequence[int], sync_mode: bool, logging_obj: Logging +) -> CustomStreamWrapper: + logging_obj.update_environment_variables(litellm_params={}, optional_params={"n": 2}) + return _openai_stream_over( + ( + _reset_test_chunk(content="Hi", index=0), + _reset_test_chunk(content="Hel", index=1), + *(_reset_test_chunk(finish_reason="stop", index=index) for index in finished_choices), + ), + _httpx2_read_error(), + sync_mode, + logging_obj, + ) + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +async def test_sdk3_dropped_connection_while_another_requested_choice_streams_raises( + logging_obj: Logging, sync_mode: bool +): + wrapper: Final = _two_choice_stream((0,), sync_mode, logging_obj) + + with pytest.raises(MidStreamFallbackError) as excinfo: + await _collect(wrapper, sync_mode) + + assert isinstance(excinfo.value.original_exception, litellm.APIConnectionError) + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +async def test_sdk3_dropped_connection_after_every_requested_choice_finished_ends_cleanly( + logging_obj: Logging, sync_mode: bool +): + wrapper: Final = _two_choice_stream((0, 1), sync_mode, logging_obj) + + chunks: Final = await _collect(wrapper, sync_mode) + + assert "".join(chunk.choices[0].delta.content or "" for chunk in chunks if chunk.choices) == "HiHel" + + @pytest.mark.asyncio @_SYNC_AND_ASYNC @_SDK3_DROPPED_CONNECTIONS diff --git a/tests/unit/llms/azure/test_azure_common_utils.py b/tests/unit/llms/azure/test_azure_common_utils.py index 1dc7ff74323..6063db4d299 100644 --- a/tests/unit/llms/azure/test_azure_common_utils.py +++ b/tests/unit/llms/azure/test_azure_common_utils.py @@ -2360,3 +2360,35 @@ def test_an_azure_client_litellm_built_its_own_http_client_for_is_still_closed(m closer.reap() assert wrapper.is_closed() is True + + +def test_full_deployment_url_keeps_its_extra_query_params_on_the_sdk_request(monkeypatch: pytest.MonkeyPatch) -> None: + def echo_url(request: httpx.Request) -> httpx.Response: + return httpx.Response( + 200, + json={ + "id": "chatcmpl-url", + "object": "chat.completion", + "created": 0, + "model": "gpt", + "choices": [ + {"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": str(request.url)}} + ], + }, + ) + + monkeypatch.setattr(litellm, "client_session", httpx.Client(transport=httpx.MockTransport(echo_url))) + client: Final = BaseAzureLLM().get_azure_openai_client( + api_key="azure-key", + api_base="https://gateway.example.com/openai/deployments/gpt/chat/completions" + "?api-version=2024-10-21&subscription-key=abc", + api_version="2025-02-01-preview", + litellm_params={}, + _is_async=False, + ) + + response: Final = client.chat.completions.create(model="gpt", messages=[{"role": "user", "content": "hi"}]) + + sent: Final = httpx.URL(response.choices[0].message.content or "") + assert sent.path == "/openai/deployments/gpt/chat/completions" + assert dict(sent.params) == {"api-version": "2025-02-01-preview", "subscription-key": "abc"} diff --git a/tests/unit/llms/openai/test_openai_file_content_streaming.py b/tests/unit/llms/openai/test_openai_file_content_streaming.py index 1455fd45e67..466918267f6 100644 --- a/tests/unit/llms/openai/test_openai_file_content_streaming.py +++ b/tests/unit/llms/openai/test_openai_file_content_streaming.py @@ -1,13 +1,6 @@ -import importlib -from types import ModuleType - import pytest -from typing import AsyncIterator, Final, Iterator, cast +from typing import AsyncIterator, Iterator, cast -from openai import AsyncOpenAI, OpenAI -from openai._types import Response as SDKResponse - -import litellm from litellm.files import main as files_main from litellm.files.streaming import FileContentStreamingResponse from litellm.files.types import FileContentStreamingResult @@ -330,80 +323,3 @@ def test_file_content_streaming_passes_exception_to_context_manager_exit(): assert response_cm.exc_info[0] is RuntimeError assert response_cm.exc_info[1] is exc_info.value assert response_cm.exc_info[2] is not None - - -@pytest.fixture(params=("httpx", "sdk")) -def http_backend(request: pytest.FixtureRequest) -> ModuleType: - return importlib.import_module("httpx" if request.param == "httpx" else SDKResponse.__module__) - - -_TRANSPORT_FAILURES: Final = pytest.mark.parametrize( - ("failure_name", "public_error"), - ( - ("ReadTimeout", litellm.Timeout), - ("ReadError", litellm.APIConnectionError), - ("RemoteProtocolError", litellm.APIConnectionError), - ), -) - - -def _body_then_failure_transport(http_backend: ModuleType, failure: Exception) -> object: - class BodyThenFailure(http_backend.SyncByteStream, http_backend.AsyncByteStream): - def __iter__(self) -> Iterator[bytes]: - yield b"hello" - raise failure - - async def __aiter__(self) -> AsyncIterator[bytes]: - yield b"hello" - raise failure - - return http_backend.MockTransport( - lambda _request: http_backend.Response( - 200, headers={"content-type": "application/octet-stream"}, stream=BodyThenFailure() - ) - ) - - -@_TRANSPORT_FAILURES -def test_file_content_stream_raises_litellm_errors_when_the_transport_fails_mid_body( - http_backend: ModuleType, failure_name: str, public_error: type[Exception] -): - failure: Final = getattr(http_backend, failure_name)("transport failed mid-body") - - with http_backend.Client(transport=_body_then_failure_transport(http_backend, failure)) as http_client: - result: Final = litellm.file_content( - file_id="file-abc123", - custom_llm_provider="openai", - client=OpenAI(api_key="transport-only", http_client=http_client), - stream=True, - chunk_size=len(b"hello"), - ) - stream: Final = cast(Iterator[bytes], result.stream_iterator) - assert next(stream) == b"hello" - with pytest.raises(public_error) as excinfo: - next(stream) - - assert excinfo.value.__cause__ is failure - - -@pytest.mark.asyncio -@_TRANSPORT_FAILURES -async def test_afile_content_stream_raises_litellm_errors_when_the_transport_fails_mid_body( - http_backend: ModuleType, failure_name: str, public_error: type[Exception] -): - failure: Final = getattr(http_backend, failure_name)("transport failed mid-body") - - async with http_backend.AsyncClient(transport=_body_then_failure_transport(http_backend, failure)) as http_client: - result: Final = await litellm.afile_content( - file_id="file-abc123", - custom_llm_provider="openai", - client=AsyncOpenAI(api_key="transport-only", http_client=http_client), - stream=True, - chunk_size=len(b"hello"), - ) - stream: Final = cast(AsyncIterator[bytes], result.stream_iterator) - assert await stream.__anext__() == b"hello" - with pytest.raises(public_error) as excinfo: - await stream.__anext__() - - assert excinfo.value.__cause__ is failure diff --git a/tests/unit/responses/test_streaming_iterator_error_events.py b/tests/unit/responses/test_streaming_iterator_error_events.py index 480946e7768..bf0eaaba98b 100644 --- a/tests/unit/responses/test_streaming_iterator_error_events.py +++ b/tests/unit/responses/test_streaming_iterator_error_events.py @@ -687,9 +687,9 @@ def test_every_openai_sdk_response_error_code_has_explicit_status_mapping(): ("empty_image_file", 400), ("failed_to_download_image", 400), ("image_file_not_found", 400), - ("data_residency_mismatch", 400), - ("bio_policy", 400), - ("misalignment_policy_violation", 403), + ("data_residency_mismatch", 500), + ("bio_policy", 500), + ("misalignment_policy_violation", 500), ("totally_unknown_future_code", 500), ("429", 429), ("503", 503), From 09b49357cbbe9fe6b3b9b3a63a8167e2313dd432 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:52:23 -0700 Subject: [PATCH 10/23] fix: end text completion n=2 streams cleanly, keep Azure image gateway query params, tighten SDK 3 test fixtures --- .circleci/scripts/run_integration.sh | 6 ++ .../litellm_core_utils/streaming_handler.py | 25 ++++-- litellm/llms/azure/azure.py | 30 ++++--- pyproject.toml | 2 +- .../_retained_openai_consumer.py | 56 ++++++++++++ .../compatibility/test_openai_consumer.py | 87 ++++++++++--------- tests/unit/conftest.py | 8 +- .../test_streaming_handler.py | 44 ++++++++++ tests/unit/llms/azure/test_azure.py | 21 +++++ 9 files changed, 217 insertions(+), 62 deletions(-) create mode 100644 tests/integration/compatibility/_retained_openai_consumer.py diff --git a/.circleci/scripts/run_integration.sh b/.circleci/scripts/run_integration.sh index 00b3c5f8fdd..2731dc9a8c4 100644 --- a/.circleci/scripts/run_integration.sh +++ b/.circleci/scripts/run_integration.sh @@ -70,6 +70,12 @@ trap 'exit 143' TERM export PATH="$PWD/.venv/bin:$PATH" export PYTHONPATH="$PWD:$PWD/tests:$PWD/tests/e2e" +if [ "$suite" = extensions ]; then + retained_openai="$(mktemp -d)/retained-openai" + uv venv --quiet --python 3.12 "$retained_openai" + uv pip install --quiet --python "$retained_openai/bin/python" --exclude-newer 2026-04-29T00:00:00Z openai==2.33.0 + export RETAINED_OPENAI_PYTHON="$retained_openai/bin/python" +fi export DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:5432/circle_test" export REDIS_HOST=127.0.0.1 REDIS_PORT=6379 export LITELLM_MASTER_KEY="sk-$(openssl rand -hex 16)" LITELLM_SALT_KEY=sk-integration-salt diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 214f4803f1a..7c826bea82a 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Any, Final, NoReturn, Protocol, TypeVar, cast import anyio import httpx +from openai.types import Completion from openai.types.chat import ChatCompletionChunk from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice from openai.types.chat.chat_completion_chunk import ChoiceDelta @@ -93,12 +94,17 @@ def _next_sync_or_exhausted(it: Iterator[object]) -> object: return _SYNC_ITER_EXHAUSTED -def _first_choice_index(chunk: ChatCompletionChunk | ModelResponseStream) -> int: +_CHOICE_CHUNK_TYPES: Final = (ChatCompletionChunk, ModelResponseStream, Completion) + + +def _first_choice_index(chunk: ChatCompletionChunk | ModelResponseStream | Completion | None) -> int: match chunk: case ChatCompletionChunk(choices=[first, *_]): return first.index case ModelResponseStream(choices=[first, *_]): return first.index + case Completion(choices=[first, *_]): + return first.index case _: return 0 @@ -343,7 +349,7 @@ class CustomStreamWrapper: self._provider_response_model: str | None = None self.received_finish_reason: str | None = None self.intermittent_finish_reason: str | None = None # finish reasons that show up mid-stream - self._finished_choice_indices: frozenset[int] = frozenset() + self._finished_choice_indices: frozenset[int] | None = None self.special_tokens = [ "<|assistant|>", "<|system|>", @@ -1503,6 +1509,7 @@ class CustomStreamWrapper: print_verbose(f"completion obj content: {completion_obj['content']}") if response_obj["is_finished"]: self.received_finish_reason = response_obj["finish_reason"] + self._record_finished_choice(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) if response_obj["usage"] is not None: _text_completion_usage: Final[Usage] = response_obj["usage"] setattr( @@ -1542,6 +1549,7 @@ class CustomStreamWrapper: print_verbose(f"completion obj content: {completion_obj['content']}") if response_obj["is_finished"]: self.received_finish_reason = response_obj["finish_reason"] + self._record_finished_choice(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) elif self.custom_llm_provider == "cached_response": cached_chunk: Final = cast(ModelResponseStream, chunk) cached_choice: Final = cached_chunk.choices[0] if cached_chunk.choices else None @@ -1586,9 +1594,7 @@ class CustomStreamWrapper: f"{self.custom_llm_provider} raised a streaming error - finish_reason: error, no content string given. Received Chunk={response_obj}" ) self.received_finish_reason = response_obj["finish_reason"] - self._finished_choice_indices = self._finished_choice_indices | { - _first_choice_index(chunk) if isinstance(chunk, (ChatCompletionChunk, ModelResponseStream)) else 0 - } + self._record_finished_choice(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) if response_obj.get("original_chunk", None) is not None: if hasattr(response_obj["original_chunk"], "id"): model_response = self.set_model_id(response_obj["original_chunk"].id, model_response) @@ -2225,11 +2231,18 @@ class CustomStreamWrapper: raise raise StopIteration from e + def _record_finished_choice(self, chunk: ChatCompletionChunk | ModelResponseStream | Completion | None) -> None: + self._finished_choice_indices = (self._finished_choice_indices or frozenset()) | {_first_choice_index(chunk)} + def _every_choice_finished(self) -> bool: if self.received_finish_reason is None: return False requested_choices: Final = self._requested_choice_count() - return requested_choices <= 1 or len(self._finished_choice_indices) >= requested_choices + match self._finished_choice_indices: + case None: + return True + case finished_choices: + return requested_choices <= 1 or len(finished_choices) >= requested_choices def _requested_choice_count(self) -> int: match getattr(self.logging_obj, "optional_params", None): diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index 02f1ba3472a..f107ca9edae 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -1,7 +1,7 @@ import asyncio import json import time -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Mapping from typing import Final import httpx @@ -13,7 +13,7 @@ from openai import ( BadRequestError, OpenAI, ) -from pydantic import BaseModel, TypeAdapter +from pydantic import BaseModel, ConfigDict, TypeAdapter import litellm from litellm.constants import AZURE_OPERATION_POLLING_TIMEOUT, DEFAULT_MAX_RETRIES @@ -140,6 +140,16 @@ def check_dynamic_azure_params( _check_dynamic_azure_params = check_dynamic_azure_params +class _AzureDefaultQuery(BaseModel): + model_config = ConfigDict(frozen=True) + + default_query: Mapping[str, str] | None = None + + +def _with_default_query(url: str, default_query: Mapping[str, str] | None) -> str: + return str(httpx.URL(url).copy_merge_params(default_query)) if default_query else url + + class AzureChatCompletion(BaseAzureLLM, BaseLLM): def __init__(self) -> None: super().__init__() @@ -1110,6 +1120,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): "/" ) api_version: Final[str] = azure_client_params.get("api_version", "") + default_query: Final = _AzureDefaultQuery.model_validate(azure_client_params).default_query if model is None: model = "" @@ -1134,17 +1145,12 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): route="/openai/images/generations", ) if v1_url is not None: - return v1_url + return _with_default_query(v1_url, default_query) - if "/openai/deployments/" in api_base: - base_url_with_deployment = api_base - else: - base_url_with_deployment = api_base + "/openai/deployments/" + model - - base_url_with_deployment += "/images/generations" - base_url_with_deployment += "?api-version=" + api_version - - return base_url_with_deployment + deployment_url: Final = ( + api_base if "/openai/deployments/" in api_base else f"{api_base}/openai/deployments/{model}" + ) + return _with_default_query(f"{deployment_url}/images/generations?api-version={api_version}", default_query) async def aimage_generation( self, diff --git a/pyproject.toml b/pyproject.toml index 90f9958b9b2..4fd3208f5f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -369,7 +369,7 @@ constraint-dependencies = [ override-dependencies = [ # a2a-sdk 1.x requires packaging>=24.0; lunary 1.4.x still caps at <24.0. "packaging>=24.0", - # semantic-router (through 1.1.0) caps openai at <3.0.0 but only imports it for encoders LiteLLM never builds. + # semantic-router (through 1.1.0) caps openai at <3.0.0, yet its OpenAI encoder for auto router files runs on SDK 3. "openai>=2.20.0,<4.0.0", "cryptography>=50.0.0,<51.0", "mcp>=2.3.0,<3", diff --git a/tests/integration/compatibility/_retained_openai_consumer.py b/tests/integration/compatibility/_retained_openai_consumer.py new file mode 100644 index 00000000000..4dec85c0bb8 --- /dev/null +++ b/tests/integration/compatibility/_retained_openai_consumer.py @@ -0,0 +1,56 @@ +import asyncio +import json +import sys +from importlib.metadata import version +from typing import Final + +import httpx +from openai import AsyncOpenAI, OpenAI +from openai.types.chat import ChatCompletion + + +def _summary(response: ChatCompletion) -> dict[str, object]: + choice: Final = response.choices[0] + calls: Final = choice.message.tool_calls or () + return { + "id": response.id, + "object": response.object, + "usage": [response.usage.prompt_tokens, response.usage.completion_tokens, response.usage.total_tokens] + if response.usage + else None, + "finish_reason": choice.finish_reason, + "content": choice.message.content, + "tool_calls": [[call.id, call.function.name, json.loads(call.function.arguments)] for call in calls], + } + + +async def _asynchronous(base_url: str, key: str, parameters: dict[str, object]) -> tuple[ChatCompletion, ...]: + plain: Final = {name: value for name, value in parameters.items() if name != "tools"} + async with AsyncOpenAI( + api_key=key, base_url=base_url, max_retries=0, http_client=httpx.AsyncClient(timeout=10, trust_env=False) + ) as client: + return (await client.chat.completions.create(**parameters), await client.chat.completions.create(**plain)) + + +def main(base_url: str, key: str, parameters_json: str) -> None: + parameters: Final = json.loads(parameters_json) + plain: Final = {name: value for name, value in parameters.items() if name != "tools"} + with OpenAI( + api_key=key, base_url=base_url, max_retries=0, http_client=httpx.Client(timeout=10, trust_env=False) + ) as client: + sync_tools: Final[ChatCompletion] = client.chat.completions.create(**parameters) + sync_text: Final[ChatCompletion] = client.chat.completions.create(**plain) + async_tools, async_text = asyncio.run(_asynchronous(base_url, key, parameters)) + print( + json.dumps( + { + "openai": version("openai"), + "tools": [_summary(sync_tools), _summary(async_tools)], + "text": [_summary(sync_text), _summary(async_text)], + } + ) + ) + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/tests/integration/compatibility/test_openai_consumer.py b/tests/integration/compatibility/test_openai_consumer.py index 6150995723b..e00617be855 100644 --- a/tests/integration/compatibility/test_openai_consumer.py +++ b/tests/integration/compatibility/test_openai_consumer.py @@ -1,7 +1,9 @@ import json +import os +import subprocess import uuid from collections.abc import Callable -from importlib.metadata import version +from pathlib import Path from typing import Final import httpx @@ -11,12 +13,22 @@ from integration._support.wire import Reply, Request, wire_server from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI -@pytest.mark.covers("other.compatibility.openai.retained_client_parses_tools_and_usage") -async def test_retained_openai_clients_parse_real_proxy_tool_and_usage_responses(gateway: Gateway) -> None: - assert version("openai") == "2.33.0", ( - "Retain this consumer version independently before upgrading the candidate lock" - ) +_RETAINED_OPENAI_VERSION: Final = "2.33.0" +_RETAINED_CONSUMER: Final = Path(__file__).with_name("_retained_openai_consumer.py") + +def _retained_openai_python() -> str: + python: Final = os.environ.get("RETAINED_OPENAI_PYTHON") + if not python: + pytest.fail( + f"Set RETAINED_OPENAI_PYTHON to a Python with openai=={_RETAINED_OPENAI_VERSION} installed, " + "as .circleci/scripts/run_integration.sh does for the extensions suite" + ) + return python + + +@pytest.mark.covers("other.compatibility.openai.retained_client_parses_tools_and_usage") +def test_retained_openai_clients_parse_real_proxy_tool_and_usage_responses(gateway: Gateway) -> None: def provider(request: Request) -> Reply: assert request.method == "POST" and request.target == "/v1/chat/completions" body: Final = json.loads(request.body) @@ -72,40 +84,35 @@ async def test_retained_openai_clients_parse_real_proxy_tool_and_usage_responses ], "extra_body": {"cache": {"no-cache": True}}, } - plain: Final = {name: value for name, value in parameters.items() if name != "tools"} - with OpenAI( - api_key=key, - base_url=str(gateway.client.base_url).rstrip("/") + "/v1", - max_retries=0, - http_client=httpx.Client(timeout=10, trust_env=False), - ) as sync: - first: Final = sync.chat.completions.create(**parameters) - first_text: Final = sync.chat.completions.create(**plain) - async with AsyncOpenAI( - api_key=key, - base_url=str(gateway.client.base_url).rstrip("/") + "/v1", - max_retries=0, - http_client=httpx.AsyncClient(timeout=10, trust_env=False), - ) as asynchronous: - second: Final = await asynchronous.chat.completions.create(**parameters) - second_text: Final = await asynchronous.chat.completions.create(**plain) - assert len({response.id for response in (first, second, first_text, second_text)}) == 4 - for response in (first, second, first_text, second_text): - assert response.object == "chat.completion" - assert ( - response.usage.prompt_tokens == 11 - and response.usage.completion_tokens == 4 - and response.usage.total_tokens == 15 - ) - for response in (first, second): - assert response.choices[0].finish_reason == "tool_calls" - call: Final = response.choices[0].message.tool_calls[0] - assert call.id == "synthetic-call" and call.function.name == "add" - assert json.loads(call.function.arguments) == {"a": 3, "b": 5} - for response in (first_text, second_text): - assert response.choices[0].finish_reason == "stop" - assert response.choices[0].message.content == "Synthetic answer: 8" - assert not response.choices[0].message.tool_calls + consumer: Final = subprocess.run( + ( + _retained_openai_python(), + "-I", + str(_RETAINED_CONSUMER), + str(gateway.client.base_url).rstrip("/") + "/v1", + key, + json.dumps(parameters), + ), + capture_output=True, + text=True, + timeout=60, + check=False, + ) + assert consumer.returncode == 0, consumer.stderr + observed: Final = json.loads(consumer.stdout) + assert observed["openai"] == _RETAINED_OPENAI_VERSION + responses: Final = (*observed["tools"], *observed["text"]) + assert len({response["id"] for response in responses}) == 4 + for response in responses: + assert response["object"] == "chat.completion" + assert response["usage"] == [11, 4, 15] + for response in observed["tools"]: + assert response["finish_reason"] == "tool_calls" + assert response["tool_calls"] == [["synthetic-call", "add", {"a": 3, "b": 5}]] + for response in observed["text"]: + assert response["finish_reason"] == "stop" + assert response["content"] == "Synthetic answer: 8" + assert response["tool_calls"] == [] assert len(wire.drain()) == 4 diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 6c740dc45f5..3d70dd656c4 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -6,6 +6,7 @@ import sys from collections.abc import Callable, Coroutine, Iterator from dataclasses import dataclass, field from functools import partial +from importlib.metadata import version from pathlib import Path from types import MappingProxyType from typing import Final, Literal, TypeAlias @@ -435,11 +436,12 @@ def fake_provider_credentials(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.fixture def openai_token_exchange_via_httpx(monkeypatch: pytest.MonkeyPatch) -> None: - workload_module: Final = sys.modules.get("openai.auth._workload") - native_client_factory: Final = getattr(workload_module, "DefaultHttpx2Client", None) - if native_client_factory is None: + if int(version("openai").split(".")[0]) < 3: return import httpx2 + import openai.auth._workload as workload_module + + native_client_factory: Final = workload_module.DefaultHttpx2Client def send_through_httpx(request: httpx2.Request) -> httpx2.Response: with httpx.Client(trust_env=False) as client: diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index 6915aaf4edc..157c75f700f 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -14,6 +14,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import httpx import openai import pytest +from openai.types import Completion, CompletionChoice from openai.types.chat import ChatCompletionChunk from pydantic import BaseModel, TypeAdapter import litellm @@ -4983,6 +4984,49 @@ async def test_sdk3_dropped_connection_after_every_requested_choice_finished_end assert "".join(chunk.choices[0].delta.content or "" for chunk in chunks if chunk.choices) == "HiHel" +def _text_completion_chunk(index: int, text: str = "", finish_reason: Optional[str] = None) -> Completion: + return Completion.model_construct( + id="cmpl-two-choices", + object="text_completion", + created=1, + model="gpt-3.5-turbo-instruct", + choices=[CompletionChoice.model_construct(index=index, text=text, finish_reason=finish_reason, logprobs=None)], + ) + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +@pytest.mark.parametrize("provider", ("text-completion-openai", "azure_text")) +@pytest.mark.parametrize( + ("finished_choices", "ends_cleanly"), (((0, 1), True), ((1,), False)), ids=("both_finished", "choice_0_open") +) +async def test_two_choice_text_completion_stream_dropped_before_done_ends_cleanly_only_once_both_finish( + logging_obj: Logging, sync_mode: bool, provider: str, finished_choices: Sequence[int], ends_cleanly: bool +): + logging_obj.update_environment_variables(litellm_params={}, optional_params={"n": 2}) + stream: Final = _chunks_then_failure( + ( + _text_completion_chunk(0, "Hi"), + _text_completion_chunk(1, "Hel"), + *(_text_completion_chunk(index, finish_reason="stop") for index in finished_choices), + ), + _httpx2_read_error(), + ) + wrapper: Final = CustomStreamWrapper( + completion_stream=stream if sync_mode else _as_async(stream), + model="gpt-3.5-turbo-instruct", + custom_llm_provider=provider, + logging_obj=logging_obj, + ) + + if not ends_cleanly: + with pytest.raises(MidStreamFallbackError): + await _collect(wrapper, sync_mode) + return + chunks: Final = await _collect(wrapper, sync_mode) + assert "".join(chunk.choices[0].delta.content or "" for chunk in chunks if chunk.choices) == "HiHel" + + @pytest.mark.asyncio @_SYNC_AND_ASYNC @_SDK3_DROPPED_CONNECTIONS diff --git a/tests/unit/llms/azure/test_azure.py b/tests/unit/llms/azure/test_azure.py index 7d5afde68ef..6858e90ac76 100644 --- a/tests/unit/llms/azure/test_azure.py +++ b/tests/unit/llms/azure/test_azure.py @@ -271,6 +271,27 @@ def test_process_azure_endpoint_url(api_base, model, expected_endpoint): assert result == expected_endpoint, "Unexpected endpoint" +@respx.mock +@pytest.mark.parametrize("api_version", ("2025-04-01-preview", "v1")) +def test_azure_image_generation_keeps_deployment_url_query_params(api_version: str): + route: Final = respx.post(url__regex=r"https://azure-gateway\.example\.com/.*").mock( + return_value=httpx.Response(200, json={"created": 1, "data": [{"b64_json": "aGk="}]}) + ) + + litellm.image_generation( + model="azure/gpt-image-1", + prompt="a red circle", + api_base="https://azure-gateway.example.com/openai/deployments/gpt-image-1/images/generations?subscription-key=gateway-key", + api_version=api_version, + api_key="test-azure-key", + ) + + request_url: Final = route.calls.last.request.url + assert request_url.path.endswith("/images/generations") + assert request_url.params["subscription-key"] == "gateway-key" + assert request_url.params["api-version"] == api_version + + @patch("azure.identity.UsernamePasswordCredential") @patch("azure.identity.get_bearer_token_provider") def test_get_azure_ad_token_from_username_password(mock_get_bearer_token_provider, mock_credential): From 6b7ccf247ad6965442736e7b8ca116e931da342d Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:00:39 -0700 Subject: [PATCH 11/23] fix(responses): report unreported cache writes as 0 so SDK 3 can parse usage SDK 3's Responses usage schema requires input_tokens_details.cache_write_tokens, so a strict parse of a provider answer that reported no cache writes failed. The 0 is added only when the usage is serialized, so the cost path still treats an unreported value as unknown and a provider's cache_creation_tokens keeps pricing --- litellm/responses/utils.py | 5 ++++- litellm/types/llms/openai.py | 9 +++++++- .../test_litellm_completion_responses.py | 6 +++--- tests/unit/responses/test_responses_utils.py | 14 +++++++++++++ .../unit/types/llms/test_types_llms_openai.py | 21 +++++++++++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 05d6a36c1f6..ec3df4e1812 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -84,7 +84,10 @@ def _prompt_tokens_details_wrapper(details: object) -> PromptTokensDetailsWrappe if _is_object_dict(details): input_details: Final = InputTokensDetails.model_validate(details) return PromptTokensDetailsWrapper( - **input_details.model_dump(exclude={"cache_write_tokens_details"}), + **{ + **input_details.model_dump(exclude={"cache_write_tokens_details"}), + "cache_write_tokens": input_details.cache_write_tokens, + }, cache_creation_token_details=_cache_write_lifetimes(input_details.cache_write_tokens_details), ) return PromptTokensDetailsWrapper( diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 6ce51abcd8a..c32fc5f5c90 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1414,7 +1414,7 @@ class InputTokensDetails(BaseLiteLLMOpenAIResponseObject): audio_tokens: int | None = None cached_tokens: int = 0 cached_tokens_details: CachedTokensDetails | None = None - cache_write_tokens: int | None = Field(default=None, exclude_if=_is_none) + cache_write_tokens: int | None = None cache_write_tokens_details: CacheWriteTokensDetails | None = Field(default=None, exclude_if=_is_none) image_tokens: int | None = None text_tokens: int | None = None @@ -1422,6 +1422,13 @@ class InputTokensDetails(BaseLiteLLMOpenAIResponseObject): model_config = {"extra": "allow"} + @model_serializer(mode="wrap") + def _report_unreported_cache_writes_as_zero( # noqa: ANN202 # annotating it replaces the model's serialization schema + self, handler: SerializerFunctionWrapHandler + ): + serialized: Final[Mapping[str, object]] = handler(self) + return {**serialized, "cache_write_tokens": self.cache_write_tokens or 0} + class ResponseAPIUsage(BaseLiteLLMOpenAIResponseObject): input_tokens: int diff --git a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 4250c78c669..7b433cee2ea 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -3439,9 +3439,9 @@ class TestUsageTransformation: assert response_usage.input_tokens_details is not None assert response_usage.input_tokens_details.cached_tokens == 5 assert response_usage.input_tokens_details.text_tokens == 8 - assert {"cache_write_tokens", "cache_write_tokens_details"}.isdisjoint( - response_usage.input_tokens_details.model_dump() - ) + dumped_details: Final = response_usage.input_tokens_details.model_dump() + assert dumped_details["cache_write_tokens"] == 0 + assert "cache_write_tokens_details" not in dumped_details def test_transform_usage_with_cached_tokens_gemini(self): """Test that cached_tokens from Gemini are properly transformed to input_tokens_details""" diff --git a/tests/unit/responses/test_responses_utils.py b/tests/unit/responses/test_responses_utils.py index 0863abd1f1e..037a6431fef 100644 --- a/tests/unit/responses/test_responses_utils.py +++ b/tests/unit/responses/test_responses_utils.py @@ -415,6 +415,20 @@ class TestResponseAPILoggingUtils: assert result.prompt_tokens_details.cache_creation_tokens == 10059 assert result.prompt_tokens_details.cached_tokens == 0 + def test_transform_response_api_usage_keeps_cache_creation_tokens_without_cache_writes(self) -> None: + unvalidated_usage: Final = ResponseAPIUsage.model_construct( + input_tokens=100, + output_tokens=1, + total_tokens=101, + input_tokens_details={"cached_tokens": 0, "cache_creation_tokens": 50}, + ) + + usage: Final = ResponseAPILoggingUtils.transform_response_api_usage_to_chat_usage(unvalidated_usage) + + assert usage.prompt_tokens_details is not None + assert usage.prompt_tokens_details.cache_creation_tokens == 50 + assert usage.prompt_tokens_details.cache_write_tokens == 50 + @pytest.mark.parametrize("as_model", [False, True]) def test_transform_response_api_usage_prices_12h_cache_writes(self, as_model: bool) -> None: usage_data: Final = { diff --git a/tests/unit/types/llms/test_types_llms_openai.py b/tests/unit/types/llms/test_types_llms_openai.py index 1b7a9693f7b..57fa7007537 100644 --- a/tests/unit/types/llms/test_types_llms_openai.py +++ b/tests/unit/types/llms/test_types_llms_openai.py @@ -8,14 +8,19 @@ import pytest import json +from openai.types.responses.response_usage import ResponseUsage as OpenAIResponseUsage + import litellm from litellm.types.llms.openai import ( ContentPartDoneEvent, ContentPartDonePartOutputText, HttpxBinaryResponseContent, + InputTokensDetails, OpenAIChatCompletionLogprobsContent, OpenAIModerationResponse, OpenAIVideoObject, + OutputTokensDetails, + ResponseAPIUsage, ResponseCompletedEvent, ResponsesAPIResponse, ) @@ -799,3 +804,19 @@ def test_rebuilding_a_dumped_responses_answer_keeps_the_tool_search_call_keys(ex rebuilt: Final = ResponsesAPIResponse(**answer.model_dump()) assert rebuilt.model_dump()["output"] == [search_call] + + +@pytest.mark.parametrize(("cache_write_tokens", "reported"), ((None, 0), (7, 7))) +def test_responses_usage_dump_matches_the_openai_sdk_usage_schema(cache_write_tokens: int | None, reported: int) -> None: + usage: Final = ResponseAPIUsage( + input_tokens=10, + output_tokens=2, + total_tokens=12, + input_tokens_details=InputTokensDetails(cached_tokens=5, cache_write_tokens=cache_write_tokens), + output_tokens_details=OutputTokensDetails(reasoning_tokens=0), + ) + + sdk_usage: Final = OpenAIResponseUsage.model_validate(usage.model_dump(exclude_none=True)) + + assert sdk_usage.input_tokens_details.cache_write_tokens == reported + assert sdk_usage.input_tokens_details.cached_tokens == 5 From 3a3987bfd786c3cc525414b422da404aa6fa9fc2 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:49:10 -0700 Subject: [PATCH 12/23] fix(openai): keep SDK 3 from sending OPENAI_ADMIN_KEY on model calls and send Azure Bearer on region and env-key calls --- litellm/llms/azure/azure.py | 29 +++++++++------ litellm/llms/azure/common_utils.py | 19 ++++++---- litellm/llms/openai/common_utils.py | 20 +++++++++++ litellm/llms/openai/openai.py | 5 +-- litellm/llms/openai/workload_identity.py | 5 +-- litellm/main.py | 16 +++++++-- .../llms/azure/test_azure_common_utils.py | 25 ++++++++++++- .../llms/openai/test_openai_common_utils.py | 33 ++++++++++++++++- tests/unit/test_main.py | 36 +++++++++++++++++++ 9 files changed, 164 insertions(+), 24 deletions(-) diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index 8bf44159a47..680a84d6c3d 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -51,6 +51,7 @@ from .common_utils import ( process_azure_headers, redact_azure_auth_headers, select_azure_base_url_or_endpoint, + with_one_azure_sdk_credential, ) from .image_generation import ( AzureFoundryMAIImageGenerationConfig, @@ -1330,21 +1331,29 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): api_base += "/" api_base += f"{model}" client = AzureOpenAI( - base_url=api_base, - api_version=api_version, - api_key=api_key, - timeout=timeout, - http_client=client_session, + **with_one_azure_sdk_credential( + { + "base_url": api_base, + "api_version": api_version, + "api_key": api_key, + "timeout": timeout, + "http_client": client_session, + } + ) ) model = None # cloudflare ai gateway, needs model=None else: client = AzureOpenAI( - api_version=api_version, - azure_endpoint=api_base, - api_key=api_key, - timeout=timeout, - http_client=client_session, + **with_one_azure_sdk_credential( + { + "api_version": api_version, + "azure_endpoint": api_base, + "api_key": api_key, + "timeout": timeout, + "http_client": client_session, + } + ) ) # only run this check if it's not cloudflare ai gateway diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index e746a8e0d0c..df088d03ae2 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -22,7 +22,12 @@ from litellm.litellm_core_utils.single_flight import SingleFlight, single_flight from litellm.litellm_core_utils.url_utils import ensure_api_route from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.llms.custom_httpx.http_handler import HTTPHandler -from litellm.llms.openai.common_utils import BaseOpenAILLM, OpenAIAsyncHTTPClient, OpenAIHTTPClient +from litellm.llms.openai.common_utils import ( + BaseOpenAILLM, + OpenAIAsyncHTTPClient, + OpenAIHTTPClient, + openai_sdk_key_params, +) from litellm.secret_managers.get_azure_ad_token_provider import ( get_azure_ad_token_provider, single_flight_bearer_token_provider, @@ -693,10 +698,12 @@ class BaseAzureLLM(BaseOpenAILLM): else v1_token_provider ) - v1_params: Final[_AzureV1ClientParams] = { - "api_key": v1_api_key, - "base_url": f"{ensure_api_route(api_base or '', 'openai/v1')}/", - } + v1_base_url: Final = f"{ensure_api_route(api_base or '', 'openai/v1')}/" + v1_params: Final[_AzureV1ClientParams] = ( + {"api_key": v1_api_key, "base_url": v1_base_url} + if callable(v1_api_key) + else {**openai_sdk_key_params(v1_api_key), "base_url": v1_base_url} + ) if "timeout" in azure_client_params: v1_params["timeout"] = azure_client_params["timeout"] if "max_retries" in azure_client_params: @@ -704,7 +711,7 @@ class BaseAzureLLM(BaseOpenAILLM): if "http_client" in azure_client_params: v1_params["http_client"] = azure_client_params["http_client"] - verbose_logger.debug("Using Azure v1 API with base_url: %s", v1_params["base_url"]) + verbose_logger.debug("Using Azure v1 API with base_url: %s", v1_base_url) if _is_async is True: openai_client = AsyncOpenAI(**v1_params) diff --git a/litellm/llms/openai/common_utils.py b/litellm/llms/openai/common_utils.py index a9d2dcdffb8..507368bba9e 100644 --- a/litellm/llms/openai/common_utils.py +++ b/litellm/llms/openai/common_utils.py @@ -24,6 +24,7 @@ from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice from openai.types.chat.chat_completion_chunk import ChoiceDelta from openai.types.completion_usage import CompletionUsage from pydantic import BaseModel, TypeAdapter +from typing_extensions import ReadOnly, TypedDict if TYPE_CHECKING: from aiohttp import ClientSession @@ -148,6 +149,25 @@ class OpenAIError(BaseLLMException): ) +_SDK_READS_ADMIN_KEY_ENV: Final = "admin_api_key" in _OPENAI_INIT_PARAMS +_MISSING_OPENAI_API_KEY_MESSAGE: Final = ( + "Missing credentials. Please pass an `api_key` or set the `OPENAI_API_KEY` environment variable." +) + + +class OpenAISdkKeyParams(TypedDict, total=False): + api_key: ReadOnly[str | None] + admin_api_key: ReadOnly[str] + + +def openai_sdk_key_params(api_key: str | None) -> OpenAISdkKeyParams: + if not _SDK_READS_ADMIN_KEY_ENV: + return {"api_key": api_key} + if not api_key: + raise OpenAIError(status_code=401, message=_MISSING_OPENAI_API_KEY_MESSAGE) + return {"api_key": api_key, "admin_api_key": api_key} + + _DECODED_JSON: Final = TypeAdapter(object) diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index af2e85c5f20..5e9d4f2af7b 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -64,6 +64,7 @@ from .common_utils import ( is_openai_backed_api_base, is_output_token_limit_error, model_dump_provider_response, + openai_sdk_key_params, ) from .workload_identity import ( build_async_openai_client, @@ -454,7 +455,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): ) if workload_identity_config is not None else AsyncOpenAI( - api_key=openai_static_api_key(api_key), + **openai_sdk_key_params(openai_static_api_key(api_key)), base_url=api_base, http_client=async_http_client, timeout=timeout, @@ -476,7 +477,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): ) if workload_identity_config is not None else OpenAI( - api_key=openai_static_api_key(api_key), + **openai_sdk_key_params(openai_static_api_key(api_key)), base_url=api_base, http_client=sync_http_client, timeout=timeout, diff --git a/litellm/llms/openai/workload_identity.py b/litellm/llms/openai/workload_identity.py index 5952ae3b3ef..30c3c83f12d 100644 --- a/litellm/llms/openai/workload_identity.py +++ b/litellm/llms/openai/workload_identity.py @@ -21,6 +21,7 @@ from .common_utils import ( OpenAIError, OpenAIHTTPClient, is_openai_backed_api_base, + openai_sdk_key_params, ) if TYPE_CHECKING: @@ -185,7 +186,7 @@ def build_openai_client( None if static_key_http_client_factory is None else static_key_http_client_factory() ) return OpenAI( - api_key=openai_static_api_key(api_key), + **openai_sdk_key_params(openai_static_api_key(api_key)), base_url=api_base, timeout=timeout, max_retries=retries, @@ -241,7 +242,7 @@ def build_async_openai_client( None if static_key_http_client_factory is None else static_key_http_client_factory() ) return AsyncOpenAI( - api_key=openai_static_api_key(api_key), + **openai_sdk_key_params(openai_static_api_key(api_key)), base_url=api_base, timeout=timeout, max_retries=retries, diff --git a/litellm/main.py b/litellm/main.py index d1af2de3a13..d45d088c132 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -6851,7 +6851,13 @@ def embedding( azure_ad_token: Final = optional_params.pop("azure_ad_token", None) or get_secret_str("AZURE_AD_TOKEN") - api_key = api_key or litellm.api_key or litellm.azure_key or get_secret_str("AZURE_API_KEY") + api_key = ( + api_key + or litellm.api_key + or litellm.azure_key + or get_secret_str("AZURE_API_KEY") + or get_secret_str("AZURE_OPENAI_API_KEY") + ) if api_base is None: raise ValueError("No API Base provided for Azure OpenAI LLM provider. Set 'AZURE_API_BASE' in .env") @@ -8441,7 +8447,13 @@ def transcription( azure_ad_token: Final = kwargs.pop("azure_ad_token", None) or get_secret_str("AZURE_AD_TOKEN") - api_key = api_key or litellm.api_key or litellm.azure_key or get_secret_str("AZURE_API_KEY") + api_key = ( + api_key + or litellm.api_key + or litellm.azure_key + or get_secret_str("AZURE_API_KEY") + or get_secret_str("AZURE_OPENAI_API_KEY") + ) optional_params["extra_headers"] = extra_headers diff --git a/tests/unit/llms/azure/test_azure_common_utils.py b/tests/unit/llms/azure/test_azure_common_utils.py index 7953f2b27e8..ecd2823b503 100644 --- a/tests/unit/llms/azure/test_azure_common_utils.py +++ b/tests/unit/llms/azure/test_azure_common_utils.py @@ -27,11 +27,12 @@ from litellm.llms.azure.common_utils import ( from litellm.secret_managers.get_azure_ad_token_provider import ( get_azure_ad_token_provider, ) -from litellm.types.router import GenericLiteLLMParams +from litellm.types.router import GenericLiteLLMParams, LiteLLM_Params from litellm.types.secret_managers.get_azure_ad_token_provider import ( AzureCredentialType, ) from litellm.types.utils import CallTypes +from litellm.utils import get_model_region # Mock the necessary dependencies @@ -2629,3 +2630,25 @@ async def test_azure_sdk_client_sends_the_auth_headers_sdk_2_sent( ) assert json.loads(response.choices[0].message.content or "{}") == expected_headers + + +def _region_only_for_key_and_bearer(request: httpx.Request) -> httpx.Response: + if (request.headers.get("api-key"), request.headers.get("authorization")) != ("azure-key", "Bearer azure-key"): + return httpx.Response(401, json={"error": {"message": "denied"}}) + return httpx.Response( + 200, + headers={"x-ms-region": "eastus"}, + json={"id": "chatcmpl-region", "object": "chat.completion", "created": 0, "model": "gpt", "choices": []}, + ) + + +@pytest.mark.parametrize( + "api_base", + ["https://region-lookup.openai.azure.com", "https://gateway.ai.cloudflare.com/v1/account/gateway/azure-openai/res"], +) +def test_azure_region_lookup_sends_the_key_and_bearer_headers(monkeypatch: pytest.MonkeyPatch, api_base: str) -> None: + session: Final = httpx.Client(transport=httpx.MockTransport(_region_only_for_key_and_bearer)) + monkeypatch.setattr(litellm, "client_session", session) + params: Final = LiteLLM_Params(model="azure/gpt-region", api_key="azure-key", api_base=api_base) + + assert get_model_region(litellm_params=params, mode="chat") == "eastus" diff --git a/tests/unit/llms/openai/test_openai_common_utils.py b/tests/unit/llms/openai/test_openai_common_utils.py index 206fd13260c..820c00c015d 100644 --- a/tests/unit/llms/openai/test_openai_common_utils.py +++ b/tests/unit/llms/openai/test_openai_common_utils.py @@ -78,10 +78,11 @@ API_FUNCTION_PARAMS = [ @pytest.mark.parametrize("function_name,is_async,args", API_FUNCTION_PARAMS) @pytest.mark.asyncio -async def test_openai_client_reuse(function_name, is_async, args): +async def test_openai_client_reuse(function_name, is_async, args, monkeypatch: pytest.MonkeyPatch): """ Test that multiple API calls reuse the same OpenAI client """ + monkeypatch.setenv("OPENAI_API_KEY", "sk-client-reuse") # Determine which client class to mock based on whether the test is async client_path = "litellm.llms.openai.openai.AsyncOpenAI" if is_async else "litellm.llms.openai.openai.OpenAI" @@ -565,3 +566,33 @@ async def test_azure_gateway_and_image_variations_use_the_callers_async_session( ) assert response.data[0].url == "https://example.com/image.png" assert not session.is_closed + + +@pytest.mark.parametrize("is_async", [False, True]) +@pytest.mark.asyncio +async def test_unkeyed_openai_deployment_never_sends_the_openai_admin_key( + monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter, is_async: bool +) -> None: + monkeypatch.setenv("OPENAI_ADMIN_KEY", "sk-admin-key-for-org-apis") + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + api_base: Final = f"https://unkeyed-{is_async}.example.com/v1" + route: Final = respx_mock.post(f"{api_base}/chat/completions").respond( + 200, + json={ + "id": "chatcmpl-unkeyed", + "object": "chat.completion", + "created": 0, + "model": "gpt-4o-mini", + "choices": [{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": "hi"}}], + }, + ) + request: Final = { + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "hi"}], + "api_base": api_base, + } + + with pytest.raises(litellm.AuthenticationError, match="Missing credentials"): + await litellm.acompletion(**request) if is_async else litellm.completion(**request) + + assert not route.called diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index e119fe133d5..a7a4cf09793 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -10410,3 +10410,39 @@ async def test_bedrock_strict_tools_limit_rejection_is_not_sent_again_when_no_to assert error is not None and "Too many strict tools" in str(error) assert len(sent) == 1 + + +_AZURE_ENV_KEY_RESPONSES: Final = { + "embedding": { + "object": "list", + "data": [{"object": "embedding", "index": 0, "embedding": [0.1]}], + "model": "text-embedding-3-small", + "usage": {"prompt_tokens": 1, "total_tokens": 1}, + }, + "transcription": {"text": "Four score and seven years ago"}, +} + + +@pytest.mark.parametrize("call", ["embedding", "transcription"]) +def test_azure_key_from_azure_openai_api_key_env_sends_the_bearer_header( + monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter, call: str +) -> None: + monkeypatch.delenv("AZURE_API_KEY", raising=False) + monkeypatch.setenv("AZURE_OPENAI_API_KEY", "azure-env-key") + api_base: Final = f"https://env-key-{call}.openai.azure.com" + route: Final = respx_mock.post(url__regex=rf"{api_base}/openai/deployments/.*").respond( + 200, json=_AZURE_ENV_KEY_RESPONSES[call] + ) + target: Final = {"api_base": api_base, "api_version": "2024-06-01", "max_retries": 0} + + if call == "embedding": + litellm.embedding(model="azure/text-embedding-3-small", input=["hello"], **target) + else: + litellm.transcription( + model="azure/whisper-1", + file=("gettysburg.wav", (Path(__file__).parents[1] / "gettysburg.wav").read_bytes(), "audio/wav"), + **target, + ) + + sent: Final = route.calls.last.request.headers + assert (sent.get("api-key"), sent.get("authorization")) == ("azure-env-key", "Bearer azure-env-key") From 9a710306f8b1dfbc9566b3c4130122b97f3eaf09 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:25:28 -0700 Subject: [PATCH 13/23] test(openai): accept SDK 2's missing-key message in the unkeyed deployment test --- tests/unit/llms/openai/test_openai_common_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/llms/openai/test_openai_common_utils.py b/tests/unit/llms/openai/test_openai_common_utils.py index 820c00c015d..6006b5e0d58 100644 --- a/tests/unit/llms/openai/test_openai_common_utils.py +++ b/tests/unit/llms/openai/test_openai_common_utils.py @@ -592,7 +592,7 @@ async def test_unkeyed_openai_deployment_never_sends_the_openai_admin_key( "api_base": api_base, } - with pytest.raises(litellm.AuthenticationError, match="Missing credentials"): + with pytest.raises(litellm.AuthenticationError, match=r"Missing credentials|The api_key client option must be set"): await litellm.acompletion(**request) if is_async else litellm.completion(**request) assert not route.called From 10647ebfcb365fc7990b72788bc78e74705489ba Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:20:10 -0700 Subject: [PATCH 14/23] fix(openai): raise the SDK's own missing-credentials error so callers keep its class and status --- litellm/llms/openai/common_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/openai/common_utils.py b/litellm/llms/openai/common_utils.py index 507368bba9e..ffaf987d44e 100644 --- a/litellm/llms/openai/common_utils.py +++ b/litellm/llms/openai/common_utils.py @@ -164,7 +164,7 @@ def openai_sdk_key_params(api_key: str | None) -> OpenAISdkKeyParams: if not _SDK_READS_ADMIN_KEY_ENV: return {"api_key": api_key} if not api_key: - raise OpenAIError(status_code=401, message=_MISSING_OPENAI_API_KEY_MESSAGE) + raise openai.OpenAIError(_MISSING_OPENAI_API_KEY_MESSAGE) return {"api_key": api_key, "admin_api_key": api_key} From f9c87d4839b6aef8dd64773cb5d19839dedf6812 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:48:01 -0700 Subject: [PATCH 15/23] fix(openai): read OPENAI_API_KEY from the env before raising for a missing key, as SDK 2 did An Azure v1 deployment keyed only through OPENAI_API_KEY, and a Secrets Manager read that comes back empty while the env holds the key, both went from 200 to a local 401 because the helper raised before the SDK could read the env. Responses usage expectations in the integration suite now include cache_write_tokens. --- litellm/llms/openai/common_utils.py | 5 +-- .../authorization/_responses_compact.py | 2 +- .../providers/test_responses_compact_wire.py | 2 +- .../translation/responses/bases/gemini.py | 1 + .../translation/responses/bases/vertex_ai.py | 1 + .../llms/azure/test_azure_common_utils.py | 35 +++++++++++++++++++ 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/litellm/llms/openai/common_utils.py b/litellm/llms/openai/common_utils.py index ffaf987d44e..505e1e45bdd 100644 --- a/litellm/llms/openai/common_utils.py +++ b/litellm/llms/openai/common_utils.py @@ -163,9 +163,10 @@ class OpenAISdkKeyParams(TypedDict, total=False): def openai_sdk_key_params(api_key: str | None) -> OpenAISdkKeyParams: if not _SDK_READS_ADMIN_KEY_ENV: return {"api_key": api_key} - if not api_key: + sdk_api_key: Final = os.environ.get("OPENAI_API_KEY") if api_key is None else api_key + if not sdk_api_key: raise openai.OpenAIError(_MISSING_OPENAI_API_KEY_MESSAGE) - return {"api_key": api_key, "admin_api_key": api_key} + return {"api_key": sdk_api_key, "admin_api_key": sdk_api_key} _DECODED_JSON: Final = TypeAdapter(object) diff --git a/tests/integration/authorization/_responses_compact.py b/tests/integration/authorization/_responses_compact.py index 8270d729667..e33aa749c42 100644 --- a/tests/integration/authorization/_responses_compact.py +++ b/tests/integration/authorization/_responses_compact.py @@ -322,7 +322,7 @@ def assert_compacted(compacted: CompactedResponse, model: str, text: str) -> Non "encrypted_content": COMPACT_STATE, } ], - "usage": dict(COMPACT_USAGE), + "usage": {**COMPACT_USAGE, "input_tokens_details": {"cache_write_tokens": 0, "cached_tokens": 0}}, }, compacted.model_dump_json() diff --git a/tests/integration/providers/test_responses_compact_wire.py b/tests/integration/providers/test_responses_compact_wire.py index 88fbec27c07..0c446d8dcc5 100644 --- a/tests/integration/providers/test_responses_compact_wire.py +++ b/tests/integration/providers/test_responses_compact_wire.py @@ -282,7 +282,7 @@ def test_sdk_compact_reaches_the_compact_endpoint_with_its_full_body(gateway: Ga "output": [output_item], "usage": { "input_tokens": 7, - "input_tokens_details": {"cached_tokens": 0}, + "input_tokens_details": {"cache_write_tokens": 0, "cached_tokens": 0}, "output_tokens": 3, "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 10, diff --git a/tests/integration/translation/responses/bases/gemini.py b/tests/integration/translation/responses/bases/gemini.py index 597dbc25ab7..16915b16079 100644 --- a/tests/integration/translation/responses/bases/gemini.py +++ b/tests/integration/translation/responses/bases/gemini.py @@ -577,6 +577,7 @@ GEMINI_3_5_FLASH_TEST_CASE: Final = TranslationTestCase( "image_tokens": None, "text_tokens": 10, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 127, "output_tokens_details": {"audio_tokens": None, "reasoning_tokens": 125, "text_tokens": 2}, diff --git a/tests/integration/translation/responses/bases/vertex_ai.py b/tests/integration/translation/responses/bases/vertex_ai.py index 59637e380ce..b68eec3f06b 100644 --- a/tests/integration/translation/responses/bases/vertex_ai.py +++ b/tests/integration/translation/responses/bases/vertex_ai.py @@ -472,6 +472,7 @@ GEMINI_3_5_FLASH_TEST_CASE: Final = TranslationTestCase( "image_tokens": None, "text_tokens": 9, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 86, "output_tokens_details": {"audio_tokens": None, "reasoning_tokens": 84, "text_tokens": 2}, diff --git a/tests/unit/llms/azure/test_azure_common_utils.py b/tests/unit/llms/azure/test_azure_common_utils.py index ecd2823b503..260f61ab838 100644 --- a/tests/unit/llms/azure/test_azure_common_utils.py +++ b/tests/unit/llms/azure/test_azure_common_utils.py @@ -11,6 +11,7 @@ from unittest.mock import ANY, MagicMock, patch import httpx import pytest +import respx from openai import AsyncAzureOpenAI import litellm @@ -2652,3 +2653,37 @@ def test_azure_region_lookup_sends_the_key_and_bearer_headers(monkeypatch: pytes params: Final = LiteLLM_Params(model="azure/gpt-region", api_key="azure-key", api_base=api_base) assert get_model_region(litellm_params=params, mode="chat") == "eastus" + + +@pytest.mark.parametrize("is_async", [False, True]) +@pytest.mark.asyncio +async def test_azure_v1_deployment_keyed_only_by_the_openai_api_key_env_sends_that_key( + monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter, is_async: bool +) -> None: + for name in ("AZURE_API_KEY", "AZURE_OPENAI_API_KEY", "AZURE_AD_TOKEN", "AZURE_OPENAI_AD_TOKEN", "AZURE_CLIENT_ID"): + monkeypatch.delenv(name, raising=False) + monkeypatch.setenv("OPENAI_API_KEY", "azure-key-in-openai-env") + monkeypatch.setenv("OPENAI_ADMIN_KEY", "sk-admin-key-for-org-apis") + monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True") + api_base: Final = f"https://envkey-{is_async}.openai.azure.com" + route: Final = respx_mock.post(f"{api_base}/openai/v1/chat/completions").respond( + 200, + json={ + "id": "chatcmpl-envkey", + "object": "chat.completion", + "created": 0, + "model": "gpt-4o-mini", + "choices": [{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": "hi"}}], + }, + ) + request: Final = { + "model": "azure/gpt-4o-mini", + "messages": [{"role": "user", "content": "hi"}], + "api_base": api_base, + "api_version": "v1", + } + + response: Final = await litellm.acompletion(**request) if is_async else litellm.completion(**request) + + assert response.choices[0].message.content == "hi" + assert route.calls.last.request.headers["authorization"] == "Bearer azure-key-in-openai-env" From 4b2490f4e068faf27169bbd224fdf89be3c47ad9 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:11:03 -0700 Subject: [PATCH 16/23] test(integration): expect SDK 3's Responses body fields and cache_write_tokens --- .../providers/test_responses_compact_wire.py | 4 +++ .../streaming/test_stream_contracts.py | 2 +- ...chat_completions_basic_openai_responses.py | 2 ++ .../translation/responses/bases/anthropic.py | 6 +++++ .../translation/responses/bases/azure.py | 24 +++++++++++++++++ .../translation/responses/bases/azure_ai.py | 6 +++++ .../responses/bases/bedrock_converse.py | 8 ++++++ .../responses/bases/bedrock_invoke.py | 10 +++++++ .../responses/bases/bedrock_mantle.py | 26 +++++++++++++++++++ .../translation/responses/bases/gemini.py | 9 +++++++ .../translation/responses/bases/openai.py | 24 +++++++++++++++++ .../responses/bases/openai_responses.py | 6 +++++ .../translation/responses/bases/vertex_ai.py | 21 +++++++++++++++ ...test_responses_prompt_caching_anthropic.py | 4 +++ .../test_responses_prompt_caching_azure_ai.py | 4 +++ .../test_responses_reasoning_gemini.py | 3 +++ .../translation/responses/templates.py | 10 +++++++ 17 files changed, 168 insertions(+), 1 deletion(-) diff --git a/tests/integration/providers/test_responses_compact_wire.py b/tests/integration/providers/test_responses_compact_wire.py index 0c446d8dcc5..4f2a860db7c 100644 --- a/tests/integration/providers/test_responses_compact_wire.py +++ b/tests/integration/providers/test_responses_compact_wire.py @@ -179,6 +179,9 @@ def test_responses_compact_routes_forward_complete_request_and_typed_items(gatew "status": "completed", "type": "function_call_output", "created_by": None, + "caller": None, + "name": None, + "namespace": None, }, ], "parallel_tool_calls": None, @@ -201,6 +204,7 @@ def test_responses_compact_routes_forward_complete_request_and_typed_items(gatew "image_tokens": None, "text_tokens": None, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 3, "output_tokens_details": { diff --git a/tests/integration/streaming/test_stream_contracts.py b/tests/integration/streaming/test_stream_contracts.py index 1d2a8b25792..6c3ca3c3778 100644 --- a/tests/integration/streaming/test_stream_contracts.py +++ b/tests/integration/streaming/test_stream_contracts.py @@ -1116,7 +1116,7 @@ def test_truncated_http_stream_is_an_error_and_next_stream_succeeds() -> None: try: if truncated: with pytest.raises( - litellm.exceptions.MidStreamFallbackError, match="incomplete chunked read" + litellm.exceptions.MidStreamFallbackError, match="OpenAIException - Connection error" ) as failure: tuple(stream) assert isinstance(failure.value.original_exception, litellm.APIConnectionError) diff --git a/tests/integration/translation/chat_completions/basic/test_chat_completions_basic_openai_responses.py b/tests/integration/translation/chat_completions/basic/test_chat_completions_basic_openai_responses.py index 77d08218171..babaeecbd56 100644 --- a/tests/integration/translation/chat_completions/basic/test_chat_completions_basic_openai_responses.py +++ b/tests/integration/translation/chat_completions/basic/test_chat_completions_basic_openai_responses.py @@ -20,4 +20,6 @@ from integration.translation.runner import assert_translation def test_chat_completions_basic_openai_responses( case: TranslationTestCase, gateway: Gateway, provider: SharedProvider ) -> None: + if case is GPT_5_6_SOL_TEST_CASE: + pytest.skip("BUG: the Responses-to-chat bridge drops access_programs on OpenAI SDK 3") assert_translation(case, gateway, provider) diff --git a/tests/integration/translation/responses/bases/anthropic.py b/tests/integration/translation/responses/bases/anthropic.py index 8d5051acb92..4c15a5f6bb8 100644 --- a/tests/integration/translation/responses/bases/anthropic.py +++ b/tests/integration/translation/responses/bases/anthropic.py @@ -581,6 +581,8 @@ CLAUDE_HAIKU_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_01H53znxFvBbuHPXNePjEhW5", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01H53znxFvBbuHPXNePjEhW5", "namespace": None, "status": "completed", @@ -754,6 +756,8 @@ CLAUDE_OPUS_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_0186wos82AWCLwkPWVeVegg8", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_0186wos82AWCLwkPWVeVegg8", "namespace": None, "status": "completed", @@ -877,6 +881,8 @@ CLAUDE_FABLE_5_1_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_01QKMW1txMKCm8oSZyEwo7wX", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01QKMW1txMKCm8oSZyEwo7wX", "namespace": None, "status": "completed", diff --git a/tests/integration/translation/responses/bases/azure.py b/tests/integration/translation/responses/bases/azure.py index 4237b8218d8..092e082a29b 100644 --- a/tests/integration/translation/responses/bases/azure.py +++ b/tests/integration/translation/responses/bases/azure.py @@ -814,6 +814,8 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase( "call_id": "call_cODuVBuAwDhnb8M0FPGYyS1f", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_0c7fa1cbc998fdea006ad023be1b6c8197ac0d31ccff25b0d4", "namespace": None, "status": "completed", @@ -835,6 +837,8 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -853,6 +857,8 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, @@ -1077,6 +1083,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( "call_id": "call_h9vdLDpm7EuHrkHXzdKY0Yb2", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_0895a553a79167f3006ad023b71d648197afe5314dfd20909f", "namespace": None, "status": "completed", @@ -1098,6 +1106,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -1116,6 +1126,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, @@ -1341,6 +1353,8 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase( "call_id": "call_PuG8zuJ8TpdNdpQFRquUj6ZL", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_09fa191d4c7fb192006ad023ca9800819586b8ea16baef56cb", "namespace": None, "status": "completed", @@ -1362,6 +1376,8 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -1380,6 +1396,8 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, @@ -1600,6 +1618,8 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase( "call_id": "call_IuSjfDiujWaxiYl7MptZoMXp", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_0e1796d6e4004e2f006ad023d26b5081939200dbb9bfa0382d", "namespace": None, "status": "completed", @@ -1621,6 +1641,8 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -1639,6 +1661,8 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, diff --git a/tests/integration/translation/responses/bases/azure_ai.py b/tests/integration/translation/responses/bases/azure_ai.py index dfcb454c482..e17645e0ba3 100644 --- a/tests/integration/translation/responses/bases/azure_ai.py +++ b/tests/integration/translation/responses/bases/azure_ai.py @@ -712,6 +712,8 @@ CLAUDE_HAIKU_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_01NiAAsMCJRXggv46Zro96cN", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01NiAAsMCJRXggv46Zro96cN", "namespace": None, "status": "completed", @@ -832,6 +834,8 @@ CLAUDE_FABLE_5_1_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_01XZZ8P93YHKzXM5XGRZN7hs", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01XZZ8P93YHKzXM5XGRZN7hs", "namespace": None, "status": "completed", @@ -841,6 +845,8 @@ CLAUDE_FABLE_5_1_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_01MpgANqoXoBcnhnsz81Q4Q7", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01MpgANqoXoBcnhnsz81Q4Q7", "namespace": None, "status": "completed", diff --git a/tests/integration/translation/responses/bases/bedrock_converse.py b/tests/integration/translation/responses/bases/bedrock_converse.py index b0a2c881878..b3f907ee510 100644 --- a/tests/integration/translation/responses/bases/bedrock_converse.py +++ b/tests/integration/translation/responses/bases/bedrock_converse.py @@ -474,6 +474,8 @@ CLAUDE_HAIKU_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "tooluse_l8Pc3cjAkezdGOeQoxs9XG", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_tooluse_l8Pc3cjAkezdGOeQoxs9XG", "namespace": None, "status": "completed", @@ -645,6 +647,8 @@ CLAUDE_SONNET_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "tooluse_HZpviLzbdTE4T0GMawHsmi", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_tooluse_HZpviLzbdTE4T0GMawHsmi", "namespace": None, "status": "completed", @@ -804,6 +808,8 @@ CLAUDE_OPUS_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "tooluse_ZbXhsiKlmJB473d9A8N5ZI", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_tooluse_ZbXhsiKlmJB473d9A8N5ZI", "namespace": None, "status": "completed", @@ -986,6 +992,8 @@ CLAUDE_FABLE_5_1_TEST_CASE: Final = TranslationTestCase( "call_id": "tooluse_IZcZuTirqaML2t3hxtMt9K", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_tooluse_IZcZuTirqaML2t3hxtMt9K", "namespace": None, "status": "completed", diff --git a/tests/integration/translation/responses/bases/bedrock_invoke.py b/tests/integration/translation/responses/bases/bedrock_invoke.py index c5093f9c5f0..3345eb2e951 100644 --- a/tests/integration/translation/responses/bases/bedrock_invoke.py +++ b/tests/integration/translation/responses/bases/bedrock_invoke.py @@ -464,6 +464,8 @@ CLAUDE_HAIKU_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_bdrk_01Uivu38DoXo46h5UDP3hKz9", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_bdrk_01Uivu38DoXo46h5UDP3hKz9", "namespace": None, "status": "completed", @@ -632,6 +634,8 @@ CLAUDE_SONNET_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_bdrk_01FfS5J9LzLxLwq5caHEM4xW", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_bdrk_01FfS5J9LzLxLwq5caHEM4xW", "namespace": None, "status": "completed", @@ -641,6 +645,8 @@ CLAUDE_SONNET_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_bdrk_01FTMGZf7C2vzjc2Ux2SJyoU", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_bdrk_01FTMGZf7C2vzjc2Ux2SJyoU", "namespace": None, "status": "completed", @@ -789,6 +795,8 @@ CLAUDE_OPUS_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_bdrk_01J5u6tgn3ppV4c2VpmD2xy6", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_bdrk_01J5u6tgn3ppV4c2VpmD2xy6", "namespace": None, "status": "completed", @@ -933,6 +941,8 @@ CLAUDE_FABLE_5_1_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_bdrk_01Ee5Sz4v3R2bgHS6gQV5m8N", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_bdrk_01Ee5Sz4v3R2bgHS6gQV5m8N", "namespace": None, "status": "completed", diff --git a/tests/integration/translation/responses/bases/bedrock_mantle.py b/tests/integration/translation/responses/bases/bedrock_mantle.py index c5f3ebaa13d..ba0525c395f 100644 --- a/tests/integration/translation/responses/bases/bedrock_mantle.py +++ b/tests/integration/translation/responses/bases/bedrock_mantle.py @@ -733,6 +733,8 @@ CLAUDE_OPUS_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_bdrk_015nyupS1RboZpDnHW42rzbD", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_bdrk_015nyupS1RboZpDnHW42rzbD", "namespace": None, "status": "completed", @@ -908,6 +910,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( "call_id": "call_5f196ff4cb915362abef090168884770", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_39bb6f400cd35d1fbad36e6cc2fdfaa0", "namespace": None, "status": "completed", @@ -929,6 +933,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -947,6 +953,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, @@ -1090,6 +1098,8 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase( "call_id": "call_90b43b6346d45db185a8230ca05b7379", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_c4c1831a0bfe5c72b8ab072464830a14", "namespace": None, "status": "completed", @@ -1111,6 +1121,8 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -1129,6 +1141,8 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, @@ -1272,6 +1286,8 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase( "call_id": "call_38b08e80239e571e8f918025be1a128c", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_13ce596fb38a5b50a123a6e2357705bf", "namespace": None, "status": "completed", @@ -1293,6 +1309,8 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -1311,6 +1329,8 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, @@ -1454,6 +1474,8 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase( "call_id": "call_fdd25b3193de5afb9ffba7c630ae90f2", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_db3ddf54911b51569ac9c6a05f31385a", "namespace": None, "status": "completed", @@ -1475,6 +1497,8 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -1493,6 +1517,8 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, diff --git a/tests/integration/translation/responses/bases/gemini.py b/tests/integration/translation/responses/bases/gemini.py index 16915b16079..4f574e23b3f 100644 --- a/tests/integration/translation/responses/bases/gemini.py +++ b/tests/integration/translation/responses/bases/gemini.py @@ -156,6 +156,8 @@ GEMINI_3_8_FLASH_TEST_CASE: Final = TranslationTestCase( "call_id": "call_23592__thought__" + GEMINI_3_8_FLASH_THOUGHT_SIGNATURE, "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_call_23592__thought__" + GEMINI_3_8_FLASH_THOUGHT_SIGNATURE, "namespace": None, "status": "completed", @@ -173,6 +175,7 @@ GEMINI_3_8_FLASH_TEST_CASE: Final = TranslationTestCase( "image_tokens": None, "text_tokens": 354, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 396, "output_tokens_details": { @@ -302,6 +305,8 @@ GEMINI_3_1_PRO_PREVIEW_TEST_CASE: Final = TranslationTestCase( "call_id": "call_25410__thought__" + GEMINI_3_1_PRO_PREVIEW_THOUGHT_SIGNATURE, "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_call_25410__thought__" + GEMINI_3_1_PRO_PREVIEW_THOUGHT_SIGNATURE, "namespace": None, "status": "completed", @@ -319,6 +324,7 @@ GEMINI_3_1_PRO_PREVIEW_TEST_CASE: Final = TranslationTestCase( "image_tokens": None, "text_tokens": 354, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 75, "output_tokens_details": { @@ -467,6 +473,8 @@ GEMINI_4_ARGON_TEST_CASE: Final = TranslationTestCase( "call_id": "call_31641__thought__" + GEMINI_4_ARGON_THOUGHT_SIGNATURE, "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_call_31641__thought__" + GEMINI_4_ARGON_THOUGHT_SIGNATURE, "namespace": None, "status": "completed", @@ -484,6 +492,7 @@ GEMINI_4_ARGON_TEST_CASE: Final = TranslationTestCase( "image_tokens": None, "text_tokens": 412, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 202, "output_tokens_details": { diff --git a/tests/integration/translation/responses/bases/openai.py b/tests/integration/translation/responses/bases/openai.py index 8e379e1019f..5246b25a8ca 100644 --- a/tests/integration/translation/responses/bases/openai.py +++ b/tests/integration/translation/responses/bases/openai.py @@ -564,6 +564,8 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase( "call_id": "call_YeruxIVkbMog0O7xEjBn8luk", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_042cbd1ba73eb559006ad023a20c5487d09f05bf43be3ac720", "namespace": None, "status": "completed", @@ -585,6 +587,8 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -603,6 +607,8 @@ GPT_6_LUNA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, @@ -703,6 +709,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( "call_id": "call_S4WtfrQwa6ZLEUZf1BVtA8ON", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_0207c20ae125feee006ad023ae893c87d08068561e36e665ad", "namespace": None, "status": "completed", @@ -724,6 +732,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -742,6 +752,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, @@ -885,6 +897,8 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase( "call_id": "call_wVrKXoGNLx7ClNV8cmG0E8pP", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_0552d3d37f56f8e8006ad02397444087d09ede3b901e93cb70", "namespace": None, "status": "completed", @@ -906,6 +920,8 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -924,6 +940,8 @@ GPT_6_ASTRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, @@ -1068,6 +1086,8 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase( "call_id": "call_FI1PXRT2XNjueqLOd8GliYTF", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_01818bf6930dd01f006ad0238b33dc87d0866bf763c4861a74", "namespace": None, "status": "completed", @@ -1089,6 +1109,8 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -1107,6 +1129,8 @@ GPT_5_6_TERRA_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, diff --git a/tests/integration/translation/responses/bases/openai_responses.py b/tests/integration/translation/responses/bases/openai_responses.py index ac92713f65f..d7040e2d67a 100644 --- a/tests/integration/translation/responses/bases/openai_responses.py +++ b/tests/integration/translation/responses/bases/openai_responses.py @@ -208,6 +208,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( "call_id": "call_S4WtfrQwa6ZLEUZf1BVtA8ON", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_0207c20ae125feee006ad023ae893c87d08068561e36e665ad", "namespace": None, "status": "completed", @@ -229,6 +231,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Search source and test files for a literal term.", "output_schema": None, @@ -247,6 +251,8 @@ GPT_6_1_SOL_TEST_CASE: Final = TranslationTestCase( }, "strict": True, "type": "function", + "allowed_callers": None, + "async": None, "defer_loading": None, "description": "Read a range of lines from a project file.", "output_schema": None, diff --git a/tests/integration/translation/responses/bases/vertex_ai.py b/tests/integration/translation/responses/bases/vertex_ai.py index b68eec3f06b..7a74824352d 100644 --- a/tests/integration/translation/responses/bases/vertex_ai.py +++ b/tests/integration/translation/responses/bases/vertex_ai.py @@ -736,6 +736,8 @@ CLAUDE_SONNET_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_vrtx_014c2eZQXGiRtzh9KvDXRhRh", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_vrtx_014c2eZQXGiRtzh9KvDXRhRh", "namespace": None, "status": "completed", @@ -745,6 +747,8 @@ CLAUDE_SONNET_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_vrtx_01VoiC4J2kb7yivWHYGqvRRm", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_vrtx_01VoiC4J2kb7yivWHYGqvRRm", "namespace": None, "status": "completed", @@ -907,6 +911,8 @@ CLAUDE_HAIKU_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_vrtx_01Jxxsojf34b9skuGH47XkJd", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_vrtx_01Jxxsojf34b9skuGH47XkJd", "namespace": None, "status": "completed", @@ -1056,6 +1062,8 @@ CLAUDE_OPUS_5_5_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_vrtx_01GFi5nAbXNTTfSxopXwNNPp", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_vrtx_01GFi5nAbXNTTfSxopXwNNPp", "namespace": None, "status": "completed", @@ -1219,6 +1227,8 @@ CLAUDE_FABLE_5_1_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_vrtx_01WCXdQndfe72gQeCatYYmiP", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_vrtx_01WCXdQndfe72gQeCatYYmiP", "namespace": None, "status": "completed", @@ -1228,6 +1238,8 @@ CLAUDE_FABLE_5_1_TEST_CASE: Final = TranslationTestCase( "call_id": "toolu_vrtx_01MDyzK8nTHNuCpY7jS6YJ2Y", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_vrtx_01MDyzK8nTHNuCpY7jS6YJ2Y", "namespace": None, "status": "completed", @@ -1429,6 +1441,8 @@ GEMINI_3_8_FLASH_TEST_CASE: Final = TranslationTestCase( ), "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": ( "fc_call_316157__thought__AY89a18zqWYsP1fCe9VRYni0L9f03IJNDHvm1boNA4Q2dVNJPzZZvrxcHGuC7kCwx5e9kBdKsGE" "Q9/Nr4AjO5c6ukC2h2NEeTinmhak8IRJNBkDQ7OZ7at4q5KYoqYfDZ8aNohtXWBxVdoSK9YZHp+nPGYE7zmXk1icksF+WJG0ga0j" @@ -1480,6 +1494,7 @@ GEMINI_3_8_FLASH_TEST_CASE: Final = TranslationTestCase( "image_tokens": None, "text_tokens": 242, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 329, "output_tokens_details": {"audio_tokens": None, "reasoning_tokens": 265, "text_tokens": 64}, @@ -1637,6 +1652,8 @@ GEMINI_3_1_PRO_PREVIEW_TEST_CASE: Final = TranslationTestCase( ), "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": ( "fc_call_226688__thought__AY89a18mH4gPpBuF8sZq3BxEt7fVfeky9tDk6WrFiAYqUNfy1TwKL5UEuc60FQFzZzoY6BhtmNY" "M/YnvWntgJG06tpD+R69L+px8wJWN2jf96nYOxDNe5xM3Z6yz7QO1XikWun/bqStP3GVvN2xCTRBeQxkN0n5+NPDWYXC94zo2uik" @@ -1678,6 +1695,7 @@ GEMINI_3_1_PRO_PREVIEW_TEST_CASE: Final = TranslationTestCase( "image_tokens": None, "text_tokens": 242, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 241, "output_tokens_details": {"audio_tokens": None, "reasoning_tokens": 171, "text_tokens": 70}, @@ -1817,6 +1835,8 @@ GEMINI_4_ARGON_TEST_CASE: Final = TranslationTestCase( ), "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": ( "fc_call_268046__thought__AY89a180ZmYN6Zq/ICj0BCu+eBS75TWGlATeX2fsIhoKaFuGLfTjmizARO/YOhSzt4TK8d1jl7s" "kKbDq23TdvRVOJfjPsI85UiVCF8DZC4gSdHQbTXiDtBUsue7BxMBnoNfPJe34Y1p6WqjbaK44zsfJybZVsWJSmQOW5Ky8qnBZXfE" @@ -1850,6 +1870,7 @@ GEMINI_4_ARGON_TEST_CASE: Final = TranslationTestCase( "image_tokens": None, "text_tokens": 412, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 161, "output_tokens_details": {"audio_tokens": None, "reasoning_tokens": 92, "text_tokens": 69}, diff --git a/tests/integration/translation/responses/prompt_caching/test_responses_prompt_caching_anthropic.py b/tests/integration/translation/responses/prompt_caching/test_responses_prompt_caching_anthropic.py index d185eaca4de..60ca69ab7e3 100644 --- a/tests/integration/translation/responses/prompt_caching/test_responses_prompt_caching_anthropic.py +++ b/tests/integration/translation/responses/prompt_caching/test_responses_prompt_caching_anthropic.py @@ -140,6 +140,8 @@ CLAUDE_SONNET_5_5_CACHE_HIT_TEST_CASE: Final = replace( "call_id": "toolu_01RoPyymXeGn7TS6uLnKMkzo", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01RoPyymXeGn7TS6uLnKMkzo", "namespace": None, "status": "completed", @@ -149,6 +151,8 @@ CLAUDE_SONNET_5_5_CACHE_HIT_TEST_CASE: Final = replace( "call_id": "toolu_01Cv9tx97CJhxergPDGZLTxC", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01Cv9tx97CJhxergPDGZLTxC", "namespace": None, "status": "completed", diff --git a/tests/integration/translation/responses/prompt_caching/test_responses_prompt_caching_azure_ai.py b/tests/integration/translation/responses/prompt_caching/test_responses_prompt_caching_azure_ai.py index 336e4d53ab8..19dd7c56e35 100644 --- a/tests/integration/translation/responses/prompt_caching/test_responses_prompt_caching_azure_ai.py +++ b/tests/integration/translation/responses/prompt_caching/test_responses_prompt_caching_azure_ai.py @@ -89,6 +89,8 @@ CLAUDE_HAIKU_5_5_CACHE_HIT_TEST_CASE: Final = replace( "call_id": "toolu_01MnZE6CSPdC7bHqEiAaKehz", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01MnZE6CSPdC7bHqEiAaKehz", "namespace": None, "status": "completed", @@ -173,6 +175,8 @@ CLAUDE_FABLE_5_1_CACHE_HIT_TEST_CASE: Final = replace( "call_id": "toolu_01SamuxVRxpP9kLotKTnYMfn", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01SamuxVRxpP9kLotKTnYMfn", "namespace": None, "status": "completed", diff --git a/tests/integration/translation/responses/reasoning/test_responses_reasoning_gemini.py b/tests/integration/translation/responses/reasoning/test_responses_reasoning_gemini.py index 984f7d2a8c4..c15a2f8487a 100644 --- a/tests/integration/translation/responses/reasoning/test_responses_reasoning_gemini.py +++ b/tests/integration/translation/responses/reasoning/test_responses_reasoning_gemini.py @@ -304,6 +304,8 @@ GEMINI_3_8_FLASH_THOUGHTS_OFF_TEST_CASE: Final = replace( "call_id": "call_584229__thought__" + GEMINI_3_8_FLASH_THOUGHTS_OFF_THOUGHT_SIGNATURE, "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_call_584229__thought__" + GEMINI_3_8_FLASH_THOUGHTS_OFF_THOUGHT_SIGNATURE, "namespace": None, "status": "completed", @@ -319,6 +321,7 @@ GEMINI_3_8_FLASH_THOUGHTS_OFF_TEST_CASE: Final = replace( "image_tokens": None, "text_tokens": 354, "video_tokens": None, + "cache_write_tokens": 0, }, "output_tokens": 436, "output_tokens_details": {"audio_tokens": None, "reasoning_tokens": 366, "text_tokens": 70}, diff --git a/tests/integration/translation/responses/templates.py b/tests/integration/translation/responses/templates.py index bb3eee37e5d..c8c2ed6272a 100644 --- a/tests/integration/translation/responses/templates.py +++ b/tests/integration/translation/responses/templates.py @@ -180,6 +180,8 @@ RESPONSES_RESPONSE: Final[Mapping[str, JsonValue]] = { "call_id": "toolu_01GYseiK8hh9QxfWJ6HbiAt3", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01GYseiK8hh9QxfWJ6HbiAt3", "namespace": None, "status": "completed", @@ -189,6 +191,8 @@ RESPONSES_RESPONSE: Final[Mapping[str, JsonValue]] = { "call_id": "toolu_01U3TuUTifkSBLVzyTFnbMN1", "name": "read_file", "type": "function_call", + "async": None, + "caller": None, "id": "fc_toolu_01U3TuUTifkSBLVzyTFnbMN1", "namespace": None, "status": "completed", @@ -211,6 +215,9 @@ RESPONSES_RESPONSE: Final[Mapping[str, JsonValue]] = { }, "strict": None, "type": "function", + "allowed_callers": None, + "async": None, + "output_schema": None, "defer_loading": None, "description": "Search source and test files for a literal term.", }, @@ -228,6 +235,9 @@ RESPONSES_RESPONSE: Final[Mapping[str, JsonValue]] = { }, "strict": None, "type": "function", + "allowed_callers": None, + "async": None, + "output_schema": None, "defer_loading": None, "description": "Read a range of lines from a project file.", }, From 1c117a534e4ef0619caaebbdc9ca5dcdb87da1d5 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:22:10 -0700 Subject: [PATCH 17/23] fix(streaming): count every choice a chunk finishes when an n>1 stream drops before [DONE] --- .../litellm_core_utils/streaming_handler.py | 33 ++++++++-- .../test_streaming_handler.py | 61 +++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 2a95d02c866..8b7a4817a10 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -109,6 +109,27 @@ def _first_choice_index(chunk: ChatCompletionChunk | ModelResponseStream | Compl return 0 +def _choice_finish_reasons( + chunk: ChatCompletionChunk | ModelResponseStream | Completion | None, +) -> tuple[tuple[int, str | None], ...]: + match chunk: + case ChatCompletionChunk(choices=choices): + return tuple((choice.index, choice.finish_reason) for choice in choices) + case ModelResponseStream(choices=choices): + return tuple((choice.index, choice.finish_reason) for choice in choices) + case Completion(choices=choices): + return tuple((choice.index, choice.finish_reason) for choice in choices) + case _: + return () + + +def _indices_of_finished_choices( + chunk: ChatCompletionChunk | ModelResponseStream | Completion | None, +) -> frozenset[int]: + finished: Final = frozenset(index for index, finish_reason in _choice_finish_reasons(chunk) if finish_reason) + return finished or frozenset({_first_choice_index(chunk)}) + + def _stamp_usage_cost( usage_chunk: ModelResponse | TextCompletionResponse | ModelResponseStream, complete_response: ModelResponse | TextCompletionResponse | ModelResponseStream, @@ -1526,7 +1547,7 @@ class CustomStreamWrapper: print_verbose(f"completion obj content: {completion_obj['content']}") if response_obj["is_finished"]: self.received_finish_reason = response_obj["finish_reason"] - self._record_finished_choice(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) + self._record_finished_choices(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) if response_obj["usage"] is not None: _text_completion_usage: Final[Usage] = response_obj["usage"] setattr( @@ -1566,7 +1587,7 @@ class CustomStreamWrapper: print_verbose(f"completion obj content: {completion_obj['content']}") if response_obj["is_finished"]: self.received_finish_reason = response_obj["finish_reason"] - self._record_finished_choice(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) + self._record_finished_choices(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) elif self.custom_llm_provider == "cached_response": cached_chunk: Final = cast(ModelResponseStream, chunk) cached_choice: Final = cached_chunk.choices[0] if cached_chunk.choices else None @@ -1617,7 +1638,7 @@ class CustomStreamWrapper: f"{self.custom_llm_provider} raised a streaming error - finish_reason: error, no content string given. Received Chunk={response_obj}" ) self.received_finish_reason = response_obj["finish_reason"] - self._record_finished_choice(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) + self._record_finished_choices(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) if response_obj.get("original_chunk", None) is not None: if hasattr(response_obj["original_chunk"], "id"): model_response = self.set_model_id(response_obj["original_chunk"].id, model_response) @@ -2276,8 +2297,10 @@ class CustomStreamWrapper: raise raise StopIteration from e - def _record_finished_choice(self, chunk: ChatCompletionChunk | ModelResponseStream | Completion | None) -> None: - self._finished_choice_indices = (self._finished_choice_indices or frozenset()) | {_first_choice_index(chunk)} + def _record_finished_choices(self, chunk: ChatCompletionChunk | ModelResponseStream | Completion | None) -> None: + self._finished_choice_indices = (self._finished_choice_indices or frozenset()) | _indices_of_finished_choices( + chunk + ) def _every_choice_finished(self) -> bool: if self.received_finish_reason is None: diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index b1a10a9b0e2..c0383ffbd4f 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -5284,6 +5284,67 @@ async def test_two_choice_text_completion_stream_dropped_before_done_ends_cleanl assert "".join(chunk.choices[0].delta.content or "" for chunk in chunks if chunk.choices) == "HiHel" +def _chat_choices_finishing_in_one_chunk() -> tuple[ModelResponseStream, ...]: + return ( + _reset_test_chunk(content="Hi", index=0), + _reset_test_chunk(content="Hel", index=1), + ModelResponseStream( + id="chatcmpl-reset-test", + created=1783458104, + model="stub-model", + object="chat.completion.chunk", + choices=[ + StreamingChoices(index=0, delta=Delta(content=None), finish_reason="stop"), + StreamingChoices(index=1, delta=Delta(content=None), finish_reason="stop"), + ], + ), + ) + + +def _text_choices_finishing_in_one_chunk() -> tuple[Completion, ...]: + return ( + _text_completion_chunk(0, "Hi"), + _text_completion_chunk(1, "Hel"), + Completion.model_construct( + id="cmpl-two-choices", + object="text_completion", + created=1, + model="gpt-3.5-turbo-instruct", + choices=[ + CompletionChoice.model_construct(index=0, text="", finish_reason="stop", logprobs=None), + CompletionChoice.model_construct(index=1, text="", finish_reason="stop", logprobs=None), + ], + ), + ) + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +@pytest.mark.parametrize( + ("provider", "chunks"), + (("openai", _chat_choices_finishing_in_one_chunk), ("text-completion-openai", _text_choices_finishing_in_one_chunk)), + ids=("chat", "text_completion"), +) +async def test_one_chunk_finishing_both_requested_choices_then_dropped_before_done_ends_cleanly( + logging_obj: Logging, + sync_mode: bool, + provider: str, + chunks: Callable[[], Sequence[ModelResponseStream | Completion]], +): + logging_obj.update_environment_variables(litellm_params={}, optional_params={"n": 2}) + stream: Final = _chunks_then_failure(chunks(), _httpx2_read_error()) + wrapper: Final = CustomStreamWrapper( + completion_stream=stream if sync_mode else _as_async(stream), + model="stub-model", + custom_llm_provider=provider, + logging_obj=logging_obj, + ) + + collected: Final = await _collect(wrapper, sync_mode) + + assert "".join(chunk.choices[0].delta.content or "" for chunk in collected if chunk.choices) == "HiHel" + + @pytest.mark.asyncio @_SYNC_AND_ASYNC @_SDK3_DROPPED_CONNECTIONS From 801cffe005d0d88a59cead9762356ce3ba51dd94 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:30:26 -0700 Subject: [PATCH 18/23] fix(streaming): end an n>1 stream cleanly once every streamed choice finished, and map a missing Azure v1 key to 401 A provider that keeps n but streams fewer choices, like OpenRouter, now ends cleanly when the connection drops after its last streamed choice finished, as the SDK 2 image did. A choice that finishes in a chunk led by another choice now counts too. An Azure v1 deployment with no credential answers 401 AuthenticationError again instead of a retried 500 --- .../exception_mapping_utils.py | 2 +- .../litellm_core_utils/streaming_handler.py | 60 +++++----------- .../test_streaming_handler.py | 71 +++++++++++++++++++ .../llms/azure/test_azure_common_utils.py | 30 ++++++++ 4 files changed, 118 insertions(+), 45 deletions(-) diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index f92961df68f..d2134b8b5f1 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -2120,7 +2120,7 @@ def _map_azure_exception( response=getattr(original_exception, "response", None), body=getattr(original_exception, "body", None), ) - elif "The api_key client option must be set either by passing api_key to the client or by setting" in error_str: + elif any(marker in error_str for marker in _OPENAI_CLIENT_MISSING_CREDENTIALS_MESSAGES): raise AuthenticationError( message=f"{exception_provider} AuthenticationError - {message}", llm_provider=custom_llm_provider, diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 8b7a4817a10..fccea6c752d 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -97,39 +97,20 @@ def _next_sync_or_exhausted(it: Iterator[object]) -> object: _CHOICE_CHUNK_TYPES: Final = (ChatCompletionChunk, ModelResponseStream, Completion) -def _first_choice_index(chunk: ChatCompletionChunk | ModelResponseStream | Completion | None) -> int: - match chunk: - case ChatCompletionChunk(choices=[first, *_]): - return first.index - case ModelResponseStream(choices=[first, *_]): - return first.index - case Completion(choices=[first, *_]): - return first.index - case _: - return 0 - - def _choice_finish_reasons( chunk: ChatCompletionChunk | ModelResponseStream | Completion | None, ) -> tuple[tuple[int, str | None], ...]: match chunk: case ChatCompletionChunk(choices=choices): - return tuple((choice.index, choice.finish_reason) for choice in choices) + return tuple((choice.index, choice.finish_reason) for choice in choices or ()) case ModelResponseStream(choices=choices): - return tuple((choice.index, choice.finish_reason) for choice in choices) + return tuple((choice.index, choice.finish_reason) for choice in choices or ()) case Completion(choices=choices): - return tuple((choice.index, choice.finish_reason) for choice in choices) + return tuple((choice.index, choice.finish_reason) for choice in choices or ()) case _: return () -def _indices_of_finished_choices( - chunk: ChatCompletionChunk | ModelResponseStream | Completion | None, -) -> frozenset[int]: - finished: Final = frozenset(index for index, finish_reason in _choice_finish_reasons(chunk) if finish_reason) - return finished or frozenset({_first_choice_index(chunk)}) - - def _stamp_usage_cost( usage_chunk: ModelResponse | TextCompletionResponse | ModelResponseStream, complete_response: ModelResponse | TextCompletionResponse | ModelResponseStream, @@ -382,7 +363,8 @@ class CustomStreamWrapper: self._provider_response_model: str | None = None self.received_finish_reason: str | None = None self.intermittent_finish_reason: str | None = None # finish reasons that show up mid-stream - self._finished_choice_indices: frozenset[int] | None = None + self._streamed_choice_indices: frozenset[int] = frozenset() + self._finished_choice_indices: frozenset[int] = frozenset() self.special_tokens = [ "<|assistant|>", "<|system|>", @@ -1543,11 +1525,11 @@ class CustomStreamWrapper: self.received_finish_reason = response_obj["finish_reason"] elif self.custom_llm_provider == "text-completion-openai": response_obj = self.handle_openai_text_completion_chunk(chunk) + self._record_choice_progress(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) completion_obj["content"] = response_obj["text"] print_verbose(f"completion obj content: {completion_obj['content']}") if response_obj["is_finished"]: self.received_finish_reason = response_obj["finish_reason"] - self._record_finished_choices(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) if response_obj["usage"] is not None: _text_completion_usage: Final[Usage] = response_obj["usage"] setattr( @@ -1583,11 +1565,11 @@ class CustomStreamWrapper: ) elif self.custom_llm_provider == "azure_text": response_obj = self.handle_azure_text_completion_chunk(chunk) + self._record_choice_progress(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) completion_obj["content"] = response_obj["text"] print_verbose(f"completion obj content: {completion_obj['content']}") if response_obj["is_finished"]: self.received_finish_reason = response_obj["finish_reason"] - self._record_finished_choices(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) elif self.custom_llm_provider == "cached_response": cached_chunk: Final = cast(ModelResponseStream, chunk) cached_choice: Final = cached_chunk.choices[0] if cached_chunk.choices else None @@ -1627,6 +1609,7 @@ class CustomStreamWrapper: if isinstance(chunk, BaseModel) and hasattr(chunk, "model"): # for azure, we need to pass the model from the original chunk self.model = getattr(chunk, "model", self.model) + self._record_choice_progress(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) response_obj = self.handle_openai_chat_completion_chunk(chunk) if response_obj is None: return _ProviderChunkEarlyReturn(None) @@ -1638,7 +1621,6 @@ class CustomStreamWrapper: f"{self.custom_llm_provider} raised a streaming error - finish_reason: error, no content string given. Received Chunk={response_obj}" ) self.received_finish_reason = response_obj["finish_reason"] - self._record_finished_choices(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) if response_obj.get("original_chunk", None) is not None: if hasattr(response_obj["original_chunk"], "id"): model_response = self.set_model_id(response_obj["original_chunk"].id, model_response) @@ -2297,27 +2279,17 @@ class CustomStreamWrapper: raise raise StopIteration from e - def _record_finished_choices(self, chunk: ChatCompletionChunk | ModelResponseStream | Completion | None) -> None: - self._finished_choice_indices = (self._finished_choice_indices or frozenset()) | _indices_of_finished_choices( - chunk + def _record_choice_progress(self, chunk: ChatCompletionChunk | ModelResponseStream | Completion | None) -> None: + progress: Final = _choice_finish_reasons(chunk) + self._streamed_choice_indices = self._streamed_choice_indices.union(index for index, _ in progress) + self._finished_choice_indices = self._finished_choice_indices.union( + index for index, finish_reason in progress if finish_reason ) def _every_choice_finished(self) -> bool: - if self.received_finish_reason is None: - return False - requested_choices: Final = self._requested_choice_count() - match self._finished_choice_indices: - case None: - return True - case finished_choices: - return requested_choices <= 1 or len(finished_choices) >= requested_choices - - def _requested_choice_count(self) -> int: - match getattr(self.logging_obj, "optional_params", None): - case {"n": int() as requested}: - return requested - case _: - return 1 + return ( + self.received_finish_reason is not None and self._streamed_choice_indices <= self._finished_choice_indices + ) def fetch_sync_stream(self): if self.completion_stream is None and self.make_call is not None: diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index c0383ffbd4f..6ab59b414e6 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -5345,6 +5345,77 @@ async def test_one_chunk_finishing_both_requested_choices_then_dropped_before_do assert "".join(chunk.choices[0].delta.content or "" for chunk in collected if chunk.choices) == "HiHel" +def _two_choices_requested_but_only_choice_0_streamed() -> tuple[ModelResponseStream, ...]: + return (_reset_test_chunk(content="Hi", index=0), _reset_test_chunk(finish_reason="stop", index=0)) + + +def _two_text_choices_requested_but_only_choice_0_streamed() -> tuple[Completion, ...]: + return (_text_completion_chunk(0, "Hi"), _text_completion_chunk(0, finish_reason="stop")) + + +def _choice_1_finishing_beside_choice_0_content() -> tuple[ModelResponseStream, ...]: + return ( + _reset_test_chunk(content="Hel", index=1), + ModelResponseStream( + id="chatcmpl-reset-test", + created=1783458104, + model="stub-model", + object="chat.completion.chunk", + choices=[ + StreamingChoices(index=0, delta=Delta(content="Hi"), finish_reason=None), + StreamingChoices(index=1, delta=Delta(content=None), finish_reason="stop"), + ], + ), + _reset_test_chunk(finish_reason="stop", index=0), + ) + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +@pytest.mark.parametrize( + ("provider", "chunks", "content"), + ( + ("openai", _two_choices_requested_but_only_choice_0_streamed, "Hi"), + ("text-completion-openai", _two_text_choices_requested_but_only_choice_0_streamed, "Hi"), + ("openai", _choice_1_finishing_beside_choice_0_content, "HelHi"), + ), + ids=("provider_streams_fewer_choices", "text_provider_streams_fewer_choices", "choice_1_finishes_mid_chunk"), +) +async def test_dropped_before_done_after_every_streamed_choice_finished_ends_cleanly( + logging_obj: Logging, + sync_mode: bool, + provider: str, + chunks: Callable[[], Sequence[ModelResponseStream | Completion]], + content: str, +): + logging_obj.update_environment_variables(litellm_params={}, optional_params={"n": 2}) + stream: Final = _chunks_then_failure(chunks(), _httpx2_read_error()) + wrapper: Final = CustomStreamWrapper( + completion_stream=stream if sync_mode else _as_async(stream), + model="stub-model", + custom_llm_provider=provider, + logging_obj=logging_obj, + ) + + collected: Final = await _collect(wrapper, sync_mode) + + assert "".join(chunk.choices[0].delta.content or "" for chunk in collected if chunk.choices) == content + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +async def test_dropped_before_done_while_choice_0_streams_beside_a_finished_choice_1_raises( + logging_obj: Logging, sync_mode: bool +): + logging_obj.update_environment_variables(litellm_params={}, optional_params={"n": 2}) + wrapper: Final = _openai_stream_over( + _choice_1_finishing_beside_choice_0_content()[:2], _httpx2_read_error(), sync_mode, logging_obj + ) + + with pytest.raises(MidStreamFallbackError): + await _collect(wrapper, sync_mode) + + @pytest.mark.asyncio @_SYNC_AND_ASYNC @_SDK3_DROPPED_CONNECTIONS diff --git a/tests/unit/llms/azure/test_azure_common_utils.py b/tests/unit/llms/azure/test_azure_common_utils.py index 260f61ab838..5419ce48d21 100644 --- a/tests/unit/llms/azure/test_azure_common_utils.py +++ b/tests/unit/llms/azure/test_azure_common_utils.py @@ -2687,3 +2687,33 @@ async def test_azure_v1_deployment_keyed_only_by_the_openai_api_key_env_sends_th assert response.choices[0].message.content == "hi" assert route.calls.last.request.headers["authorization"] == "Bearer azure-key-in-openai-env" + + +@pytest.mark.parametrize("is_async", [False, True]) +@pytest.mark.asyncio +async def test_azure_v1_deployment_with_no_credential_is_an_authentication_error( + monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter, is_async: bool +) -> None: + for name in ( + "AZURE_API_KEY", + "AZURE_OPENAI_API_KEY", + "AZURE_AD_TOKEN", + "AZURE_OPENAI_AD_TOKEN", + "AZURE_CLIENT_ID", + "OPENAI_API_KEY", + ): + monkeypatch.delenv(name, raising=False) + monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True") + api_base: Final = f"https://nokey-{is_async}.openai.azure.com" + route: Final = respx_mock.post(f"{api_base}/openai/v1/embeddings").respond(200, json={}) + request: Final = { + "model": "azure/text-embedding-3-small", + "input": ["hi"], + "api_base": api_base, + "api_version": "v1", + } + + with pytest.raises(litellm.AuthenticationError): + await litellm.aembedding(**request) if is_async else litellm.embedding(**request) + + assert not route.called From 9963d4f990411dd33cfbfa59401386a85e9dcb21 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:04:45 -0700 Subject: [PATCH 19/23] fix(streaming): count a provider's dict done chunk when a stream drops before [DONE] An ollama/ /api/generate stream sends its done line as a generic streaming chunk dict, which the n>1 drop rule did not record as finishing choice 0, so a drop after that line raised instead of ending cleanly --- .../litellm_core_utils/streaming_handler.py | 27 +++++++------- .../test_streaming_handler.py | 35 +++++++++++++++++++ 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index fccea6c752d..2974758deb7 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -94,19 +94,18 @@ def _next_sync_or_exhausted(it: Iterator[object]) -> object: return _SYNC_ITER_EXHAUSTED -_CHOICE_CHUNK_TYPES: Final = (ChatCompletionChunk, ModelResponseStream, Completion) - - -def _choice_finish_reasons( - chunk: ChatCompletionChunk | ModelResponseStream | Completion | None, -) -> tuple[tuple[int, str | None], ...]: +def _choice_progress(chunk: object) -> tuple[tuple[int, bool], ...]: match chunk: case ChatCompletionChunk(choices=choices): - return tuple((choice.index, choice.finish_reason) for choice in choices or ()) + return tuple((choice.index, bool(choice.finish_reason)) for choice in choices or ()) case ModelResponseStream(choices=choices): - return tuple((choice.index, choice.finish_reason) for choice in choices or ()) + return tuple((choice.index, bool(choice.finish_reason)) for choice in choices or ()) case Completion(choices=choices): - return tuple((choice.index, choice.finish_reason) for choice in choices or ()) + return tuple((choice.index, bool(choice.finish_reason)) for choice in choices or ()) + case {"is_finished": bool() as is_finished, "index": int() as index}: + return ((index, is_finished),) + case {"is_finished": bool() as is_finished}: + return ((0, is_finished),) case _: return () @@ -1352,6 +1351,7 @@ class CustomStreamWrapper: completion_obj: dict[str, object], ) -> _ProviderChunkResult: response_obj: dict[str, Any] = {} + self._record_choice_progress(chunk) if ( isinstance(chunk, ModelResponseStream) and self.custom_llm_provider is not None @@ -1525,7 +1525,6 @@ class CustomStreamWrapper: self.received_finish_reason = response_obj["finish_reason"] elif self.custom_llm_provider == "text-completion-openai": response_obj = self.handle_openai_text_completion_chunk(chunk) - self._record_choice_progress(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) completion_obj["content"] = response_obj["text"] print_verbose(f"completion obj content: {completion_obj['content']}") if response_obj["is_finished"]: @@ -1565,7 +1564,6 @@ class CustomStreamWrapper: ) elif self.custom_llm_provider == "azure_text": response_obj = self.handle_azure_text_completion_chunk(chunk) - self._record_choice_progress(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) completion_obj["content"] = response_obj["text"] print_verbose(f"completion obj content: {completion_obj['content']}") if response_obj["is_finished"]: @@ -1609,7 +1607,6 @@ class CustomStreamWrapper: if isinstance(chunk, BaseModel) and hasattr(chunk, "model"): # for azure, we need to pass the model from the original chunk self.model = getattr(chunk, "model", self.model) - self._record_choice_progress(chunk if isinstance(chunk, _CHOICE_CHUNK_TYPES) else None) response_obj = self.handle_openai_chat_completion_chunk(chunk) if response_obj is None: return _ProviderChunkEarlyReturn(None) @@ -2279,11 +2276,11 @@ class CustomStreamWrapper: raise raise StopIteration from e - def _record_choice_progress(self, chunk: ChatCompletionChunk | ModelResponseStream | Completion | None) -> None: - progress: Final = _choice_finish_reasons(chunk) + def _record_choice_progress(self, chunk: object) -> None: + progress: Final = _choice_progress(chunk) self._streamed_choice_indices = self._streamed_choice_indices.union(index for index, _ in progress) self._finished_choice_indices = self._finished_choice_indices.union( - index for index, finish_reason in progress if finish_reason + index for index, finished in progress if finished ) def _every_choice_finished(self) -> bool: diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index 6ab59b414e6..e6388f4e289 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -32,6 +32,7 @@ from litellm.litellm_core_utils.streaming_handler import ( _ProviderChunkParsed, ) from litellm.llms.custom_httpx.http_handler import HTTPHandler +from litellm.llms.ollama.completion.transformation import OllamaTextCompletionResponseIterator from litellm.llms.openai.chat.gpt_transformation import OpenAIChatCompletionStreamingHandler from litellm.responses.mcp.chat_completions_handler import _add_mcp_metadata_to_response from litellm.types.utils import ( @@ -5416,6 +5417,40 @@ async def test_dropped_before_done_while_choice_0_streams_beside_a_finished_choi await _collect(wrapper, sync_mode) +def _ollama_generate_lines_through_done_then(failure: Exception) -> Iterator[str]: + yield json.dumps({"model": "smollm2:135m", "response": "Hi", "done": False}) + yield json.dumps( + { + "model": "smollm2:135m", + "response": "", + "done": True, + "done_reason": "stop", + "prompt_eval_count": 3, + "eval_count": 1, + } + ) + raise failure + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +async def test_ollama_generate_stream_dropped_after_its_done_line_ends_cleanly(logging_obj: Logging, sync_mode: bool): + lines: Final = _ollama_generate_lines_through_done_then(_httpx2_read_error()) + wrapper: Final = CustomStreamWrapper( + completion_stream=OllamaTextCompletionResponseIterator( + streaming_response=lines if sync_mode else _as_async(lines), sync_stream=sync_mode + ), + model="smollm2:135m", + custom_llm_provider="ollama", + logging_obj=logging_obj, + ) + + collected: Final = await _collect(wrapper, sync_mode) + + assert "".join(chunk.choices[0].delta.content or "" for chunk in collected if chunk.choices) == "Hi" + assert [chunk.choices[0].finish_reason for chunk in collected if chunk.choices[0].finish_reason] == ["stop"] + + @pytest.mark.asyncio @_SYNC_AND_ASYNC @_SDK3_DROPPED_CONNECTIONS From 8781b42c3574825509c8a8e1f317470b7222a27d Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:18:04 -0700 Subject: [PATCH 20/23] fix(streaming): treat every generic chunk as choice 0 in the stream drop rule A generic streaming chunk always lands in choices[0] for the client, and Cohere sets its index to the content block or tool call position, so reading that index as a choice made a drop after the finish raise --- .../litellm_core_utils/streaming_handler.py | 2 -- .../test_streaming_handler.py | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 2974758deb7..6e608062cb4 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -102,8 +102,6 @@ def _choice_progress(chunk: object) -> tuple[tuple[int, bool], ...]: return tuple((choice.index, bool(choice.finish_reason)) for choice in choices or ()) case Completion(choices=choices): return tuple((choice.index, bool(choice.finish_reason)) for choice in choices or ()) - case {"is_finished": bool() as is_finished, "index": int() as index}: - return ((index, is_finished),) case {"is_finished": bool() as is_finished}: return ((0, is_finished),) case _: diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index e6388f4e289..e7445b0873e 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -38,6 +38,7 @@ from litellm.responses.mcp.chat_completions_handler import _add_mcp_metadata_to_ from litellm.types.utils import ( CompletionTokensDetailsWrapper, Delta, + GenericStreamingChunk, ModelResponse, ModelResponseStream, PromptTokensDetailsWrapper, @@ -5451,6 +5452,31 @@ async def test_ollama_generate_stream_dropped_after_its_done_line_ends_cleanly(l assert [chunk.choices[0].finish_reason for chunk in collected if chunk.choices[0].finish_reason] == ["stop"] +def _generic_chunks_with_a_second_content_block_then(failure: Exception) -> Iterator[GenericStreamingChunk]: + yield GenericStreamingChunk(text="Hi", is_finished=False, finish_reason="", usage=None, index=0) + yield GenericStreamingChunk(text="!", is_finished=False, finish_reason="", usage=None, index=1) + yield GenericStreamingChunk(text="", is_finished=True, finish_reason="stop", usage=None, index=0) + raise failure + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +async def test_generic_chunk_index_is_not_a_choice_when_the_stream_drops_after_its_finish( + logging_obj: Logging, sync_mode: bool +): + chunks: Final = _generic_chunks_with_a_second_content_block_then(_httpx2_read_error()) + wrapper: Final = CustomStreamWrapper( + completion_stream=chunks if sync_mode else _as_async(chunks), + model="command-a-03-2025", + custom_llm_provider="cohere_chat", + logging_obj=logging_obj, + ) + + collected: Final = await _collect(wrapper, sync_mode) + + assert "".join(chunk.choices[0].delta.content or "" for chunk in collected if chunk.choices) == "Hi!" + + @pytest.mark.asyncio @_SYNC_AND_ASYNC @_SDK3_DROPPED_CONNECTIONS From 09857e30c0c8ec8af8a9f71c2c9c8f9d22d8ebda Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 30 Oct 2026 01:35:30 -0700 Subject: [PATCH 21/23] fix(streaming): let a finished generic chunk end every streamed choice An OpenAI-compatible provider that streams n=2 without per-choice finish_reason ends with `[DONE]`, which arrives as one finished generic chunk on choice 0. A drop after it errored because choice 1 never finished, while SDK 2 ended the stream cleanly. A finished generic chunk now ends every choice streamed so far --- .../litellm_core_utils/streaming_handler.py | 14 ++++++- .../test_streaming_handler.py | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 6e608062cb4..cc2343a62af 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -108,6 +108,14 @@ def _choice_progress(chunk: object) -> tuple[tuple[int, bool], ...]: return () +def _ends_every_choice(chunk: object) -> bool: + match chunk: + case {"is_finished": True}: + return True + case _: + return False + + def _stamp_usage_cost( usage_chunk: ModelResponse | TextCompletionResponse | ModelResponseStream, complete_response: ModelResponse | TextCompletionResponse | ModelResponseStream, @@ -2277,8 +2285,10 @@ class CustomStreamWrapper: def _record_choice_progress(self, chunk: object) -> None: progress: Final = _choice_progress(chunk) self._streamed_choice_indices = self._streamed_choice_indices.union(index for index, _ in progress) - self._finished_choice_indices = self._finished_choice_indices.union( - index for index, finished in progress if finished + self._finished_choice_indices = ( + self._streamed_choice_indices + if _ends_every_choice(chunk) + else self._finished_choice_indices.union(index for index, finished in progress if finished) ) def _every_choice_finished(self) -> bool: diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index e7445b0873e..f379104d97c 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -32,6 +32,7 @@ from litellm.litellm_core_utils.streaming_handler import ( _ProviderChunkParsed, ) from litellm.llms.custom_httpx.http_handler import HTTPHandler +from litellm.llms.fireworks_ai.chat.transformation import FireworksAIChatCompletionStreamingHandler from litellm.llms.ollama.completion.transformation import OllamaTextCompletionResponseIterator from litellm.llms.openai.chat.gpt_transformation import OpenAIChatCompletionStreamingHandler from litellm.responses.mcp.chat_completions_handler import _add_mcp_metadata_to_response @@ -5452,6 +5453,45 @@ async def test_ollama_generate_stream_dropped_after_its_done_line_ends_cleanly(l assert [chunk.choices[0].finish_reason for chunk in collected if chunk.choices[0].finish_reason] == ["stop"] + +def _fireworks_sse_line_for_choice(index: int, content: str) -> str: + chunk: Final = { + "id": "chatcmpl-two-choices", + "object": "chat.completion.chunk", + "created": 1, + "model": "accounts/fireworks/models/deepseek-v4p1-flash", + "choices": [{"index": index, "delta": {"role": "assistant", "content": content}, "finish_reason": None}], + } + return f"data: {json.dumps(chunk)}" + + +def _fireworks_n2_lines_without_finish_reasons_through_done_then(failure: Exception) -> Iterator[str]: + yield _fireworks_sse_line_for_choice(0, "Hi") + yield _fireworks_sse_line_for_choice(1, "Hel") + yield "data: [DONE]" + raise failure + + +@pytest.mark.asyncio +@_SYNC_AND_ASYNC +async def test_n2_stream_dropped_after_done_without_choice_finish_reasons_ends_cleanly( + logging_obj: Logging, sync_mode: bool +): + logging_obj.update_environment_variables(litellm_params={}, optional_params={"n": 2}) + lines: Final = _fireworks_n2_lines_without_finish_reasons_through_done_then(_httpx2_read_error()) + wrapper: Final = CustomStreamWrapper( + completion_stream=FireworksAIChatCompletionStreamingHandler( + streaming_response=lines if sync_mode else _as_async(lines), sync_stream=sync_mode + ), + model="accounts/fireworks/models/deepseek-v4p1-flash", + custom_llm_provider="fireworks_ai", + logging_obj=logging_obj, + ) + + collected: Final = await _collect(wrapper, sync_mode) + + assert "".join(chunk.choices[0].delta.content or "" for chunk in collected if chunk.choices) == "HiHel" + def _generic_chunks_with_a_second_content_block_then(failure: Exception) -> Iterator[GenericStreamingChunk]: yield GenericStreamingChunk(text="Hi", is_finished=False, finish_reason="", usage=None, index=0) yield GenericStreamingChunk(text="!", is_finished=False, finish_reason="", usage=None, index=1) From e949e94eb8a475ccb04b90681936ed95c5099c15 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:38:22 -0700 Subject: [PATCH 22/23] test(openai): run the httpx2 error cases only on openai SDK 3 --- tests/unit/llms/openai/test_openai.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/unit/llms/openai/test_openai.py b/tests/unit/llms/openai/test_openai.py index 2b2282dd8b8..8096dd0deb7 100644 --- a/tests/unit/llms/openai/test_openai.py +++ b/tests/unit/llms/openai/test_openai.py @@ -4,6 +4,7 @@ import importlib import json import os from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping +from importlib.metadata import version from types import ModuleType from typing import Final, Literal, Protocol from unittest.mock import MagicMock, Mock, patch @@ -472,6 +473,11 @@ async def test_mapped_error_carries_the_upstream_error_response( +_SDK_TAKES_HTTPX2: Final = pytest.mark.skipif( + int(version("openai").split(".")[0]) < 3, reason="openai SDK 2 accepts only httpx clients" +) + + def _sdk_default_transport_rejecting(_request: httpx2.Request) -> httpx2.Response: return httpx2.Response( 400, @@ -500,6 +506,7 @@ async def _acompletion_on_the_sdks_default_transport() -> None: await litellm.acompletion(messages=_MESSAGES, client=client, **_upstream("openai", "gpt-5.6")) +@_SDK_TAKES_HTTPX2 @pytest.mark.asyncio @pytest.mark.parametrize( "route", @@ -607,7 +614,13 @@ def _httpx2_client_answering(status_code: int, body: str) -> httpx2.Client: ) -@pytest.mark.parametrize("http_client_answering", (_httpx_client_answering, _httpx2_client_answering), ids=("httpx", "httpx2")) +@pytest.mark.parametrize( + "http_client_answering", + ( + pytest.param(_httpx_client_answering, id="httpx"), + pytest.param(_httpx2_client_answering, id="httpx2", marks=_SDK_TAKES_HTTPX2), + ), +) @pytest.mark.parametrize("provider", ("openai", "litellm_proxy")) @pytest.mark.parametrize( ("status_code", "mapped_error", "body"), From cea07b60649068cd4ef3d9750f74b8ace1f417f5 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:03:45 -0800 Subject: [PATCH 23/23] test(audio): assert dropped transcription streams by error type and cause on SDK 3 Main's transcription streaming tests matched the upstream transport message, which openai SDK 3 replaces with "Connection error." when a stream read fails. Assert the LiteLLM APIConnectionError and the dropped connection in its cause chain instead, which holds on SDK 2 and 3. --- .../audio_utils/test_transcription_streaming.py | 5 ++++- tests/unit/proxy/proxy_server/test_routes_audio.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/litellm_core_utils/audio_utils/test_transcription_streaming.py b/tests/unit/litellm_core_utils/audio_utils/test_transcription_streaming.py index 5296ea3b15a..5b0e061d3f5 100644 --- a/tests/unit/litellm_core_utils/audio_utils/test_transcription_streaming.py +++ b/tests/unit/litellm_core_utils/audio_utils/test_transcription_streaming.py @@ -19,6 +19,7 @@ from litellm.litellm_core_utils.audio_utils.transcription_streaming import ( litellm_exception_mapper, wrap_transcription_stream, ) +from litellm.llms.custom_httpx.transport_errors import TransportErrorKind, chained_transport_error_kind from litellm.types.utils import TranscriptionResponse _STARTED_AT: Final = datetime.datetime(2026, 10, 28, 12, 0, 0) # noqa: DTZ001 # callback timestamps use the legacy naive contract @@ -177,10 +178,12 @@ async def test_a_stream_the_upstream_drops_mid_transcript_raises_a_litellm_error model="openai/gpt-transcribe", file=("sample.wav", b"audio"), stream=True, client=upstream ) - with pytest.raises(litellm.APIConnectionError, match="peer closed connection"): + with pytest.raises(litellm.APIConnectionError) as raised: async for _ in stream: pass + assert chained_transport_error_kind(raised.value) is TransportErrorKind.DROPPED_CONNECTION + class _FailureHookCounter(CustomLogger): def __init__(self) -> None: diff --git a/tests/unit/proxy/proxy_server/test_routes_audio.py b/tests/unit/proxy/proxy_server/test_routes_audio.py index 9fa617c0dc3..4f25546f1bd 100644 --- a/tests/unit/proxy/proxy_server/test_routes_audio.py +++ b/tests/unit/proxy/proxy_server/test_routes_audio.py @@ -1037,7 +1037,7 @@ def test_streamed_transcription_error_mid_stream_ends_with_an_error_event_and_ru assert tuple(frame.get("delta") for frame in frames[:2]) == ("hello ", "world") assert len(frames) == 3 assert data_lines[-1] == "[DONE]" - assert "upstream connection dropped" in frames[2]["error"]["message"] + assert "APIConnectionError" in frames[2]["error"]["message"] assert "/srv/app/stt_stream.py" not in frames[2]["error"]["message"] assert "stt.internal.corp" not in frames[2]["error"]["message"] assert set(frames[2]["error"]) == {"message", "type", "param", "code"}