mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
feat(proxy): configure the request header buffer size (#49883)
* feat(proxy): configure the request header buffer size * fix(proxy): preserve automatic HTTP parser selection
This commit is contained in:
parent
2d0c720425
commit
e3b12fe703
6 changed files with 197 additions and 9 deletions
|
|
@ -135,6 +135,7 @@ ANTHROPIC_DEFAULT_FILE_LIST_LIMIT: Final = 20
|
|||
DEFAULT_SQS_FLUSH_INTERVAL_SECONDS: Final = get_env_int("DEFAULT_SQS_FLUSH_INTERVAL_SECONDS", 10)
|
||||
DEFAULT_NUM_WORKERS_LITELLM_PROXY: Final = get_env_int("DEFAULT_NUM_WORKERS_LITELLM_PROXY", 1)
|
||||
GRACEFUL_SHUTDOWN_TEARDOWN_HEADROOM_SECONDS: Final = 10
|
||||
DEFAULT_MAX_REQUEST_HEADER_SIZE: Final = 16 * 1024
|
||||
budget_reservation_disabled_info_emitted = False
|
||||
DYNAMIC_RATE_LIMIT_ERROR_THRESHOLD_PER_MINUTE = get_env_int("DYNAMIC_RATE_LIMIT_ERROR_THRESHOLD_PER_MINUTE", 1)
|
||||
DEFAULT_SQS_BATCH_SIZE: Final = get_env_int("DEFAULT_SQS_BATCH_SIZE", 512)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from typing_extensions import ReadOnly, TypedDict
|
|||
|
||||
import litellm
|
||||
from litellm._logging import StartupOutputSettings, print_startup_message, print_startup_warning, terminal_text
|
||||
from litellm.constants import DEFAULT_NUM_WORKERS_LITELLM_PROXY
|
||||
from litellm.constants import DEFAULT_MAX_REQUEST_HEADER_SIZE, DEFAULT_NUM_WORKERS_LITELLM_PROXY
|
||||
from litellm.proxy.db.pgbouncer import (
|
||||
PgBouncerError,
|
||||
PgBouncerSettings,
|
||||
|
|
@ -409,6 +409,7 @@ class ProxyInitializationHelpers:
|
|||
log_config: str | None = None,
|
||||
keepalive_timeout: int | None = None,
|
||||
timeout_worker_healthcheck: int | None = None,
|
||||
max_request_header_size: int | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Get the arguments for `uvicorn` worker
|
||||
|
|
@ -427,6 +428,9 @@ class ProxyInitializationHelpers:
|
|||
"port": port,
|
||||
"server_header": False,
|
||||
"timeout_graceful_shutdown": GracefulShutdownManager.get_timeout(),
|
||||
"h11_max_incomplete_event_size": (
|
||||
max_request_header_size if max_request_header_size is not None else DEFAULT_MAX_REQUEST_HEADER_SIZE
|
||||
),
|
||||
}
|
||||
if log_config is not None:
|
||||
print_startup_message(f"Using log_config: {log_config}")
|
||||
|
|
@ -680,6 +684,7 @@ class ProxyInitializationHelpers:
|
|||
ssl_keyfile_path: str,
|
||||
max_requests_before_restart: int | None = None,
|
||||
max_requests_before_restart_jitter: int | None = None,
|
||||
max_request_header_size: int | None = None,
|
||||
):
|
||||
"""
|
||||
Run litellm with `gunicorn`
|
||||
|
|
@ -754,7 +759,7 @@ class ProxyInitializationHelpers:
|
|||
gunicorn_options: Final = {
|
||||
"bind": f"{host}:{port}",
|
||||
"workers": num_workers, # default is 1
|
||||
"worker_class": build_graceful_shutdown_uvicorn_worker(graceful_shutdown_timeout),
|
||||
"worker_class": build_graceful_shutdown_uvicorn_worker(graceful_shutdown_timeout, max_request_header_size),
|
||||
"graceful_timeout": math.ceil(graceful_shutdown_timeout) + GRACEFUL_SHUTDOWN_TEARDOWN_HEADROOM_SECONDS,
|
||||
"preload": True, # Add the preload flag,
|
||||
"accesslog": "-", # Log to stdout
|
||||
|
|
@ -1108,6 +1113,18 @@ class ProxyInitializationHelpers:
|
|||
help="Set the uvicorn keepalive timeout in seconds (uvicorn timeout_keep_alive parameter)",
|
||||
envvar="KEEPALIVE_TIMEOUT",
|
||||
)
|
||||
@click.option(
|
||||
"--max_request_header_size",
|
||||
default=None,
|
||||
show_default=f"{DEFAULT_MAX_REQUEST_HEADER_SIZE} bytes with h11",
|
||||
type=click.IntRange(min=1),
|
||||
help=(
|
||||
"Maximum bytes buffered for an incomplete HTTP request header block. "
|
||||
"Applies when uvicorn or --run_gunicorn uses h11. Parser selection is unchanged; httptools ignores this setting. "
|
||||
"Ignored under --run_hypercorn / --run_granian."
|
||||
),
|
||||
envvar="MAX_REQUEST_HEADER_SIZE",
|
||||
)
|
||||
@click.option(
|
||||
"--timeout_worker_healthcheck",
|
||||
default=None,
|
||||
|
|
@ -1253,6 +1270,7 @@ def run_server(
|
|||
check_migrations_against_existing_data: bool,
|
||||
keepalive_timeout,
|
||||
timeout_worker_healthcheck,
|
||||
max_request_header_size: int | None,
|
||||
max_requests_before_restart: int | None,
|
||||
max_requests_before_restart_jitter: int | None,
|
||||
limit_concurrency: int | None,
|
||||
|
|
@ -1747,6 +1765,7 @@ def run_server(
|
|||
log_config=log_config,
|
||||
keepalive_timeout=keepalive_timeout,
|
||||
timeout_worker_healthcheck=(timeout_worker_healthcheck if running_uvicorn else None),
|
||||
max_request_header_size=max_request_header_size,
|
||||
)
|
||||
# Optional: recycle uvicorn workers after N requests
|
||||
if max_requests_before_restart is not None:
|
||||
|
|
@ -1821,6 +1840,7 @@ def run_server(
|
|||
ssl_keyfile_path=ssl_keyfile_path,
|
||||
max_requests_before_restart=max_requests_before_restart,
|
||||
max_requests_before_restart_jitter=max_requests_before_restart_jitter,
|
||||
max_request_header_size=max_request_header_size,
|
||||
)
|
||||
elif run_hypercorn is True:
|
||||
ProxyInitializationHelpers.init_hypercorn_server(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
from uvicorn.workers import UvicornWorker
|
||||
|
||||
from litellm.constants import DEFAULT_MAX_REQUEST_HEADER_SIZE
|
||||
|
||||
def build_graceful_shutdown_uvicorn_worker(timeout_graceful_shutdown: float) -> type[UvicornWorker]:
|
||||
|
||||
def build_graceful_shutdown_uvicorn_worker(
|
||||
timeout_graceful_shutdown: float,
|
||||
max_request_header_size: int | None = None,
|
||||
) -> type[UvicornWorker]:
|
||||
class GracefulShutdownUvicornWorker(UvicornWorker):
|
||||
def __init__(self, *args: object) -> None:
|
||||
self.CONFIG_KWARGS = {**UvicornWorker.CONFIG_KWARGS, "timeout_graceful_shutdown": timeout_graceful_shutdown}
|
||||
self.CONFIG_KWARGS = {
|
||||
**UvicornWorker.CONFIG_KWARGS,
|
||||
"timeout_graceful_shutdown": timeout_graceful_shutdown,
|
||||
"h11_max_incomplete_event_size": (
|
||||
max_request_header_size if max_request_header_size is not None else DEFAULT_MAX_REQUEST_HEADER_SIZE
|
||||
),
|
||||
}
|
||||
super().__init__(*args)
|
||||
|
||||
return GracefulShutdownUvicornWorker
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ proxy-dev = [
|
|||
"azure-identity==1.25.2",
|
||||
"a2a-sdk==1.1.0",
|
||||
"backports-zstd==1.8.0; python_version < '3.14'",
|
||||
"httptools>=0.7.1,<0.8",
|
||||
]
|
||||
ci = [
|
||||
"psutil==7.2.2",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import asyncio
|
||||
import gc
|
||||
import importlib
|
||||
import inspect
|
||||
|
|
@ -30,7 +31,7 @@ from uvicorn.importer import import_from_string
|
|||
|
||||
import litellm
|
||||
from litellm._logging import turn_on_json
|
||||
from litellm.constants import GRACEFUL_SHUTDOWN_TEARDOWN_HEADROOM_SECONDS
|
||||
from litellm.constants import DEFAULT_MAX_REQUEST_HEADER_SIZE, GRACEFUL_SHUTDOWN_TEARDOWN_HEADROOM_SECONDS
|
||||
from litellm.proxy import proxy_cli
|
||||
from litellm.proxy.proxy_cli import ProxyInitializationHelpers, run_server
|
||||
from litellm.proxy.shutdown.graceful_shutdown_manager import DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT
|
||||
|
|
@ -39,12 +40,114 @@ from litellm.proxy.shutdown.graceful_shutdown_manager import DEFAULT_GRACEFUL_SH
|
|||
@pytest.fixture(autouse=True)
|
||||
def fork_reservation():
|
||||
"""Reserving is irreversible: it would forbid native routes in this pytest worker for good"""
|
||||
with patch( # test-quality-ok: process-global native state, a real reservation would poison every later test in the worker
|
||||
"litellm.rust_bridge.fork_guard.reserve_process_for_forking"
|
||||
) as reserve:
|
||||
with (
|
||||
patch( # test-quality-ok: process-global native state, a real reservation would poison every later test in the worker
|
||||
"litellm.rust_bridge.fork_guard.reserve_process_for_forking"
|
||||
) as reserve
|
||||
):
|
||||
yield reserve
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cli_value", "env_value", "expected"),
|
||||
[(None, None, None), (None, "131072", 131072), ("65536", "131072", 65536)],
|
||||
)
|
||||
def test_max_request_header_size_cli_precedence(
|
||||
monkeypatch: pytest.MonkeyPatch, cli_value: str | None, env_value: str | None, expected: int | None
|
||||
) -> None:
|
||||
monkeypatch.delenv("MAX_REQUEST_HEADER_SIZE", raising=False)
|
||||
if env_value is not None:
|
||||
monkeypatch.setenv("MAX_REQUEST_HEADER_SIZE", env_value)
|
||||
args: Final = [] if cli_value is None else ["--max_request_header_size", cli_value]
|
||||
with run_server.make_context("litellm", args) as context:
|
||||
assert context.params["max_request_header_size"] == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["0", "-1", "invalid"])
|
||||
@pytest.mark.parametrize("source", ["cli", "env"])
|
||||
def test_max_request_header_size_rejects_invalid_values(
|
||||
monkeypatch: pytest.MonkeyPatch, value: str, source: str
|
||||
) -> None:
|
||||
monkeypatch.delenv("MAX_REQUEST_HEADER_SIZE", raising=False)
|
||||
if source == "env":
|
||||
monkeypatch.setenv("MAX_REQUEST_HEADER_SIZE", value)
|
||||
args: Final = ["--max_request_header_size", value] if source == "cli" else []
|
||||
with pytest.raises(click.BadParameter):
|
||||
run_server.make_context("litellm", args)
|
||||
|
||||
|
||||
def _request_header_test_config(server: str, limit: int | None) -> uvicorn.Config:
|
||||
from starlette.responses import PlainTextResponse
|
||||
|
||||
app: Final = PlainTextResponse("ok")
|
||||
if server == "uvicorn":
|
||||
args: Final = ProxyInitializationHelpers.get_default_unvicorn_init_args(
|
||||
"127.0.0.1", 4000, max_request_header_size=limit
|
||||
)
|
||||
return uvicorn.Config(**{**args, "app": app, "log_config": None})
|
||||
|
||||
import gunicorn.config
|
||||
import gunicorn.glogging
|
||||
|
||||
from litellm.proxy.shutdown.graceful_uvicorn_worker import build_graceful_shutdown_uvicorn_worker
|
||||
|
||||
gunicorn_config: Final = gunicorn.config.Config()
|
||||
worker: Final = build_graceful_shutdown_uvicorn_worker(7.5, limit)(
|
||||
0, os.getpid(), [], MagicMock(), 30, gunicorn_config, gunicorn.glogging.Logger(gunicorn_config)
|
||||
)
|
||||
worker.config.app = app
|
||||
return worker.config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("server", ["uvicorn", "gunicorn"])
|
||||
@pytest.mark.parametrize("limit", [None, DEFAULT_MAX_REQUEST_HEADER_SIZE, 128 * 1024])
|
||||
def test_request_header_size_preserves_parser_selection(server: str, limit: int | None) -> None:
|
||||
config: Final = _request_header_test_config(server, limit)
|
||||
assert config.http == "auto"
|
||||
assert config.h11_max_incomplete_event_size == (limit if limit is not None else DEFAULT_MAX_REQUEST_HEADER_SIZE)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("server", ["uvicorn", "gunicorn"])
|
||||
@pytest.mark.parametrize("parser", ["h11", "httptools"])
|
||||
@pytest.mark.parametrize(
|
||||
("limit", "header_bytes", "expected_status"),
|
||||
[(16384, 16384, 200), (16384, 16896, 400), (16384, 39936, 400), (131072, 39936, 200), (131072, 131073, 400)],
|
||||
)
|
||||
async def test_segmented_request_headers(
|
||||
monkeypatch: pytest.MonkeyPatch, server: str, parser: str, limit: int, header_bytes: int, expected_status: int
|
||||
) -> None:
|
||||
from uvicorn.server import ServerState
|
||||
|
||||
from uvicorn.protocols.http.h11_impl import H11Protocol
|
||||
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
||||
|
||||
selected_protocol: Final = H11Protocol if parser == "h11" else HttpToolsProtocol
|
||||
monkeypatch.setattr("uvicorn.protocols.http.auto.AutoHTTPProtocol", selected_protocol)
|
||||
config: Final = _request_header_test_config(server, limit)
|
||||
config.load()
|
||||
assert config.http_protocol_class is selected_protocol
|
||||
protocol: Final = config.http_protocol_class(config, ServerState(), {}, _loop=asyncio.get_running_loop())
|
||||
transport: Final = MagicMock(spec=asyncio.Transport)
|
||||
transport.get_extra_info.return_value = None
|
||||
transport.is_closing.return_value = False
|
||||
protocol.connection_made(transport)
|
||||
prefix: Final = b"GET /health/liveliness HTTP/1.1\r\nHost: localhost\r\nX-Large: "
|
||||
pending_headers: Final = prefix + b"x" * (header_bytes - len(prefix))
|
||||
try:
|
||||
for offset in range(0, len(pending_headers), 4096):
|
||||
protocol.data_received(pending_headers[offset : offset + 4096])
|
||||
if transport.close.called:
|
||||
break
|
||||
if not transport.close.called:
|
||||
protocol.data_received(b"\r\n\r\n")
|
||||
await asyncio.gather(*protocol.tasks)
|
||||
response: Final = b"".join(call.args[0] for call in transport.write.call_args_list)
|
||||
assert response.startswith(f"HTTP/1.1 {expected_status if parser == 'h11' else 200} ".encode())
|
||||
finally:
|
||||
protocol.connection_lost(None)
|
||||
|
||||
|
||||
@pytest.mark.xdist_group("proxy_cli")
|
||||
class TestProxyInitializationHelpers:
|
||||
@patch("importlib.metadata.version")
|
||||
|
|
@ -1550,6 +1653,7 @@ class TestProxyInitializationHelpers:
|
|||
log_config=None,
|
||||
keepalive_timeout=30,
|
||||
timeout_worker_healthcheck=None,
|
||||
max_request_header_size=None,
|
||||
)
|
||||
mock_uvicorn_run.assert_called_once()
|
||||
|
||||
|
|
@ -1626,6 +1730,7 @@ class TestProxyInitializationHelpers:
|
|||
log_config=None,
|
||||
keepalive_timeout=None,
|
||||
timeout_worker_healthcheck=15,
|
||||
max_request_header_size=None,
|
||||
)
|
||||
|
||||
@patch("uvicorn.run")
|
||||
|
|
@ -2092,7 +2197,10 @@ class TestProxyInitializationHelpers:
|
|||
assert captured["options"]["max_requests_jitter"] == 50
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="gunicorn server path skips Windows")
|
||||
def test_gunicorn_workers_bound_graceful_shutdown_by_graceful_shutdown_timeout(self, monkeypatch):
|
||||
@pytest.mark.parametrize("max_request_header_size", [DEFAULT_MAX_REQUEST_HEADER_SIZE, 128 * 1024])
|
||||
def test_gunicorn_workers_bound_graceful_shutdown_by_graceful_shutdown_timeout(
|
||||
self, monkeypatch, max_request_header_size: int
|
||||
):
|
||||
pytest.importorskip("gunicorn")
|
||||
import gunicorn.config
|
||||
import gunicorn.glogging
|
||||
|
|
@ -2117,6 +2225,7 @@ class TestProxyInitializationHelpers:
|
|||
num_workers=2,
|
||||
ssl_certfile_path=None,
|
||||
ssl_keyfile_path=None,
|
||||
max_request_header_size=max_request_header_size,
|
||||
)
|
||||
|
||||
for name in ("uvicorn.error", "uvicorn.access"):
|
||||
|
|
@ -2132,6 +2241,7 @@ class TestProxyInitializationHelpers:
|
|||
assert worker.config.timeout_graceful_shutdown == 7.5
|
||||
assert worker.config.loop == UvicornWorker.CONFIG_KWARGS["loop"]
|
||||
assert worker.config.http == UvicornWorker.CONFIG_KWARGS["http"]
|
||||
assert worker.config.h11_max_incomplete_event_size == max_request_header_size
|
||||
assert captured["options"]["graceful_timeout"] == 8 + GRACEFUL_SHUTDOWN_TEARDOWN_HEADROOM_SECONDS
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="gunicorn server path skips Windows")
|
||||
|
|
|
|||
45
uv.lock
generated
45
uv.lock
generated
|
|
@ -3715,6 +3715,49 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148, upload-time = "2026-06-26T10:13:54.985Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httptools"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
|
|
@ -5203,6 +5246,7 @@ proxy-dev = [
|
|||
{ name = "a2a-sdk" },
|
||||
{ name = "azure-identity" },
|
||||
{ name = "backports-zstd", marker = "python_full_version < '3.14'" },
|
||||
{ name = "httptools" },
|
||||
{ name = "hypercorn" },
|
||||
{ name = "litellm-admin-mcp", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "opentelemetry-api" },
|
||||
|
|
@ -5431,6 +5475,7 @@ proxy-dev = [
|
|||
{ name = "a2a-sdk", specifier = "==1.1.0" },
|
||||
{ name = "azure-identity", specifier = "==1.25.2" },
|
||||
{ name = "backports-zstd", marker = "python_full_version < '3.14'", specifier = "==1.8.0" },
|
||||
{ name = "httptools", specifier = ">=0.7.1,<0.8" },
|
||||
{ name = "hypercorn", specifier = "==0.17.3" },
|
||||
{ name = "litellm-admin-mcp", marker = "python_full_version >= '3.12'", url = "https://github.com/BerriAI/liteadmin-mcp/archive/31fb096904ae05684c0779110a93b135b5601182.tar.gz" },
|
||||
{ name = "opentelemetry-api", specifier = "==1.45.1" },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue