From 2acc93e4515fb91f3cb6abc3075dc5499f9fef2a Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Sat, 21 Feb 2026 15:28:18 -0800 Subject: [PATCH 1/7] BUMP --- poetry.lock | 100 +++++++++++++++++++++++++++---------------------- pyproject.toml | 4 +- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/poetry.lock b/poetry.lock index 48e5c33288..b6c3b83757 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. [[package]] name = "a2a-sdk" @@ -7,11 +7,11 @@ description = "A2A Python SDK" optional = false python-versions = ">=3.10" groups = ["main", "proxy-dev"] +markers = "python_version >= \"3.10\"" files = [ {file = "a2a_sdk-0.3.22-py3-none-any.whl", hash = "sha256:b98701135bb90b0ff85d35f31533b6b7a299bf810658c1c65f3814a6c15ea385"}, {file = "a2a_sdk-0.3.22.tar.gz", hash = "sha256:77a5694bfc4f26679c11b70c7f1062522206d430b34bc1215cfbb1eba67b7e7d"}, ] -markers = {main = "python_version >= \"3.10\" and extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] google-api-core = ">=1.26.0" @@ -385,7 +385,6 @@ files = [ {file = "azure_core-1.36.0-py3-none-any.whl", hash = "sha256:fee9923a3a753e94a259563429f3644aaf05c486d45b1215d098115102d91d3b"}, {file = "azure_core-1.36.0.tar.gz", hash = "sha256:22e5605e6d0bf1d229726af56d9e92bc37b6e726b141a18be0b4d424131741b7"}, ] -markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] requests = ">=2.21.0" @@ -406,7 +405,6 @@ files = [ {file = "azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651"}, {file = "azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456"}, ] -markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] azure-core = ">=1.31.0" @@ -600,7 +598,7 @@ files = [ {file = "cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace"}, {file = "cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} [[package]] name = "certifi" @@ -707,7 +705,7 @@ files = [ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] -markers = {main = "(platform_python_implementation != \"PyPy\" or extra == \"proxy\") and (python_version >= \"3.10\" or extra == \"proxy\" or extra == \"extra-proxy\") and (extra == \"proxy\" or extra == \"extra-proxy\" or extra == \"mlflow\")", dev = "platform_python_implementation != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\""} +markers = {main = "platform_python_implementation != \"PyPy\" or extra == \"proxy\"", dev = "platform_python_implementation != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} @@ -1057,7 +1055,6 @@ files = [ {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"proxy\" or extra == \"extra-proxy\" or extra == \"mlflow\") or extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} @@ -1840,11 +1837,11 @@ description = "Google API client core library" optional = false python-versions = ">=3.7" groups = ["main", "proxy-dev"] +markers = "python_version >= \"3.14\"" files = [ {file = "google_api_core-2.25.2-py3-none-any.whl", hash = "sha256:e9a8f62d363dc8424a8497f4c2a47d6bcda6c16514c935629c257ab5d10210e7"}, {file = "google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300"}, ] -markers = {main = "python_version >= \"3.14\" and (extra == \"extra-proxy\" or extra == \"google\")", proxy-dev = "python_version >= \"3.14\""} [package.dependencies] google-auth = ">=2.14.1,<3.0.0" @@ -1872,7 +1869,7 @@ files = [ {file = "google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c"}, {file = "google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8"}, ] -markers = {main = "python_version < \"3.14\" and (extra == \"extra-proxy\" or extra == \"google\")", proxy-dev = "python_version >= \"3.10\" and python_version < \"3.14\""} +markers = {main = "(python_version >= \"3.10\" or extra == \"google\" or extra == \"extra-proxy\") and python_version < \"3.14\"", proxy-dev = "python_version >= \"3.10\" and python_version < \"3.14\""} [package.dependencies] google-auth = ">=2.14.1,<3.0.0" @@ -1909,7 +1906,7 @@ files = [ {file = "google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16"}, {file = "google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] cachetools = ">=2.0.0,<7.0" @@ -2081,11 +2078,11 @@ files = [ ] [package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0.dev0", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" -grpc-google-iam-v1 = ">=0.12.4,<1.0.0.dev0" -proto-plus = ">=1.22.3,<2.0.0.dev0" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" +grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" +proto-plus = ">=1.22.3,<2.0.0dev" +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" [[package]] name = "google-cloud-resource-manager" @@ -2267,7 +2264,7 @@ files = [ {file = "googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038"}, {file = "googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\") or extra == \"google\" or extra == \"extra-proxy\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\""} [package.dependencies] grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""} @@ -2676,11 +2673,11 @@ description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = false python-versions = ">=3.9" groups = ["main", "proxy-dev"] +markers = "python_version >= \"3.10\"" files = [ {file = "httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc"}, {file = "httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"proxy\" or extra == \"extra-proxy\")", proxy-dev = "python_version >= \"3.10\""} [[package]] name = "huey" @@ -3045,7 +3042,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.3.6" +jsonschema-specifications = ">=2023.03.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -3716,7 +3713,6 @@ files = [ {file = "msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1"}, {file = "msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f"}, ] -markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] cryptography = ">=2.5,<49" @@ -3737,7 +3733,6 @@ files = [ {file = "msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca"}, {file = "msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4"}, ] -markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] msal = ">=1.29,<2" @@ -3988,7 +3983,6 @@ files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] -markers = {main = "extra == \"extra-proxy\""} [[package]] name = "numpy" @@ -4111,7 +4105,7 @@ files = [ {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, ] -markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""} +markers = {main = "python_version >= \"3.10\""} [package.dependencies] importlib-metadata = ">=6.0,<8.8.0" @@ -4226,7 +4220,7 @@ files = [ {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, ] -markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""} +markers = {main = "python_version >= \"3.10\""} [package.dependencies] opentelemetry-api = "1.39.1" @@ -4244,7 +4238,7 @@ files = [ {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, ] -markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""} +markers = {main = "python_version >= \"3.10\""} [package.dependencies] opentelemetry-api = "1.39.1" @@ -4728,7 +4722,6 @@ files = [ {file = "prisma-0.11.0-py3-none-any.whl", hash = "sha256:22bb869e59a2968b99f3483bb417717273ffbc569fd1e9ceed95e5614cbaf53a"}, {file = "prisma-0.11.0.tar.gz", hash = "sha256:3f2f2fd2361e1ec5ff655f2a04c7860c2f2a5bc4c91f78ca9c5c6349735bf693"}, ] -markers = {main = "extra == \"extra-proxy\""} [package.dependencies] click = ">=7.1.2" @@ -4902,7 +4895,7 @@ files = [ {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] protobuf = ">=3.19.0,<7.0.0" @@ -4930,7 +4923,7 @@ files = [ {file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"}, {file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\""} [[package]] name = "psutil" @@ -5090,7 +5083,7 @@ files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} [[package]] name = "pyasn1-modules" @@ -5103,7 +5096,7 @@ files = [ {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] pyasn1 = ">=0.6.1,<0.7.0" @@ -5131,7 +5124,7 @@ files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, ] -markers = {main = "implementation_name != \"PyPy\" and (platform_python_implementation != \"PyPy\" or extra == \"proxy\") and (python_version >= \"3.10\" or extra == \"proxy\" or extra == \"extra-proxy\") and (extra == \"proxy\" or extra == \"extra-proxy\" or extra == \"mlflow\")", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} +markers = {main = "implementation_name != \"PyPy\" and (platform_python_implementation != \"PyPy\" or extra == \"proxy\")", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} [[package]] name = "pydantic" @@ -5354,7 +5347,6 @@ files = [ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, ] -markers = {main = "(python_version <= \"3.13\" or extra == \"proxy\" or extra == \"extra-proxy\") and (extra == \"extra-proxy\" or extra == \"proxy\")"} [package.dependencies] cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} @@ -6284,7 +6276,7 @@ files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] pyasn1 = ">=0.1.3" @@ -6330,10 +6322,10 @@ files = [ ] [package.dependencies] -botocore = ">=1.37.4,<2.0a0" +botocore = ">=1.37.4,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] [[package]] name = "scikit-learn" @@ -6486,9 +6478,9 @@ tornado = ">=6.4.2,<7" urllib3 = ">=1.26,<3" [package.extras] -all = ["boto3 (>=1.34.98,<2)", "botocore (>=1.34.110,<2)", "cohere (>=5.9.4,<6.0)", "dagger-io (>=0.1.1) ; python_version >= \"3.11\"", "fastembed (>=0.3.0,<0.4) ; python_version < \"3.13\"", "google-cloud-aiplatform (>=1.45.0,<2)", "ipykernel (>=6.25.0,<7)", "llama-cpp-python (>=0.2.28,<0.2.86) ; python_version < \"3.13\"", "mistralai (>=0.0.12,<0.1.0)", "mypy (>=1.7.1,<2)", "ollama (>=0.1.7)", "pillow (>=10.2.0,<11.0.0) ; python_version < \"3.13\"", "pinecone[asyncio] (>=7.0.0,<8.0.0)", "psycopg[binary] (>=3.1.0,<4)", "pytest (>=8.2,<9.0)", "pytest-asyncio (>=0.24.0,<0.25)", "pytest-cov (>=4.1.0,<5)", "pytest-mock (>=3.12.0,<4)", "pytest-timeout", "pytest-xdist (>=3.5.0,<4)", "python-dotenv (>=1.0.0,<2)", "qdrant-client (>=1.11.1,<2)", "requests-mock (>=1.12.1,<2)", "ruff (>=0.11.2,<0.12)", "sentence-transformers (>=5.0.0) ; python_version < \"3.13\"", "tokenizers (>=0.19) ; python_version < \"3.13\"", "torch (>=2.6.0) ; python_version < \"3.13\"", "torchvision (>=0.17.0) ; python_version < \"3.13\"", "transformers (>=4.36.2) ; python_version < \"3.13\"", "types-pyyaml (>=6.0.12.12,<7)", "types-requests (>=2.31.0,<3)"] +all = ["boto3 (>=1.34.98,<2)", "botocore (>=1.34.110,<2)", "cohere (>=5.9.4,<6.00)", "dagger-io (>=0.1.1) ; python_version >= \"3.11\"", "fastembed (>=0.3.0,<0.4) ; python_version < \"3.13\"", "google-cloud-aiplatform (>=1.45.0,<2)", "ipykernel (>=6.25.0,<7)", "llama-cpp-python (>=0.2.28,<0.2.86) ; python_version < \"3.13\"", "mistralai (>=0.0.12,<0.1.0)", "mypy (>=1.7.1,<2)", "ollama (>=0.1.7)", "pillow (>=10.2.0,<11.0.0) ; python_version < \"3.13\"", "pinecone[asyncio] (>=7.0.0,<8.0.0)", "psycopg[binary] (>=3.1.0,<4)", "pytest (>=8.2,<9.0)", "pytest-asyncio (>=0.24.0,<0.25)", "pytest-cov (>=4.1.0,<5)", "pytest-mock (>=3.12.0,<4)", "pytest-timeout", "pytest-xdist (>=3.5.0,<4)", "python-dotenv (>=1.0.0,<2)", "qdrant-client (>=1.11.1,<2)", "requests-mock (>=1.12.1,<2)", "ruff (>=0.11.2,<0.12)", "sentence-transformers (>=5.0.0) ; python_version < \"3.13\"", "tokenizers (>=0.19) ; python_version < \"3.13\"", "torch (>=2.6.0) ; python_version < \"3.13\"", "torchvision (>=0.17.0) ; python_version < \"3.13\"", "transformers (>=4.36.2) ; python_version < \"3.13\"", "types-pyyaml (>=6.0.12.12,<7)", "types-requests (>=2.31.0,<3)"] bedrock = ["boto3 (>=1.34.98,<2)", "botocore (>=1.34.110,<2)"] -cohere = ["cohere (>=5.9.4,<6.0)"] +cohere = ["cohere (>=5.9.4,<6.00)"] dev = ["dagger-io (>=0.1.1) ; python_version >= \"3.11\"", "ipykernel (>=6.25.0,<7)", "mypy (>=1.7.1,<2)", "pytest (>=8.2,<9.0)", "pytest-asyncio (>=0.24.0,<0.25)", "pytest-cov (>=4.1.0,<5)", "pytest-mock (>=3.12.0,<4)", "pytest-timeout", "pytest-xdist (>=3.5.0,<4)", "python-dotenv (>=1.0.0,<2)", "requests-mock (>=1.12.1,<2)", "ruff (>=0.11.2,<0.12)", "types-pyyaml (>=6.0.12.12,<7)", "types-requests (>=2.31.0,<3)"] docs = ["pydoc-markdown (>=4.8.2) ; python_version < \"3.12\""] fastembed = ["fastembed (>=0.3.0,<0.4) ; python_version < \"3.13\""] @@ -7216,7 +7208,6 @@ files = [ {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] -markers = {main = "extra == \"extra-proxy\""} [[package]] name = "tornado" @@ -7496,15 +7487,15 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.31.1" +version = "0.39.0" description = "The lightning-fast ASGI server." optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\"" +markers = "python_version == \"3.9\" and extra == \"proxy\"" files = [ - {file = "uvicorn-0.31.1-py3-none-any.whl", hash = "sha256:adc42d9cac80cf3e51af97c1851648066841e7cfb6993a4ca8de29ac1548ed41"}, - {file = "uvicorn-0.31.1.tar.gz", hash = "sha256:f5167919867b161b7bcaf32646c6a94cdbd4c3aa2eb5c17d36bb9aa5cfd8c493"}, + {file = "uvicorn-0.39.0-py3-none-any.whl", hash = "sha256:7beec21bd2693562b386285b188a7963b06853c0d006302b3e4cfed950c9929a"}, + {file = "uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302"}, ] [package.dependencies] @@ -7513,7 +7504,28 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvicorn" +version = "0.41.0" +description = "The lightning-fast ASGI server." +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\")" +files = [ + {file = "uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187"}, + {file = "uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=10.4)"] [[package]] name = "uvloop" @@ -7968,4 +7980,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "3dd495ee4e23d7cb750525c4f364ee96a4ef34fa9d9d5c4ed07b5432c0925d48" +content-hash = "97936ece74659668c195c2c05ff36c6edd24c5c18b52ab3a5bb8a3cd2f329e5d" diff --git a/pyproject.toml b/pyproject.toml index 469a892fbc..9a8ace9487 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm" -version = "1.81.13" +version = "1.81.14" description = "Library to easily interface with LLM API providers" authors = ["BerriAI"] license = "MIT" @@ -183,7 +183,7 @@ requires = ["poetry-core", "wheel"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "1.81.13" +version = "1.81.14" version_files = [ "pyproject.toml:^version" ] From 9fc6fd647c1ef3a253825d232d8610cad8540ceb Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Sat, 21 Feb 2026 15:32:47 -0800 Subject: [PATCH 2/7] Agent Builder - support new experimental agent builder, to ensure agents pass compliance checks (#21817) * fix: feat: add litellm_system_prompt support * feat: support new 'litellm_agent' model provider * feat: ui/ - new agent builder ui * fix(anthropic/chat/transformation.py): normalize max_tokens if decimal * feat(agentbuilderview.tsx): run compliance datasets against litellm agent --- litellm/__init__.py | 1 + .../integrations/litellm_agent/__init__.py | 5 + .../litellm_agent_model_resolver.py | 79 ++++ .../custom_logger_registry.py | 4 +- litellm/litellm_core_utils/litellm_logging.py | 18 + .../prompt_templates/common_utils.py | 77 +++- litellm/llms/anthropic/chat/transformation.py | 111 +++-- litellm/main.py | 60 ++- litellm/proxy/_new_secret_config.yaml | 4 + litellm/router.py | 27 +- litellm/types/utils.py | 3 +- .../test_litellm_agent_model_resolver.py | 81 ++++ ...ore_utils_prompt_templates_common_utils.py | 55 +++ .../test_anthropic_chat_transformation.py | 20 + .../src/app/(dashboard)/playground/page.tsx | 13 + .../playground/chat_ui/AgentBuilderView.tsx | 379 ++++++++++++++++++ .../components/playground/chat_ui/ChatUI.tsx | 71 +++- .../playground/complianceUI/ComplianceUI.tsx | 266 ++++++++---- .../playground/llm_calls/fetch_agents.tsx | 53 ++- 19 files changed, 1146 insertions(+), 181 deletions(-) create mode 100644 litellm/integrations/litellm_agent/__init__.py create mode 100644 litellm/integrations/litellm_agent/litellm_agent_model_resolver.py create mode 100644 tests/test_litellm/integrations/litellm_agent/test_litellm_agent_model_resolver.py create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx diff --git a/litellm/__init__.py b/litellm/__init__.py index a994db85b1..97f36a9b00 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -98,6 +98,7 @@ _custom_logger_compatible_callbacks_literal = Literal[ "openmeter", "logfire", "literalai", + "litellm_agent", "dynamic_rate_limiter", "dynamic_rate_limiter_v3", "langsmith", diff --git a/litellm/integrations/litellm_agent/__init__.py b/litellm/integrations/litellm_agent/__init__.py new file mode 100644 index 0000000000..f09434080e --- /dev/null +++ b/litellm/integrations/litellm_agent/__init__.py @@ -0,0 +1,5 @@ +"""LiteLLM Agent integration - model name resolver for litellm_agent/ prefix.""" + +from .litellm_agent_model_resolver import LiteLLMAgentModelResolver + +__all__ = ["LiteLLMAgentModelResolver"] diff --git a/litellm/integrations/litellm_agent/litellm_agent_model_resolver.py b/litellm/integrations/litellm_agent/litellm_agent_model_resolver.py new file mode 100644 index 0000000000..85d209da5b --- /dev/null +++ b/litellm/integrations/litellm_agent/litellm_agent_model_resolver.py @@ -0,0 +1,79 @@ +""" +Hook for LiteLLM that strips the litellm_agent/ prefix from model names. + +When model is litellm_agent/gpt-3.5-turbo, this hook replaces it with gpt-3.5-turbo +before the completion call, similar to langfuse/model resolution. +""" + +from typing import Dict, List, Optional, Tuple + +from litellm.integrations.custom_logger import CustomLogger +from litellm.types.llms.openai import AllMessageValues +from litellm.types.prompts.init_prompts import PromptSpec +from litellm.types.utils import StandardCallbackDynamicParams + +LITELLM_AGENT_PREFIX = "litellm_agent/" + + +class LiteLLMAgentModelResolver(CustomLogger): + """ + CustomLogger that strips litellm_agent/ prefix from model names. + + Enables model configs like litellm_agent/gpt-3.5-turbo to resolve to gpt-3.5-turbo. + """ + + def get_chat_completion_prompt( + self, + model: str, + messages: List[AllMessageValues], + non_default_params: dict, + prompt_id: Optional[str], + prompt_variables: Optional[dict], + dynamic_callback_params: StandardCallbackDynamicParams, + prompt_spec: Optional[PromptSpec] = None, + prompt_label: Optional[str] = None, + prompt_version: Optional[int] = None, + ignore_prompt_manager_model: Optional[bool] = False, + ignore_prompt_manager_optional_params: Optional[bool] = False, + ) -> Tuple[str, List[AllMessageValues], dict]: + """ + Strip litellm_agent/ prefix from model name. + + Returns: + (resolved_model, messages, non_default_params) + """ + if ignore_prompt_manager_model: + return model, messages, non_default_params + resolved_model = model.replace(LITELLM_AGENT_PREFIX, "", 1) + return resolved_model, messages, non_default_params + + async def async_get_chat_completion_prompt( + self, + model: str, + messages: List[AllMessageValues], + non_default_params: dict, + prompt_id: Optional[str], + prompt_variables: Optional[dict], + dynamic_callback_params: StandardCallbackDynamicParams, + litellm_logging_obj: object, + prompt_spec: Optional[PromptSpec] = None, + tools: Optional[List[Dict]] = None, + prompt_label: Optional[str] = None, + prompt_version: Optional[int] = None, + ignore_prompt_manager_model: Optional[bool] = False, + ignore_prompt_manager_optional_params: Optional[bool] = False, + ) -> Tuple[str, List[AllMessageValues], dict]: + """Async delegate to get_chat_completion_prompt.""" + return self.get_chat_completion_prompt( + model=model, + messages=messages, + non_default_params=non_default_params, + prompt_id=prompt_id, + prompt_variables=prompt_variables, + dynamic_callback_params=dynamic_callback_params, + prompt_spec=prompt_spec, + prompt_label=prompt_label, + prompt_version=prompt_version, + ignore_prompt_manager_model=ignore_prompt_manager_model, + ignore_prompt_manager_optional_params=ignore_prompt_manager_optional_params, + ) diff --git a/litellm/litellm_core_utils/custom_logger_registry.py b/litellm/litellm_core_utils/custom_logger_registry.py index a3c25ab65e..fc73701ea9 100644 --- a/litellm/litellm_core_utils/custom_logger_registry.py +++ b/litellm/litellm_core_utils/custom_logger_registry.py @@ -18,11 +18,11 @@ from litellm.integrations.azure_storage.azure_storage import AzureBlobStorageLog from litellm.integrations.bitbucket import BitBucketPromptManager from litellm.integrations.braintrust_logging import BraintrustLogger from litellm.integrations.cloudzero.cloudzero import CloudZeroLogger -from litellm.integrations.focus.focus_logger import FocusLogger from litellm.integrations.datadog.datadog import DataDogLogger from litellm.integrations.datadog.datadog_llm_obs import DataDogLLMObsLogger from litellm.integrations.deepeval import DeepEvalLogger from litellm.integrations.dotprompt import DotpromptManager +from litellm.integrations.focus.focus_logger import FocusLogger from litellm.integrations.galileo import GalileoObserve from litellm.integrations.gcs_bucket.gcs_bucket import GCSBucketLogger from litellm.integrations.gcs_pubsub.pub_sub import GcsPubSubLogger @@ -33,6 +33,7 @@ from litellm.integrations.langfuse.langfuse_prompt_management import ( LangfusePromptManagement, ) from litellm.integrations.langsmith import LangsmithLogger +from litellm.integrations.litellm_agent import LiteLLMAgentModelResolver from litellm.integrations.literal_ai import LiteralAILogger from litellm.integrations.mlflow import MlflowLogger from litellm.integrations.openmeter import OpenMeterLogger @@ -61,6 +62,7 @@ class CustomLoggerRegistry: "galileo": GalileoObserve, "langsmith": LangsmithLogger, "literalai": LiteralAILogger, + "litellm_agent": LiteLLMAgentModelResolver, "prometheus": PrometheusLogger, "datadog": DataDogLogger, "datadog_llm_observability": DataDogLLMObsLogger, diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 56a22af3c2..5a67b89816 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -147,6 +147,7 @@ from ..integrations.langfuse.langfuse import LangFuseLogger from ..integrations.langfuse.langfuse_handler import LangFuseHandler from ..integrations.langfuse.langfuse_prompt_management import LangfusePromptManagement from ..integrations.langsmith import LangsmithLogger +from ..integrations.litellm_agent import LiteLLMAgentModelResolver from ..integrations.literal_ai import LiteralAILogger from ..integrations.logfire_logger import LogfireLevel, LogfireLogger from ..integrations.lunary import LunaryLogger @@ -587,6 +588,11 @@ class Logging(LiteLLMLoggingBaseClass): if prompt_id: return True + # Check if model uses litellm_agent prefix (model replacement without prompt_id) + model = non_default_params.get("model", "") + if isinstance(model, str) and model.startswith("litellm_agent/"): + return True + if self._should_run_prompt_management_hooks_without_prompt_id( non_default_params=non_default_params, tools=tools, @@ -3629,6 +3635,14 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 _literalai_logger = LiteralAILogger() _in_memory_loggers.append(_literalai_logger) return _literalai_logger # type: ignore + elif logging_integration == "litellm_agent": + for callback in _in_memory_loggers: + if isinstance(callback, LiteLLMAgentModelResolver): + return callback # type: ignore + + _litellm_agent_resolver = LiteLLMAgentModelResolver() + _in_memory_loggers.append(_litellm_agent_resolver) + return _litellm_agent_resolver # type: ignore elif logging_integration == "prometheus": PrometheusLogger = _get_cached_prometheus_logger() @@ -4183,6 +4197,10 @@ def get_custom_logger_compatible_class( # noqa: PLR0915 for callback in _in_memory_loggers: if isinstance(callback, LiteralAILogger): return callback + elif logging_integration == "litellm_agent": + for callback in _in_memory_loggers: + if isinstance(callback, LiteLLMAgentModelResolver): + return callback elif logging_integration == "prometheus": PrometheusLogger = _get_cached_prometheus_logger() for callback in _in_memory_loggers: diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index cdddee4e54..b1d571343c 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -452,7 +452,7 @@ def update_responses_input_with_model_file_ids( For managed files (unified file IDs), uses model_file_id_mapping if provided, otherwise decodes the base64-encoded unified file ID and extracts the llm_output_file_id directly. - + Args: input: The responses API input parameter model_id: The model ID to use for looking up provider-specific file IDs @@ -488,9 +488,13 @@ def update_responses_input_with_model_file_ids( file_id = content_item.get("file_id") if file_id: provider_file_id = file_id # Default to original - + # Check if we have a mapping for this file ID - if model_file_id_mapping and model_id and file_id in model_file_id_mapping: + if ( + model_file_id_mapping + and model_id + and file_id in model_file_id_mapping + ): # Use the model-specific file ID from mapping provider_file_id = ( model_file_id_mapping.get(file_id, {}).get(model_id) @@ -501,15 +505,19 @@ def update_responses_input_with_model_file_ids( updated_content.append(updated_content_item) else: # Check if this is a base64-encoded unified file ID without mapping - is_unified_file_id = _is_base64_encoded_unified_file_id(file_id) + is_unified_file_id = _is_base64_encoded_unified_file_id( + file_id + ) if is_unified_file_id: # Fallback: decode unified file ID - unified_file_id = convert_b64_uid_to_unified_uid(file_id) + unified_file_id = convert_b64_uid_to_unified_uid( + file_id + ) if "llm_output_file_id," in unified_file_id: provider_file_id = unified_file_id.split( "llm_output_file_id," )[1].split(";")[0] - + updated_content_item = content_item.copy() updated_content_item["file_id"] = provider_file_id updated_content.append(updated_content_item) @@ -534,9 +542,9 @@ def update_responses_tools_with_model_file_ids( ) -> Optional[List[Dict[str, Any]]]: """ Updates responses API tools with provider-specific file IDs. - + Handles code_interpreter tools with container.file_ids. - + Args: tools: The responses API tools parameter model_id: The model ID to use for looking up provider-specific file IDs @@ -545,18 +553,18 @@ def update_responses_tools_with_model_file_ids( """ if not tools or not isinstance(tools, list): return tools - + if not model_file_id_mapping or not model_id: return tools - + updated_tools = [] for tool in tools: if not isinstance(tool, dict): updated_tools.append(tool) continue - + updated_tool = tool.copy() - + # Handle code_interpreter with container file_ids if tool.get("type") == "code_interpreter": container = tool.get("container") @@ -578,14 +586,14 @@ def update_responses_tools_with_model_file_ids( updated_file_ids.append(file_id) else: updated_file_ids.append(file_id) - + # Update the tool with new file IDs updated_container = container.copy() updated_container["file_ids"] = updated_file_ids updated_tool["container"] = updated_container - + updated_tools.append(updated_tool) - + return updated_tools @@ -1104,6 +1112,45 @@ def set_last_user_message( return messages +def add_system_prompt_to_messages( + messages: List[AllMessageValues], + system_prompt: str, + merge_with_first_system: bool = False, +) -> List[AllMessageValues]: + """ + Add a system prompt to the messages list. + + Args: + messages: List of chat completion messages + system_prompt: The system prompt content to add. If empty or None, returns messages unchanged. + merge_with_first_system: If True and the first message is already a system message, + prepends the new prompt to that message's content. If False, adds a new system + message at the beginning. + + Returns: + New list of messages with the system prompt added + """ + if not system_prompt: + return list(messages) + + if merge_with_first_system and messages and messages[0].get("role") == "system": + first = dict(messages[0]) + existing_content = first.get("content", "") + if isinstance(existing_content, str): + merged_content = f"{system_prompt.strip()}\n\n{existing_content}" + elif isinstance(existing_content, list): + merged_content = [{"type": "text", "text": system_prompt.strip()}] + list( + existing_content + ) + else: + merged_content = [{"type": "text", "text": system_prompt.strip()}] + first["content"] = merged_content + return [cast(AllMessageValues, first)] + list(messages[1:]) + + system_message: AllMessageValues = {"role": "system", "content": system_prompt} + return [system_message, *messages] + + def convert_prefix_message_to_non_prefix_messages( messages: List[AllMessageValues], ) -> List[AllMessageValues]: diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 0f613ceb50..fe57046f80 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -46,6 +46,7 @@ from litellm.types.llms.openai import ( ChatCompletionToolCallChunk, ChatCompletionToolCallFunctionChunk, ChatCompletionToolParam, + OpenAIChatCompletionFinishReason, OpenAIMcpServerTool, OpenAIWebSearchOptions, ) @@ -54,10 +55,7 @@ from litellm.types.utils import ( CompletionTokensDetailsWrapper, ) from litellm.types.utils import Message as LitellmMessage -from litellm.types.utils import ( - PromptTokensDetailsWrapper, - ServerToolUse, -) +from litellm.types.utils import PromptTokensDetailsWrapper, ServerToolUse from litellm.utils import ( ModelResponse, Usage, @@ -251,10 +249,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): # All numeric/string/array constraints not supported by Anthropic unsupported_fields = { - "maxItems", "minItems", # array constraints - "minimum", "maximum", # numeric constraints - "exclusiveMinimum", "exclusiveMaximum", # numeric constraints - "minLength", "maxLength", # string constraints + "maxItems", + "minItems", # array constraints + "minimum", + "maximum", # numeric constraints + "exclusiveMinimum", + "exclusiveMaximum", # numeric constraints + "minLength", + "maxLength", # string constraints } # Build description additions from removed constraints @@ -844,7 +846,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): @staticmethod def map_openai_context_management_to_anthropic( - context_management: Union[List[Dict[str, Any]], Dict[str, Any]] + context_management: Union[List[Dict[str, Any]], Dict[str, Any]], ) -> Optional[Dict[str, Any]]: """ OpenAI format: [{"type": "compaction", "compact_threshold": 200000}] @@ -876,19 +878,22 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): entry_type = entry.get("type") if entry_type == "compaction": - anthropic_edit: Dict[str, Any] = { - "type": "compact_20260112" - } + anthropic_edit: Dict[str, Any] = {"type": "compact_20260112"} compact_threshold = entry.get("compact_threshold") # Rewrite to 'trigger' with correct nesting if threshold exists - if compact_threshold is not None and isinstance(compact_threshold, (int, float)): + if compact_threshold is not None and isinstance( + compact_threshold, (int, float) + ): anthropic_edit["trigger"] = { "type": "input_tokens", - "value": int(compact_threshold) + "value": int(compact_threshold), } # Map any other keys by passthrough except handled ones for k in entry: - if k not in {"type", "compact_threshold"}: # only passthrough other keys + if k not in { + "type", + "compact_threshold", + }: # only passthrough other keys anthropic_edit[k] = entry[k] anthropic_edits.append(anthropic_edit) @@ -911,10 +916,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): for param, value in non_default_params.items(): if param == "max_tokens": - optional_params["max_tokens"] = value - if param == "max_completion_tokens": - optional_params["max_tokens"] = value - if param == "tools": + optional_params["max_tokens"] = ( + value if isinstance(value, int) else max(1, int(round(value))) + ) + elif param == "max_completion_tokens": + optional_params["max_tokens"] = ( + value if isinstance(value, int) else max(1, int(round(value))) + ) + elif param == "tools": # check if optional params already has tools anthropic_tools, mcp_servers = self._map_tools(value) optional_params = self._add_tools_to_optional_params( @@ -922,7 +931,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) if mcp_servers: optional_params["mcp_servers"] = mcp_servers - if param == "tool_choice" or param == "parallel_tool_calls": + elif param == "tool_choice" or param == "parallel_tool_calls": _tool_choice: Optional[AnthropicMessagesToolChoice] = ( self._map_tool_choice( tool_choice=non_default_params.get("tool_choice"), @@ -932,17 +941,19 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if _tool_choice is not None: optional_params["tool_choice"] = _tool_choice - if param == "stream" and value is True: + elif param == "stream" and value is True: optional_params["stream"] = value - if param == "stop" and (isinstance(value, str) or isinstance(value, list)): + elif param == "stop" and ( + isinstance(value, str) or isinstance(value, list) + ): _value = self._map_stop_sequences(value) if _value is not None: optional_params["stop_sequences"] = _value - if param == "temperature": + elif param == "temperature": optional_params["temperature"] = value - if param == "top_p": + elif param == "top_p": optional_params["top_p"] = value - if param == "response_format" and isinstance(value, dict): + elif param == "response_format" and isinstance(value, dict): if any( substring in model for substring in { @@ -982,14 +993,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): optional_params=optional_params, tools=[_tool] ) optional_params["json_mode"] = True - if ( + elif ( param == "user" and value is not None and isinstance(value, str) and _valid_user_id(value) # anthropic fails on emails ): optional_params["metadata"] = {"user_id": value} - if param == "thinking": + elif param == "thinking": optional_params["thinking"] = value elif param == "reasoning_effort" and isinstance(value, str): optional_params["thinking"] = AnthropicConfig._map_reasoning_effort( @@ -1007,9 +1018,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): elif param == "context_management": # Supports both OpenAI list format and Anthropic dict format if isinstance(value, (list, dict)): - anthropic_context_management = self.map_openai_context_management_to_anthropic(value) + anthropic_context_management = ( + self.map_openai_context_management_to_anthropic(value) + ) if anthropic_context_management is not None: - optional_params["context_management"] = anthropic_context_management + optional_params["context_management"] = ( + anthropic_context_management + ) elif param == "speed" and isinstance(value, str): # Pass through Anthropic-specific speed parameter for fast mode optional_params["speed"] = value @@ -1071,7 +1086,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if not system_message_block["content"]: continue # Skip system messages containing x-anthropic-billing-header metadata - if system_message_block["content"].startswith("x-anthropic-billing-header:"): + if system_message_block["content"].startswith( + "x-anthropic-billing-header:" + ): continue anthropic_system_message_content = AnthropicSystemMessageContent( type="text", @@ -1091,7 +1108,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if _content.get("type") == "text" and not text_value: continue # Skip system messages containing x-anthropic-billing-header metadata - if _content.get("type") == "text" and text_value and text_value.startswith("x-anthropic-billing-header:"): + if ( + _content.get("type") == "text" + and text_value + and text_value.startswith("x-anthropic-billing-header:") + ): continue anthropic_system_message_content = ( AnthropicSystemMessageContent( @@ -1201,7 +1222,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): # Add context management header if any other edits/entries exist if has_other: self._ensure_beta_header( - headers, ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value + headers, + ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value, ) def update_headers_with_optional_anthropic_beta( @@ -1227,7 +1249,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ANTHROPIC_HOSTED_TOOLS.MEMORY.value ): self._ensure_beta_header( - headers, ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value + headers, + ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value, ) if optional_params.get("context_management") is not None: self._ensure_context_management_beta_header( @@ -1491,7 +1514,16 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if thinking_content is not None: reasoning_content += thinking_content - return text_content, citations, thinking_blocks, reasoning_content, tool_calls, web_search_results, tool_results, compaction_blocks + return ( + text_content, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) def calculate_usage( self, @@ -1576,7 +1608,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) completion_token_details = CompletionTokensDetailsWrapper( reasoning_tokens=reasoning_tokens if reasoning_tokens > 0 else 0, - text_tokens=completion_tokens - reasoning_tokens if reasoning_tokens > 0 else completion_tokens, + text_tokens=( + completion_tokens - reasoning_tokens + if reasoning_tokens > 0 + else completion_tokens + ), ) total_tokens = prompt_tokens + completion_tokens @@ -1696,8 +1732,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): "content" ] # allow user to access raw anthropic tool calling response - model_response.choices[0].finish_reason = map_finish_reason( - completion_response["stop_reason"] + model_response.choices[0].finish_reason = cast( + OpenAIChatCompletionFinishReason, + map_finish_reason(completion_response["stop_reason"]), ) ## CALCULATING USAGE diff --git a/litellm/main.py b/litellm/main.py index 356ca7ecf1..52e7475169 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -159,6 +159,7 @@ from .litellm_core_utils.fallback_utils import ( completion_with_fallbacks, ) from .litellm_core_utils.prompt_templates.common_utils import ( + add_system_prompt_to_messages, get_completion_messages, update_messages_with_model_file_ids, ) @@ -599,7 +600,7 @@ async def acompletion( # noqa: PLR0915 # Add the context to the function ctx = contextvars.copy_context() func_with_context = partial(ctx.run, func) - + init_response = await loop.run_in_executor(None, func_with_context) if isinstance(init_response, dict) or isinstance( init_response, ModelResponse @@ -939,7 +940,7 @@ def responses_api_bridge_check( model = model.replace("responses/", "") mode = "responses" model_info["mode"] = mode - + if web_search_options is not None and custom_llm_provider == "xai": model_info["mode"] = "responses" model = model.replace("responses/", "") @@ -1108,9 +1109,7 @@ def completion( # type: ignore # noqa: PLR0915 skip_mcp_handler = kwargs.pop("_skip_mcp_handler", False) if not skip_mcp_handler and tools: - from litellm.responses.mcp.chat_completions_handler import ( - acompletion_with_mcp, - ) + from litellm.responses.mcp.chat_completions_handler import acompletion_with_mcp from litellm.responses.mcp.litellm_proxy_mcp_handler import ( LiteLLM_Proxy_MCP_Handler, ) @@ -1245,6 +1244,7 @@ def completion( # type: ignore # noqa: PLR0915 ### PROMPT MANAGEMENT ### prompt_id = cast(Optional[str], kwargs.get("prompt_id", None)) prompt_variables = cast(Optional[dict], kwargs.get("prompt_variables", None)) + litellm_system_prompt = kwargs.get("litellm_system_prompt", None) ### COPY MESSAGES ### - related issue https://github.com/BerriAI/litellm/discussions/4489 messages = get_completion_messages( messages=messages, @@ -1276,6 +1276,14 @@ def completion( # type: ignore # noqa: PLR0915 prompt_version=kwargs.get("prompt_version", None), ) + ### LITELLM SYSTEM PROMPT ### + if litellm_system_prompt: + messages = add_system_prompt_to_messages( + messages=messages, + system_prompt=litellm_system_prompt, + merge_with_first_system=True, + ) + try: if base_url is not None: api_base = base_url @@ -1558,7 +1566,9 @@ def completion( # type: ignore # noqa: PLR0915 ## RESPONSES API BRIDGE LOGIC ## - check if model has 'mode: responses' in litellm.model_cost map model_info, model = responses_api_bridge_check( - model=model, custom_llm_provider=custom_llm_provider, web_search_options=web_search_options + model=model, + custom_llm_provider=custom_llm_provider, + web_search_options=web_search_options, ) if model_info.get("mode") == "responses": @@ -2209,17 +2219,19 @@ def completion( # type: ignore # noqa: PLR0915 elif custom_llm_provider == "a2a": # A2A (Agent-to-Agent) Protocol # Resolve agent configuration from registry if model format is "a2a/" - api_base, api_key, headers = litellm.A2AConfig.resolve_agent_config_from_registry( - model=model, - api_base=api_base, - api_key=api_key, - headers=headers, - optional_params=optional_params, + api_base, api_key, headers = ( + litellm.A2AConfig.resolve_agent_config_from_registry( + model=model, + api_base=api_base, + api_key=api_key, + headers=headers, + optional_params=optional_params, + ) ) - + # Fall back to environment variables and defaults api_base = api_base or litellm.api_base or get_secret_str("A2A_API_BASE") - + if api_base is None: raise Exception( "api_base is required for A2A provider. " @@ -4783,7 +4795,10 @@ def embedding( # noqa: PLR0915 or custom_llm_provider == "together_ai" or custom_llm_provider == "nvidia_nim" or custom_llm_provider == "litellm_proxy" - or (model in litellm.open_ai_embedding_models and custom_llm_provider is None) + or ( + model in litellm.open_ai_embedding_models + and custom_llm_provider is None + ) ): api_base = ( api_base @@ -7239,7 +7254,11 @@ def stream_chunk_builder( # noqa: PLR0915 continue choice = chunk["choices"][0] - delta_obj = choice.get("delta", {}) if isinstance(choice, dict) else getattr(choice, "delta", {}) + delta_obj = ( + choice.get("delta", {}) + if isinstance(choice, dict) + else getattr(choice, "delta", {}) + ) if isinstance(delta_obj, dict): delta = delta_obj elif hasattr(delta_obj, "model_dump"): @@ -7266,7 +7285,9 @@ def stream_chunk_builder( # noqa: PLR0915 if is_simple_text_stream: if simple_content_parts: - response["choices"][0]["message"]["content"] = "".join(simple_content_parts) + response["choices"][0]["message"]["content"] = "".join( + simple_content_parts + ) completion_output = get_content_from_model_response(response) usage = processor.calculate_usage( chunks=chunks, @@ -7291,7 +7312,9 @@ def stream_chunk_builder( # noqa: PLR0915 if litellm.include_cost_in_streaming_usage and logging_obj is not None: setattr( - usage, "cost", logging_obj._response_cost_calculator(result=response) + usage, + "cost", + logging_obj._response_cost_calculator(result=response), ) return response @@ -7504,6 +7527,7 @@ def __getattr__(name: str) -> Any: # before loading tiktoken, ensuring the local cache is used # instead of downloading from the internet from litellm._lazy_imports import _get_default_encoding + _encoding = _get_default_encoding() # Cache it in the module's __dict__ for subsequent accesses import sys diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index c4b6a4fa09..813a4fb3a6 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -16,6 +16,10 @@ model_list: - model_name: gpt-5-mini litellm_params: model: openai/gpt-5-mini + - model_name: custom_litellm_model + litellm_params: + model: litellm_agent/claude-sonnet-4-5-20250929 + litellm_system_prompt: "Be a helpful assistant." guardrails: diff --git a/litellm/router.py b/litellm/router.py index c6409ce168..ac2862da68 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -163,11 +163,7 @@ from litellm.types.utils import ( ) from litellm.types.utils import ModelInfo from litellm.types.utils import ModelInfo as ModelMapInfo -from litellm.types.utils import ( - ModelResponseStream, - StandardLoggingPayload, - Usage, -) +from litellm.types.utils import ModelResponseStream, StandardLoggingPayload, Usage from litellm.utils import ( CustomStreamWrapper, EmbeddingResponse, @@ -1996,12 +1992,11 @@ class Router: When both have tools, concatenate them (deployment tools first, then request tools). tool_choice: use request value if provided, else deployment's. """ - dep_params = deployment.get("litellm_params", {}) or {} - dep_params = ( - dep_params.model_dump(exclude_none=True) - if hasattr(dep_params, "model_dump") - else dep_params - ) + dep_params_raw = deployment.get("litellm_params", {}) or {} + if isinstance(dep_params_raw, dict): + dep_params = dep_params_raw + else: + dep_params = dep_params_raw.model_dump(exclude_none=True) dep_tools = dep_params.get("tools") or [] req_tools = kwargs.get("tools") or [] if dep_tools or req_tools: @@ -2573,6 +2568,12 @@ class Router: litellm_model = data.get("model", None) + # litellm_agent/ prefix only strips the model name, no prompt_id needed + is_litellm_agent_model = ( + isinstance(litellm_model, str) + and litellm_model.startswith("litellm_agent/") + ) + prompt_id = kwargs.get("prompt_id") or prompt_management_deployment[ "litellm_params" ].get("prompt_id", None) @@ -2585,7 +2586,9 @@ class Router: "litellm_params" ].get("prompt_label", None) - if prompt_id is None or not isinstance(prompt_id, str): + if not is_litellm_agent_model and ( + prompt_id is None or not isinstance(prompt_id, str) + ): raise ValueError( f"Prompt ID is not set or not a string. Got={prompt_id}, type={type(prompt_id)}" ) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 9228b25b03..ac795ba57c 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2917,8 +2917,9 @@ all_litellm_params = ( "api_key", "api_version", "prompt_id", - "provider_specific_header", "prompt_variables", + "litellm_system_prompt", + "provider_specific_header", "prompt_version", "api_base", "force_timeout", diff --git a/tests/test_litellm/integrations/litellm_agent/test_litellm_agent_model_resolver.py b/tests/test_litellm/integrations/litellm_agent/test_litellm_agent_model_resolver.py new file mode 100644 index 0000000000..c91da9a4b8 --- /dev/null +++ b/tests/test_litellm/integrations/litellm_agent/test_litellm_agent_model_resolver.py @@ -0,0 +1,81 @@ +"""Unit tests for LiteLLMAgentModelResolver - litellm_agent/ prefix model resolution.""" + +from unittest.mock import MagicMock + +import pytest + +from litellm.integrations.litellm_agent import LiteLLMAgentModelResolver + + +class TestLiteLLMAgentModelResolver: + def test_get_chat_completion_prompt_strips_prefix(self): + """Verify get_chat_completion_prompt strips litellm_agent/ prefix from model.""" + resolver = LiteLLMAgentModelResolver() + messages = [{"role": "user", "content": "Hello"}] + + resolved_model, out_messages, out_params = resolver.get_chat_completion_prompt( + model="litellm_agent/gpt-3.5-turbo", + messages=messages, + non_default_params={}, + prompt_id=None, + prompt_variables=None, + dynamic_callback_params={}, + ) + + assert resolved_model == "gpt-3.5-turbo" + assert out_messages == messages + assert out_params == {} + + def test_get_chat_completion_prompt_preserves_rest_of_model(self): + """Verify model name after prefix is preserved (e.g. openai/gpt-3.5-turbo).""" + resolver = LiteLLMAgentModelResolver() + messages = [{"role": "user", "content": "Test"}] + + resolved_model, _, _ = resolver.get_chat_completion_prompt( + model="litellm_agent/openai/gpt-3.5-turbo", + messages=messages, + non_default_params={}, + prompt_id=None, + prompt_variables=None, + dynamic_callback_params={}, + ) + + assert resolved_model == "openai/gpt-3.5-turbo" + + def test_get_chat_completion_prompt_respects_ignore_prompt_manager_model(self): + """Verify model is unchanged when ignore_prompt_manager_model is True.""" + resolver = LiteLLMAgentModelResolver() + messages = [{"role": "user", "content": "Hello"}] + + resolved_model, _, _ = resolver.get_chat_completion_prompt( + model="litellm_agent/gpt-3.5-turbo", + messages=messages, + non_default_params={}, + prompt_id=None, + prompt_variables=None, + dynamic_callback_params={}, + ignore_prompt_manager_model=True, + ) + + assert resolved_model == "litellm_agent/gpt-3.5-turbo" + + @pytest.mark.asyncio + async def test_async_get_chat_completion_prompt_strips_prefix(self): + """Verify async_get_chat_completion_prompt strips prefix.""" + resolver = LiteLLMAgentModelResolver() + messages = [{"role": "user", "content": "Hello"}] + + resolved_model, out_messages, _ = ( + await resolver.async_get_chat_completion_prompt( + model="litellm_agent/gpt-3.5-turbo", + messages=messages, + non_default_params={}, + prompt_id=None, + prompt_variables=None, + dynamic_callback_params={}, + litellm_logging_obj=MagicMock(), + ) + ) + + assert resolved_model == "gpt-3.5-turbo" + assert out_messages == messages diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py index f566f91841..81fe56640b 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py @@ -10,6 +10,7 @@ sys.path.insert( ) # Adds the parent directory to the system path from litellm.litellm_core_utils.prompt_templates.common_utils import ( + add_system_prompt_to_messages, get_format_from_file_id, handle_any_messages_to_chat_completion_str_messages_conversion, split_concatenated_json_objects, @@ -128,6 +129,60 @@ def test_handle_any_messages_to_chat_completion_str_messages_conversion_complex( assert result[0]["input"] == json.dumps(message) +def test_add_system_prompt_to_messages_prepend(): + """Adds system prompt at beginning when no system message exists.""" + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there"}, + ] + result = add_system_prompt_to_messages(messages, "You are a helpful assistant.") + assert result == [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there"}, + ] + + +def test_add_system_prompt_to_messages_empty_prompt_unchanged(): + """Returns messages unchanged when system_prompt is empty.""" + messages = [{"role": "user", "content": "Hello"}] + assert add_system_prompt_to_messages(messages, "") == messages + assert add_system_prompt_to_messages(messages, None) == messages + + +def test_add_system_prompt_to_messages_merge_with_first_system(): + """Merges new prompt into first system message when merge_with_first_system=True.""" + messages = [ + {"role": "system", "content": "Existing system prompt."}, + {"role": "user", "content": "Hello"}, + ] + result = add_system_prompt_to_messages( + messages, "You are helpful.", merge_with_first_system=True + ) + assert result == [ + {"role": "system", "content": "You are helpful.\n\nExisting system prompt."}, + {"role": "user", "content": "Hello"}, + ] + + +def test_add_system_prompt_to_messages_merge_with_first_system_adds_new_when_no_system(): + """When merge_with_first_system=True but no system message, adds new one at start.""" + messages = [{"role": "user", "content": "Hello"}] + result = add_system_prompt_to_messages( + messages, "You are helpful.", merge_with_first_system=True + ) + assert result == [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + + +def test_add_system_prompt_to_messages_empty_list(): + """Adds system prompt to empty messages list.""" + result = add_system_prompt_to_messages([], "You are helpful.") + assert result == [{"role": "system", "content": "You are helpful."}] + + def test_convert_prefix_message_to_non_prefix_messages(): from litellm.litellm_core_utils.prompt_templates.common_utils import ( convert_prefix_message_to_non_prefix_messages, diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index 5d7a02f90d..40ecfdd305 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -2924,3 +2924,23 @@ def test_fast_mode_parameter_mapping(): assert "speed" in result assert result["speed"] == "fast" + + +def test_map_openai_params_max_tokens_normalized_to_int(): + """ + Test that map_openai_params normalizes max_tokens to an integer (e.g. 0.7 -> 1). + """ + config = AnthropicConfig() + + non_default_params = {"max_tokens": 0.7} + optional_params = {} + + result = config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model="claude-3-5-sonnet-20241022", + drop_params=False, + ) + + assert "max_tokens" in result + assert result["max_tokens"] == 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx index 375529f517..555930a576 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import AgentBuilderView from "@/components/playground/chat_ui/AgentBuilderView"; import ChatUI from "@/components/playground/chat_ui/ChatUI"; import CompareUI from "@/components/playground/compareUI/CompareUI"; import ComplianceUI from "@/components/playground/complianceUI/ComplianceUI"; @@ -39,6 +40,7 @@ export default function PlaygroundPage() { Chat Compare Compliance + Agent Builder (Experimental) @@ -57,6 +59,17 @@ export default function PlaygroundPage() { + + + ); diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx new file mode 100644 index 0000000000..10719b7ff4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/AgentBuilderView.tsx @@ -0,0 +1,379 @@ +"use client"; + +import { CommentOutlined, ExperimentOutlined, PlusOutlined, RobotOutlined, SaveOutlined } from "@ant-design/icons"; +import { Button, Input, Select, Spin, Tabs } from "antd"; +import React, { useCallback, useEffect, useState } from "react"; +import NotificationsManager from "../../molecules/notifications_manager"; +import { modelCreateCall } from "../../networking"; +import { AgentModel, fetchAvailableAgentModels } from "../llm_calls/fetch_agents"; +import { fetchAvailableModels, ModelGroup } from "../llm_calls/fetch_models"; +import ComplianceUI from "../complianceUI/ComplianceUI"; +import ChatUI from "./ChatUI"; + +const { TextArea } = Input; + +export interface AgentBuilderViewProps { + accessToken: string | null; + token: string | null; + userID: string | null; + userRole: string | null; + disabledPersonalKeyCreation?: boolean; + proxySettings?: { + PROXY_BASE_URL?: string; + LITELLM_UI_API_DOC_BASE_URL?: string | null; + }; + apiKey?: string; + customProxyBaseUrl?: string; +} + +const NEW_AGENT_ID = "__new__"; + +export default function AgentBuilderView({ + accessToken, + token, + userID, + userRole, + disabledPersonalKeyCreation = false, + proxySettings, + apiKey, + customProxyBaseUrl, +}: AgentBuilderViewProps) { + const [agentModels, setAgentModels] = useState([]); + const [modelGroups, setModelGroups] = useState([]); + const [loadingAgents, setLoadingAgents] = useState(true); + const [selectedId, setSelectedId] = useState(null); + const [activeTab, setActiveTab] = useState<"configure" | "chat" | "test">("configure"); + + // Draft for new agent + const [draftName, setDraftName] = useState(""); + const [draftSystemPrompt, setDraftSystemPrompt] = useState(""); + const [draftUnderlyingModel, setDraftUnderlyingModel] = useState(undefined); + const [draftTemperature, setDraftTemperature] = useState(0.7); + const [draftMaxTokens, setDraftMaxTokens] = useState(4096); + + const [saving, setSaving] = useState(false); + + const effectiveApiKey = apiKey || accessToken || ""; + const selectedAgent = selectedId === NEW_AGENT_ID ? null : agentModels.find((a) => a.model_name === selectedId) ?? null; + const isNewAgent = selectedId === NEW_AGENT_ID; + + const loadAgents = useCallback(async () => { + if (!accessToken || !userID || !userRole) return; + setLoadingAgents(true); + try { + const list = await fetchAvailableAgentModels(accessToken, userID, userRole); + setAgentModels(list); + if (!selectedId || (selectedId !== NEW_AGENT_ID && !list.some((a) => a.model_name === selectedId))) { + setSelectedId(list.length > 0 ? list[0].model_name : null); + } + } catch (e) { + console.error(e); + NotificationsManager.fromBackend("Failed to load agents"); + } finally { + setLoadingAgents(false); + } + }, [accessToken, userID, userRole]); + + const loadModels = useCallback(async () => { + if (!effectiveApiKey) return; + try { + const models = await fetchAvailableModels(effectiveApiKey); + setModelGroups(models); + if (!draftUnderlyingModel && models.length > 0) { + setDraftUnderlyingModel(models[0].model_group); + } + } catch (e) { + console.error(e); + } + }, [effectiveApiKey]); + + useEffect(() => { + loadAgents(); + }, [loadAgents]); + + useEffect(() => { + loadModels(); + }, [loadModels]); + + const handleAddAgent = () => { + setSelectedId(NEW_AGENT_ID); + setDraftName(""); + setDraftSystemPrompt("You are a helpful assistant."); + setDraftUnderlyingModel(modelGroups[0]?.model_group); + setDraftTemperature(0.7); + setDraftMaxTokens(4096); + setActiveTab("configure"); + }; + + const handleSaveAgent = async () => { + if (!accessToken || !draftName?.trim() || !draftUnderlyingModel) { + NotificationsManager.fromBackend("Name and underlying model are required"); + return; + } + setSaving(true); + try { + await modelCreateCall(accessToken, { + model_name: draftName.trim(), + litellm_params: { + model: `litellm_agent/${draftUnderlyingModel}`, + litellm_system_prompt: draftSystemPrompt.trim() || undefined, + temperature: draftTemperature, + max_tokens: draftMaxTokens, + }, + model_info: {}, + }); + const newName = draftName.trim(); + await loadAgents(); + setSelectedId(newName); + setActiveTab("chat"); + } catch (e) { + NotificationsManager.fromBackend("Failed to save agent"); + } finally { + setSaving(false); + } + }; + + if (!accessToken || !userID || !userRole) { + return ( +
+ Sign in to use Agent Builder. +
+ ); + } + + return ( +
+
+
+ Agent Builder + {isNewAgent ? ( + + ) : ( + Build Agents that pass your compliance requirements. + )} +
+
+ + + Agent Builder is experimental and may change or be removed without notice. + +
+
+ +
+ {/* Roster */} +
+
+ Agents +
+
+ {loadingAgents ? ( +
+ +
+ ) : ( + <> + {agentModels.map((agent) => ( + + ))} + + + )} +
+
+ + {/* Main content */} +
+ {selectedId === null && !isNewAgent && agentModels.length === 0 && !loadingAgents && ( +
+ No agents yet. Add an agent to get started. +
+ )} + {(selectedId !== null || isNewAgent) && ( + <> + setActiveTab(k as "configure" | "chat" | "test")} + className="flex-1 overflow-hidden [&_.ant-tabs-content]:h-full [&_.ant-tabs-tabpane]:h-full [&_.ant-tabs-nav]:pl-4" + items={[ + { + key: "configure", + label: ( + + Configure + + ), + children: ( +
+ {isNewAgent ? ( +
+
+ + setDraftName(e.target.value)} + placeholder="My Agent" + /> +
+
+ +