Merge pull request #19234 from BerriAI/litellm_staging_01_16_2026

Litellm staging 01 16 2026
This commit is contained in:
Sameer Kankute
2026-01-19 19:34:53 +05:30
committed by GitHub
14 changed files with 438 additions and 163 deletions
+2
View File
@@ -1,6 +1,8 @@
[supervisord]
nodaemon=true
loglevel=info
logfile=/tmp/supervisord.log
pidfile=/tmp/supervisord.pid
[group:litellm]
programs=main,health
+32 -12
View File
@@ -113,7 +113,9 @@ def _get_a2a_model_info(a2a_client: Any, kwargs: Dict[str, Any]) -> str:
litellm_logging_obj.model = model
litellm_logging_obj.custom_llm_provider = custom_llm_provider
litellm_logging_obj.model_call_details["model"] = model
litellm_logging_obj.model_call_details["custom_llm_provider"] = custom_llm_provider
litellm_logging_obj.model_call_details[
"custom_llm_provider"
] = custom_llm_provider
return agent_name
@@ -197,7 +199,11 @@ async def asend_message(
)
# Extract params from request
params = request.params.model_dump(mode="json") if hasattr(request.params, "model_dump") else dict(request.params)
params = (
request.params.model_dump(mode="json")
if hasattr(request.params, "model_dump")
else dict(request.params)
)
response_dict = await A2ACompletionBridgeHandler.handle_non_streaming(
request_id=str(request.id),
@@ -216,7 +222,9 @@ async def asend_message(
# Create A2A client if not provided but api_base is available
if a2a_client is None:
if api_base is None:
raise ValueError("Either a2a_client or api_base is required for standard A2A flow")
raise ValueError(
"Either a2a_client or api_base is required for standard A2A flow"
)
a2a_client = await create_a2a_client(base_url=api_base)
# Type assertion: a2a_client is guaranteed to be non-None here
@@ -235,7 +243,11 @@ async def asend_message(
# Calculate token usage from request and response
response_dict = a2a_response.model_dump(mode="json", exclude_none=True)
prompt_tokens, completion_tokens, _ = A2ARequestUtils.calculate_usage_from_request_response(
(
prompt_tokens,
completion_tokens,
_,
) = A2ARequestUtils.calculate_usage_from_request_response(
request=request,
response_dict=response_dict,
)
@@ -280,7 +292,9 @@ def send_message(
if loop is not None:
return asend_message(a2a_client=a2a_client, request=request, **kwargs)
else:
return asyncio.run(asend_message(a2a_client=a2a_client, request=request, **kwargs))
return asyncio.run(
asend_message(a2a_client=a2a_client, request=request, **kwargs)
)
async def asend_message_streaming(
@@ -347,7 +361,11 @@ async def asend_message_streaming(
)
# Extract params from request
params = request.params.model_dump(mode="json") if hasattr(request.params, "model_dump") else dict(request.params)
params = (
request.params.model_dump(mode="json")
if hasattr(request.params, "model_dump")
else dict(request.params)
)
async for chunk in A2ACompletionBridgeHandler.handle_streaming(
request_id=str(request.id),
@@ -365,7 +383,9 @@ async def asend_message_streaming(
# Create A2A client if not provided but api_base is available
if a2a_client is None:
if api_base is None:
raise ValueError("Either a2a_client or api_base is required for standard A2A flow")
raise ValueError(
"Either a2a_client or api_base is required for standard A2A flow"
)
a2a_client = await create_a2a_client(base_url=api_base)
# Type assertion: a2a_client is guaranteed to be non-None here
@@ -378,7 +398,9 @@ async def asend_message_streaming(
stream = a2a_client.send_message_streaming(request)
# Build logging object for streaming completion callbacks
agent_card = getattr(a2a_client, "_litellm_agent_card", None) or getattr(a2a_client, "agent_card", None)
agent_card = getattr(a2a_client, "_litellm_agent_card", None) or getattr(
a2a_client, "agent_card", None
)
agent_name = getattr(agent_card, "name", "unknown") if agent_card else "unknown"
model = f"a2a_agent/{agent_name}"
@@ -456,7 +478,7 @@ async def create_a2a_client(
if not A2A_SDK_AVAILABLE:
raise ImportError(
"The 'a2a' package is required for A2A agent invocation. "
"Install it with: pip install a2a"
"Install it with: pip install a2a-sdk"
)
verbose_logger.info(f"Creating A2A client for {base_url}")
@@ -512,7 +534,7 @@ async def aget_agent_card(
if not A2A_SDK_AVAILABLE:
raise ImportError(
"The 'a2a' package is required for A2A agent invocation. "
"Install it with: pip install a2a"
"Install it with: pip install a2a-sdk"
)
verbose_logger.info(f"Fetching agent card from {base_url}")
@@ -534,5 +556,3 @@ async def aget_agent_card(
f"Fetched agent card: {agent_card.name if hasattr(agent_card, 'name') else 'unknown'}"
)
return agent_card
+4 -1
View File
@@ -987,7 +987,10 @@ class OpenTelemetry(CustomLogger):
# TODO: Refactor to use the proper OTEL Logs API instead of directly creating SDK LogRecords
from opentelemetry._logs import SeverityNumber, get_logger, get_logger_provider
from opentelemetry.sdk._logs import LogRecord as SdkLogRecord
try:
from opentelemetry.sdk._logs import LogRecord as SdkLogRecord # OTEL < 1.39.0
except ImportError:
from opentelemetry.sdk._logs._internal import LogRecord as SdkLogRecord # OTEL >= 1.39.0
otel_logger = get_logger(LITELLM_LOGGER_NAME)
@@ -4410,9 +4410,10 @@ def _bedrock_tools_pt(tools: List) -> List[BedrockToolBlock]:
defs = parameters.pop("$defs", {})
defs_copy = copy.deepcopy(defs)
# flatten the defs
for _, value in defs_copy.items():
unpack_defs(value, defs_copy)
# Expand $ref references in parameters using the definitions
# Note: We don't pre-flatten defs as that causes exponential memory growth
# with circular references (see issue #19098). unpack_defs handles nested
# refs recursively and correctly detects/skips circular references.
unpack_defs(parameters, defs_copy)
tool_input_schema = BedrockToolInputSchemaBlock(
json=BedrockToolJsonSchemaBlock(
+4 -3
View File
@@ -453,9 +453,10 @@ def _build_vertex_schema(parameters: dict, add_property_ordering: bool = False):
valid_schema_fields = set(get_type_hints(Schema).keys())
defs = parameters.pop("$defs", {})
# flatten the defs
for name, value in defs.items():
unpack_defs(value, defs)
# Expand $ref references in parameters using the definitions
# Note: We don't pre-flatten defs as that causes exponential memory growth
# with circular references (see issue #19098). unpack_defs handles nested
# refs recursively and correctly detects/skips circular references.
unpack_defs(parameters, defs)
# 5. Nullable fields:
@@ -7857,6 +7857,24 @@
"supports_tool_choice": true,
"supports_vision": true
},
"dall-e-2": {
"input_cost_per_image": 0.02,
"litellm_provider": "openai",
"mode": "image_generation",
"supported_endpoints": [
"/v1/images/generations",
"/v1/images/edits",
"/v1/images/variations"
]
},
"dall-e-3": {
"input_cost_per_image": 0.04,
"litellm_provider": "openai",
"mode": "image_generation",
"supported_endpoints": [
"/v1/images/generations"
]
},
"deepseek-chat": {
"cache_read_input_token_cost": 2.8e-08,
"input_cost_per_token": 2.8e-07,
+67 -21
View File
@@ -55,9 +55,30 @@ async def _handle_stream_message(
proxy_server_request: Optional[dict] = None,
) -> StreamingResponse:
"""Handle message/stream method via SDK functions."""
from a2a.types import MessageSendParams, SendStreamingMessageRequest
from litellm.a2a_protocol import asend_message_streaming
from litellm.a2a_protocol.main import A2A_SDK_AVAILABLE
# Check is handled in invoke_agent_a2a, but if called directly:
if not A2A_SDK_AVAILABLE:
# Return a streaming response that yields an error
async def _error_stream():
yield json.dumps(
{
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": -32603,
"message": "Server error: 'a2a' package not installed",
},
}
) + "\n"
return StreamingResponse(_error_stream(), media_type="application/x-ndjson")
from a2a.types import (
MessageSendParams,
SendStreamingMessageRequest,
)
async def stream_response():
try:
@@ -75,16 +96,20 @@ async def _handle_stream_message(
):
# Chunk may be dict or object depending on bridge vs standard path
if hasattr(chunk, "model_dump"):
yield json.dumps(chunk.model_dump(mode="json", exclude_none=True)) + "\n"
yield json.dumps(
chunk.model_dump(mode="json", exclude_none=True)
) + "\n"
else:
yield json.dumps(chunk) + "\n"
except Exception as e:
verbose_proxy_logger.exception(f"Error streaming A2A response: {e}")
yield json.dumps({
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32603, "message": f"Streaming error: {str(e)}"},
}) + "\n"
yield json.dumps(
{
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32603, "message": f"Streaming error: {str(e)}"},
}
) + "\n"
return StreamingResponse(stream_response(), media_type="application/x-ndjson")
@@ -169,9 +194,8 @@ async def invoke_agent_a2a(
- message/send: Send a message and get a response
- message/stream: Send a message and stream the response
"""
from a2a.types import MessageSendParams, SendMessageRequest
from litellm.a2a_protocol import asend_message
from litellm.a2a_protocol.main import A2A_SDK_AVAILABLE
from litellm.proxy.agent_endpoints.auth.agent_permission_handler import (
AgentRequestHandler,
)
@@ -189,16 +213,28 @@ async def invoke_agent_a2a(
# Validate JSON-RPC format
if body.get("jsonrpc") != "2.0":
return _jsonrpc_error(body.get("id"), -32600, "Invalid Request: jsonrpc must be '2.0'")
return _jsonrpc_error(
body.get("id"), -32600, "Invalid Request: jsonrpc must be '2.0'"
)
request_id = body.get("id")
method = body.get("method")
params = body.get("params", {})
if not A2A_SDK_AVAILABLE:
return _jsonrpc_error(
request_id,
-32603,
"Server error: 'a2a' package not installed. Please install 'a2a-sdk'.",
500,
)
# Find the agent
agent = _get_agent(agent_id)
if agent is None:
return _jsonrpc_error(request_id, -32000, f"Agent '{agent_id}' not found", 404)
return _jsonrpc_error(
request_id, -32000, f"Agent '{agent_id}' not found", 404
)
is_allowed = await AgentRequestHandler.is_agent_allowed(
agent_id=agent.agent_id,
@@ -213,23 +249,29 @@ async def invoke_agent_a2a(
# Get backend URL and agent name
agent_url = agent.agent_card_params.get("url")
agent_name = agent.agent_card_params.get("name", agent_id)
# Get litellm_params (may include custom_llm_provider for completion bridge)
litellm_params = agent.litellm_params or {}
custom_llm_provider = litellm_params.get("custom_llm_provider")
# URL is required unless using completion bridge with a provider that derives endpoint from model
# (e.g., bedrock/agentcore derives endpoint from ARN in model string)
if not agent_url and not custom_llm_provider:
return _jsonrpc_error(request_id, -32000, f"Agent '{agent_id}' has no URL configured", 500)
return _jsonrpc_error(
request_id, -32000, f"Agent '{agent_id}' has no URL configured", 500
)
verbose_proxy_logger.info(f"Proxying A2A request to agent '{agent_id}' at {agent_url or 'completion-bridge'}")
verbose_proxy_logger.info(
f"Proxying A2A request to agent '{agent_id}' at {agent_url or 'completion-bridge'}"
)
# Set up data dict for litellm processing
body.update({
"model": f"a2a_agent/{agent_name}",
"custom_llm_provider": "a2a_agent",
})
body.update(
{
"model": f"a2a_agent/{agent_name}",
"custom_llm_provider": "a2a_agent",
}
)
# Add litellm data (user_api_key, user_id, team_id, etc.)
data = await add_litellm_data_to_request(
@@ -243,6 +285,8 @@ async def invoke_agent_a2a(
# Route through SDK functions
if method == "message/send":
from a2a.types import MessageSendParams, SendMessageRequest
a2a_request = SendMessageRequest(
id=request_id,
params=MessageSendParams(**params),
@@ -255,7 +299,9 @@ async def invoke_agent_a2a(
metadata=data.get("metadata", {}),
proxy_server_request=data.get("proxy_server_request"),
)
return JSONResponse(content=response.model_dump(mode="json", exclude_none=True))
return JSONResponse(
content=response.model_dump(mode="json", exclude_none=True)
)
elif method == "message/stream":
return await _handle_stream_message(
+18
View File
@@ -7857,6 +7857,24 @@
"supports_tool_choice": true,
"supports_vision": true
},
"dall-e-2": {
"input_cost_per_image": 0.02,
"litellm_provider": "openai",
"mode": "image_generation",
"supported_endpoints": [
"/v1/images/generations",
"/v1/images/edits",
"/v1/images/variations"
]
},
"dall-e-3": {
"input_cost_per_image": 0.04,
"litellm_provider": "openai",
"mode": "image_generation",
"supported_endpoints": [
"/v1/images/generations"
]
},
"deepseek-chat": {
"cache_read_input_token_cost": 2.8e-08,
"input_cost_per_token": 2.8e-07,
Generated
+110 -88
View File
@@ -1,4 +1,36 @@
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "a2a-sdk"
version = "0.3.22"
description = "A2A Python SDK"
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "python_version >= \"3.10\" and extra == \"extra-proxy\""
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"},
]
[package.dependencies]
google-api-core = ">=1.26.0"
httpx = ">=0.28.1"
httpx-sse = ">=0.4.0"
protobuf = ">=5.29.5"
pydantic = ">=2.11.3"
[package.extras]
all = ["cryptography (>=43.0.0)", "fastapi (>=0.115.2)", "grpcio (>=1.60)", "grpcio-reflection (>=1.7.0)", "grpcio-tools (>=1.60)", "opentelemetry-api (>=1.33.0)", "opentelemetry-sdk (>=1.33.0)", "pyjwt (>=2.0.0)", "sqlalchemy[aiomysql,asyncio] (>=2.0.0)", "sqlalchemy[aiosqlite,asyncio] (>=2.0.0)", "sqlalchemy[asyncio,postgresql-asyncpg] (>=2.0.0)", "sse-starlette", "starlette"]
encryption = ["cryptography (>=43.0.0)"]
grpc = ["grpcio (>=1.60)", "grpcio-reflection (>=1.7.0)", "grpcio-tools (>=1.60)"]
http-server = ["fastapi (>=0.115.2)", "sse-starlette", "starlette"]
mysql = ["sqlalchemy[aiomysql,asyncio] (>=2.0.0)"]
postgresql = ["sqlalchemy[asyncio,postgresql-asyncpg] (>=2.0.0)"]
signing = ["pyjwt (>=2.0.0)"]
sql = ["sqlalchemy[aiomysql,asyncio] (>=2.0.0)", "sqlalchemy[aiosqlite,asyncio] (>=2.0.0)", "sqlalchemy[asyncio,postgresql-asyncpg] (>=2.0.0)"]
sqlite = ["sqlalchemy[aiosqlite,asyncio] (>=2.0.0)"]
telemetry = ["opentelemetry-api (>=1.33.0)", "opentelemetry-sdk (>=1.33.0)"]
[[package]]
name = "aiofiles"
@@ -1268,25 +1300,6 @@ dev = ["autoflake", "black", "build", "databricks-connect", "httpx", "ipython",
notebook = ["ipython (>=8,<10)", "ipywidgets (>=8,<9)"]
openai = ["httpx", "langchain-openai ; python_version > \"3.7\"", "openai"]
[[package]]
name = "deprecated"
version = "1.3.1"
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
groups = ["main", "dev", "proxy-dev"]
files = [
{file = "deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f"},
{file = "deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223"},
]
markers = {main = "python_version >= \"3.10\""}
[package.dependencies]
wrapt = ">=1.10,<3"
[package.extras]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"]
[[package]]
name = "diskcache"
version = "5.6.3"
@@ -2521,7 +2534,7 @@ description = "Consume Server-Sent Event (SSE) messages with HTTPX."
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version >= \"3.10\" and extra == \"proxy\""
markers = "python_version >= \"3.10\" and (extra == \"proxy\" or extra == \"extra-proxy\")"
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"},
@@ -4036,143 +4049,153 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
[[package]]
name = "opentelemetry-api"
version = "1.25.0"
version = "1.39.1"
description = "OpenTelemetry Python API"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["main", "dev", "proxy-dev"]
files = [
{file = "opentelemetry_api-1.25.0-py3-none-any.whl", hash = "sha256:757fa1aa020a0f8fa139f8959e53dec2051cc26b832e76fa839a6d76ecefd737"},
{file = "opentelemetry_api-1.25.0.tar.gz", hash = "sha256:77c4985f62f2614e42ce77ee4c9da5fa5f0bc1e1821085e9a47533a9323ae869"},
{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\""}
[package.dependencies]
deprecated = ">=1.2.6"
importlib-metadata = ">=6.0,<=7.1"
importlib-metadata = ">=6.0,<8.8.0"
typing-extensions = ">=4.5.0"
[[package]]
name = "opentelemetry-exporter-otlp"
version = "1.25.0"
version = "1.39.1"
description = "OpenTelemetry Collector Exporters"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["dev", "proxy-dev"]
files = [
{file = "opentelemetry_exporter_otlp-1.25.0-py3-none-any.whl", hash = "sha256:d67a831757014a3bc3174e4cd629ae1493b7ba8d189e8a007003cacb9f1a6b60"},
{file = "opentelemetry_exporter_otlp-1.25.0.tar.gz", hash = "sha256:ce03199c1680a845f82e12c0a6a8f61036048c07ec7a0bd943142aca8fa6ced0"},
{file = "opentelemetry_exporter_otlp-1.39.1-py3-none-any.whl", hash = "sha256:68ae69775291f04f000eb4b698ff16ff685fdebe5cb52871bc4e87938a7b00fe"},
{file = "opentelemetry_exporter_otlp-1.39.1.tar.gz", hash = "sha256:7cf7470e9fd0060c8a38a23e4f695ac686c06a48ad97f8d4867bc9b420180b9c"},
]
[package.dependencies]
opentelemetry-exporter-otlp-proto-grpc = "1.25.0"
opentelemetry-exporter-otlp-proto-http = "1.25.0"
opentelemetry-exporter-otlp-proto-grpc = "1.39.1"
opentelemetry-exporter-otlp-proto-http = "1.39.1"
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.25.0"
version = "1.39.1"
description = "OpenTelemetry Protobuf encoding"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["dev", "proxy-dev"]
files = [
{file = "opentelemetry_exporter_otlp_proto_common-1.25.0-py3-none-any.whl", hash = "sha256:15637b7d580c2675f70246563363775b4e6de947871e01d0f4e3881d1848d693"},
{file = "opentelemetry_exporter_otlp_proto_common-1.25.0.tar.gz", hash = "sha256:c93f4e30da4eee02bacd1e004eb82ce4da143a2f8e15b987a9f603e0a85407d3"},
{file = "opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde"},
{file = "opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464"},
]
[package.dependencies]
opentelemetry-proto = "1.25.0"
opentelemetry-proto = "1.39.1"
[[package]]
name = "opentelemetry-exporter-otlp-proto-grpc"
version = "1.25.0"
version = "1.39.1"
description = "OpenTelemetry Collector Protobuf over gRPC Exporter"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["dev", "proxy-dev"]
files = [
{file = "opentelemetry_exporter_otlp_proto_grpc-1.25.0-py3-none-any.whl", hash = "sha256:3131028f0c0a155a64c430ca600fd658e8e37043cb13209f0109db5c1a3e4eb4"},
{file = "opentelemetry_exporter_otlp_proto_grpc-1.25.0.tar.gz", hash = "sha256:c0b1661415acec5af87625587efa1ccab68b873745ca0ee96b69bb1042087eac"},
{file = "opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18"},
{file = "opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad"},
]
[package.dependencies]
deprecated = ">=1.2.6"
googleapis-common-protos = ">=1.52,<2.0"
grpcio = ">=1.0.0,<2.0.0"
googleapis-common-protos = ">=1.57,<2.0"
grpcio = [
{version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""},
{version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""},
]
opentelemetry-api = ">=1.15,<2.0"
opentelemetry-exporter-otlp-proto-common = "1.25.0"
opentelemetry-proto = "1.25.0"
opentelemetry-sdk = ">=1.25.0,<1.26.0"
opentelemetry-exporter-otlp-proto-common = "1.39.1"
opentelemetry-proto = "1.39.1"
opentelemetry-sdk = ">=1.39.1,<1.40.0"
typing-extensions = ">=4.6.0"
[package.extras]
gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.25.0"
version = "1.39.1"
description = "OpenTelemetry Collector Protobuf over HTTP Exporter"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["dev", "proxy-dev"]
files = [
{file = "opentelemetry_exporter_otlp_proto_http-1.25.0-py3-none-any.whl", hash = "sha256:2eca686ee11b27acd28198b3ea5e5863a53d1266b91cda47c839d95d5e0541a6"},
{file = "opentelemetry_exporter_otlp_proto_http-1.25.0.tar.gz", hash = "sha256:9f8723859e37c75183ea7afa73a3542f01d0fd274a5b97487ea24cb683d7d684"},
{file = "opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985"},
{file = "opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb"},
]
[package.dependencies]
deprecated = ">=1.2.6"
googleapis-common-protos = ">=1.52,<2.0"
opentelemetry-api = ">=1.15,<2.0"
opentelemetry-exporter-otlp-proto-common = "1.25.0"
opentelemetry-proto = "1.25.0"
opentelemetry-sdk = ">=1.25.0,<1.26.0"
opentelemetry-exporter-otlp-proto-common = "1.39.1"
opentelemetry-proto = "1.39.1"
opentelemetry-sdk = ">=1.39.1,<1.40.0"
requests = ">=2.7,<3.0"
typing-extensions = ">=4.5.0"
[package.extras]
gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"]
[[package]]
name = "opentelemetry-proto"
version = "1.25.0"
version = "1.39.1"
description = "OpenTelemetry Python Proto"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["main", "dev", "proxy-dev"]
files = [
{file = "opentelemetry_proto-1.25.0-py3-none-any.whl", hash = "sha256:f07e3341c78d835d9b86665903b199893befa5e98866f63d22b00d0b7ca4972f"},
{file = "opentelemetry_proto-1.25.0.tar.gz", hash = "sha256:35b6ef9dc4a9f7853ecc5006738ad40443701e52c26099e197895cbda8b815a3"},
{file = "opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007"},
{file = "opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8"},
]
markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""}
[package.dependencies]
protobuf = ">=3.19,<5.0"
protobuf = ">=5.0,<7.0"
[[package]]
name = "opentelemetry-sdk"
version = "1.25.0"
version = "1.39.1"
description = "OpenTelemetry Python SDK"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["main", "dev", "proxy-dev"]
files = [
{file = "opentelemetry_sdk-1.25.0-py3-none-any.whl", hash = "sha256:d97ff7ec4b351692e9d5a15af570c693b8715ad78b8aafbec5c7100fe966b4c9"},
{file = "opentelemetry_sdk-1.25.0.tar.gz", hash = "sha256:ce7fc319c57707ef5bf8b74fb9f8ebdb8bfafbe11898410e0d2a761d08a98ec7"},
{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\""}
[package.dependencies]
opentelemetry-api = "1.25.0"
opentelemetry-semantic-conventions = "0.46b0"
typing-extensions = ">=3.7.4"
opentelemetry-api = "1.39.1"
opentelemetry-semantic-conventions = "0.60b1"
typing-extensions = ">=4.5.0"
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.46b0"
version = "0.60b1"
description = "OpenTelemetry Semantic Conventions"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["main", "dev", "proxy-dev"]
files = [
{file = "opentelemetry_semantic_conventions-0.46b0-py3-none-any.whl", hash = "sha256:6daef4ef9fa51d51855d9f8e0ccd3a1bd59e0e545abe99ac6203804e36ab3e07"},
{file = "opentelemetry_semantic_conventions-0.46b0.tar.gz", hash = "sha256:fbc982ecbb6a6e90869b15c1673be90bd18c8a56ff1cffc0864e38e2edffaefa"},
{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\""}
[package.dependencies]
opentelemetry-api = "1.25.0"
opentelemetry-api = "1.39.1"
typing-extensions = ">=4.5.0"
[[package]]
name = "orjson"
@@ -4828,23 +4851,23 @@ testing = ["google-api-core (>=1.31.5)"]
[[package]]
name = "protobuf"
version = "4.25.8"
version = "5.29.5"
description = ""
optional = false
python-versions = ">=3.8"
groups = ["main", "dev", "proxy-dev"]
files = [
{file = "protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0"},
{file = "protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9"},
{file = "protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f"},
{file = "protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7"},
{file = "protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0"},
{file = "protobuf-4.25.8-cp38-cp38-win32.whl", hash = "sha256:27d498ffd1f21fb81d987a041c32d07857d1d107909f5134ba3350e1ce80a4af"},
{file = "protobuf-4.25.8-cp38-cp38-win_amd64.whl", hash = "sha256:d552c53d0415449c8d17ced5c341caba0d89dbf433698e1436c8fa0aae7808a3"},
{file = "protobuf-4.25.8-cp39-cp39-win32.whl", hash = "sha256:077ff8badf2acf8bc474406706ad890466274191a48d0abd3bd6987107c9cde5"},
{file = "protobuf-4.25.8-cp39-cp39-win_amd64.whl", hash = "sha256:f4510b93a3bec6eba8fd8f1093e9d7fb0d4a24d1a81377c10c0e5bbfe9e4ed24"},
{file = "protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59"},
{file = "protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd"},
{file = "protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079"},
{file = "protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc"},
{file = "protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671"},
{file = "protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015"},
{file = "protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61"},
{file = "protobuf-5.29.5-cp38-cp38-win32.whl", hash = "sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238"},
{file = "protobuf-5.29.5-cp38-cp38-win_amd64.whl", hash = "sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e"},
{file = "protobuf-5.29.5-cp39-cp39-win32.whl", hash = "sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736"},
{file = "protobuf-5.29.5-cp39-cp39-win_amd64.whl", hash = "sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353"},
{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 == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\""}
@@ -7687,7 +7710,7 @@ version = "1.17.3"
description = "Module for decorators, wrappers and monkey patching."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev", "proxy-dev"]
groups = ["dev"]
files = [
{file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"},
{file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"},
@@ -7771,7 +7794,6 @@ files = [
{file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"},
{file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"},
]
markers = {main = "python_version >= \"3.10\""}
[[package]]
name = "wsproto"
@@ -7972,7 +7994,7 @@ type = ["pytest-mypy"]
[extras]
caching = ["diskcache"]
extra-proxy = ["azure-identity", "azure-keyvault-secrets", "google-cloud-iam", "google-cloud-kms", "prisma", "redisvl", "resend"]
extra-proxy = ["a2a-sdk", "azure-identity", "azure-keyvault-secrets", "google-cloud-iam", "google-cloud-kms", "prisma", "redisvl", "resend"]
mlflow = ["mlflow"]
proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "soundfile", "uvicorn", "uvloop", "websockets"]
semantic-router = ["semantic-router"]
+9 -7
View File
@@ -59,6 +59,7 @@ websockets = {version = "^15.0.1", optional = true}
boto3 = {version = "1.36.0", optional = true}
redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"}
mcp = {version = "^1.21.2", optional = true, python = ">=3.10"}
a2a-sdk = {version = "^0.3.22", optional = true, python = ">=3.10"}
litellm-proxy-extras = {version = "0.4.23", optional = true}
rich = {version = "13.7.1", optional = true}
litellm-enterprise = {version = "0.1.27", optional = true}
@@ -111,7 +112,8 @@ extra_proxy = [
"google-cloud-kms",
"google-cloud-iam",
"resend",
"redisvl"
"redisvl",
"a2a-sdk"
]
utils = [
@@ -147,9 +149,9 @@ types-requests = "*"
types-setuptools = "*"
types-redis = "*"
types-PyYAML = "*"
opentelemetry-api = "1.25.0"
opentelemetry-sdk = "1.25.0"
opentelemetry-exporter-otlp = "1.25.0"
opentelemetry-api = "^1.28.0"
opentelemetry-sdk = "^1.28.0"
opentelemetry-exporter-otlp = "^1.28.0"
langfuse = "^2.45.0"
fastapi-offline = "^1.7.3"
@@ -157,9 +159,9 @@ fastapi-offline = "^1.7.3"
prisma = "0.11.0"
hypercorn = "^0.15.0"
prometheus-client = "0.20.0"
opentelemetry-api = "1.25.0"
opentelemetry-sdk = "1.25.0"
opentelemetry-exporter-otlp = "1.25.0"
opentelemetry-api = "^1.28.0"
opentelemetry-sdk = "^1.28.0"
opentelemetry-exporter-otlp = "^1.28.0"
azure-identity = {version = "^1.15.0", python = ">=3.9"}
[build-system]
+7 -6
View File
@@ -16,12 +16,12 @@ prisma==0.11.0 # for db
nodejs-wheel-binaries==24.12.0 ## required by prisma for migrations, prevents runtime download (updated from nodejs-bin for security fixes)
mangum==0.17.0 # for aws lambda functions
pynacl==1.6.2 # for encrypting keys
google-cloud-aiplatform==1.47.0 # for vertex ai calls
google-cloud-aiplatform==1.133.0 # for vertex ai calls
google-cloud-iam==2.19.1 # for GCP IAM Redis authentication
google-genai==1.22.0
google-genai==1.37.0
anthropic[vertex]==0.54.0
mcp==1.25.0 ; python_version >= "3.10" # for MCP server
google-generativeai==0.5.0 # for vertex ai calls
# google-generativeai removed - deprecated, replaced by google-genai (line 21)
async_generator==1.10.0 # for async ollama calls
langfuse==2.59.7 # for langfuse self-hosted logging
prometheus_client==0.20.0 # for /metrics endpoint on proxy
@@ -38,9 +38,10 @@ azure-ai-contentsafety==1.0.0 # for azure content safety
azure-identity==1.16.1 ; python_version >= "3.9" # for azure content safety
azure-keyvault==4.2.0 # for azure KMS integration
azure-storage-file-datalake==12.20.0 # for azure buck storage logging
opentelemetry-api==1.25.0
opentelemetry-sdk==1.25.0
opentelemetry-exporter-otlp==1.25.0
opentelemetry-api==1.28.0
opentelemetry-sdk==1.28.0
opentelemetry-exporter-otlp==1.28.0
a2a-sdk>=0.3.22 ; python_version >= "3.10"
# grpcio: 1.68.0-1.68.1 has reconnect bug (#38290), 1.75+ has Python 3.14 wheels + fix
grpcio>=1.62.3,!=1.68.*,!=1.69.*,!=1.70.*,!=1.71.0,!=1.71.1,!=1.72.0,!=1.72.1,!=1.73.0; python_version < "3.14"
grpcio>=1.75.0; python_version >= "3.14"
+1
View File
@@ -89,6 +89,7 @@ tokenizers: >=0.20.2 # Apache 2.0 License
jinja2: >=3.1.4 # BSD 3-Clause License
litellm-proxy-extras: >=0.1.1 # MIT License
litellm-enterprise: >=0.1.1 # LiteLLM Enterprise License
a2a-sdk: >=0.3.22 # Apache 2.0 license
anyio: >=4.5.0 # Unknown license
httpx-aiohttp: >=0.1.4 # Unknown license
backoff: >=2.2.1 # Unknown license
@@ -1392,3 +1392,134 @@ def test_anthropic_messages_pt_server_tool_use_passthrough():
b for b in assistant_msg["content"] if b.get("type") == "text"
)
assert text_block["text"] == "I found the time tool. How can I help you?"
def test_bedrock_tools_unpack_defs_no_oom_with_nested_refs():
"""
Regression test for issue #19098: unpack_defs() causes OOM with nested tool schemas.
The old implementation had a "flatten defs" loop that would pre-expand each def
using unpack_defs(), but since defs often reference each other, each subsequent
call would copy already-expanded content, causing exponential memory growth.
This test creates a schema with multiple nested $defs that reference each other
to verify the fix prevents memory explosion while still correctly resolving refs.
"""
import sys
import copy
from litellm.litellm_core_utils.prompt_templates.factory import _bedrock_tools_pt
# Schema with multiple nested $defs that reference each other
# This pattern would cause OOM with the old "flatten defs" loop
complex_nested_schema = {
"type": "object",
"properties": {
"query": {"$ref": "#/$defs/Expression"},
},
"$defs": {
"Expression": {
"type": "object",
"properties": {
"type": {"type": "string", "enum": ["and", "or", "not", "comparison"]},
"left": {"$ref": "#/$defs/Operand"},
"right": {"$ref": "#/$defs/Operand"},
"operator": {"$ref": "#/$defs/Operator"},
},
},
"Operand": {
"type": "object",
"anyOf": [
{"$ref": "#/$defs/Literal"},
{"$ref": "#/$defs/FieldRef"},
{"$ref": "#/$defs/Expression"}, # Circular: Operand -> Expression -> Operand
],
},
"Literal": {
"type": "object",
"properties": {
"type": {"type": "string", "const": "literal"},
"value": {"$ref": "#/$defs/LiteralValue"},
},
},
"LiteralValue": {
"oneOf": [
{"type": "string"},
{"type": "number"},
{"type": "boolean"},
{"type": "null"},
],
},
"FieldRef": {
"type": "object",
"properties": {
"type": {"type": "string", "const": "field"},
"name": {"type": "string"},
"table": {"$ref": "#/$defs/TableRef"},
},
},
"TableRef": {
"type": "object",
"properties": {
"name": {"type": "string"},
"alias": {"type": "string"},
},
},
"Operator": {
"type": "string",
"enum": ["=", "!=", "<", ">", "<=", ">=", "LIKE", "IN"],
},
},
}
tools = [
{
"type": "function",
"function": {
"name": "execute_query",
"description": "Execute a query with complex expressions",
"parameters": complex_nested_schema,
},
}
]
# Measure initial size
def get_size(obj, seen=None):
size = sys.getsizeof(obj)
if seen is None:
seen = set()
obj_id = id(obj)
if obj_id in seen:
return 0
seen.add(obj_id)
if isinstance(obj, dict):
size += sum([get_size(v, seen) for v in obj.values()])
size += sum([get_size(k, seen) for k in obj.keys()])
elif hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes, bytearray)):
size += sum([get_size(i, seen) for i in obj])
return size
initial_size = get_size(tools)
# Process through _bedrock_tools_pt - this should complete without OOM
tools_copy = copy.deepcopy(tools)
result = _bedrock_tools_pt(tools=tools_copy)
final_size = get_size(result)
# The expansion factor should be reasonable (< 100x), not exponential (35000x as in #19098)
expansion_factor = final_size / initial_size
assert expansion_factor < 100, (
f"Memory expansion factor {expansion_factor:.1f}x is too high. "
f"Initial: {initial_size} bytes, Final: {final_size} bytes"
)
# Verify the result is valid Bedrock tools format
assert isinstance(result, list)
assert len(result) == 1
assert "toolSpec" in result[0]
assert result[0]["toolSpec"]["name"] == "execute_query"
# Verify $defs have been removed (Bedrock doesn't support them)
tool_schema = result[0]["toolSpec"].get("inputSchema", {}).get("json", {})
assert "$defs" not in tool_schema, "$defs should be removed after expansion"
@@ -49,18 +49,20 @@ async def test_invoke_agent_a2a_adds_litellm_data():
# Mock request
mock_request = MagicMock()
mock_request.json = AsyncMock(return_value={
"jsonrpc": "2.0",
"id": "test-id",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Hello"}],
"messageId": "msg-123",
}
},
})
mock_request.json = AsyncMock(
return_value={
"jsonrpc": "2.0",
"id": "test-id",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Hello"}],
"messageId": "msg-123",
}
},
}
)
mock_user_api_key_dict = UserAPIKeyAuth(
api_key="sk-test-key",
@@ -77,40 +79,44 @@ async def test_invoke_agent_a2a_adds_litellm_data():
SendMessageRequest,
SendStreamingMessageRequest,
)
# Real types available - use them
use_real_types = True
pass
except ImportError:
# Real types not available - create realistic mocks
use_real_types = False
pass
def make_mock_pydantic_class(name):
"""Create a mock class that behaves like a Pydantic model."""
class MockPydanticClass:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
# Store kwargs for model_dump() if needed
self._kwargs = kwargs
def model_dump(self, mode="json", exclude_none=False):
"""Mock model_dump method."""
result = dict(self._kwargs)
if exclude_none:
result = {k: v for k, v in result.items() if v is not None}
return result
MockPydanticClass.__name__ = name
return MockPydanticClass
MessageSendParams = make_mock_pydantic_class("MessageSendParams")
SendMessageRequest = make_mock_pydantic_class("SendMessageRequest")
SendStreamingMessageRequest = make_mock_pydantic_class("SendStreamingMessageRequest")
SendStreamingMessageRequest = make_mock_pydantic_class(
"SendStreamingMessageRequest"
)
# Create a mock module for a2a.types
mock_a2a_types = MagicMock()
mock_a2a_types.MessageSendParams = MessageSendParams
mock_a2a_types.SendMessageRequest = SendMessageRequest
mock_a2a_types.SendStreamingMessageRequest = SendStreamingMessageRequest
# Patch at the source modules
with patch(
"litellm.proxy.agent_endpoints.a2a_endpoints._get_agent",
@@ -137,12 +143,15 @@ async def test_invoke_agent_a2a_adds_litellm_data():
), patch.dict(
sys.modules,
{"a2a": MagicMock(), "a2a.types": mock_a2a_types},
), patch(
"litellm.a2a_protocol.main.A2A_SDK_AVAILABLE",
True,
):
from litellm.proxy.agent_endpoints.a2a_endpoints import invoke_agent_a2a
mock_fastapi_response = MagicMock()
result = await invoke_agent_a2a(
await invoke_agent_a2a(
agent_id="test-agent",
request=mock_request,
fastapi_response=mock_fastapi_response,