mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-16 04:29:31 +00:00
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Deploy main to gateway-dev / dispatch (push) Waiting to run
Publish lint base counts / publish (basedpyright, scripts/type_check_gate.py) (push) Waiting to run
Publish lint base counts / publish (openapi-docs, scripts/openapi_docs_gate.py) (push) Waiting to run
Publish lint base counts / publish (ruff-strict, scripts/ruff_strict_gate.py) (push) Waiting to run
Publish lint base counts / publish (test-quality, scripts/test_quality_gate.py) (push) Waiting to run
Publish lint base counts / publish (type-discipline, scripts/type_discipline_gate.py) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
unit / lens-python-310 (push) Blocked by required conditions
unit / caching-local (push) Blocked by required conditions
unit / core-utils (push) Blocked by required conditions
unit / endpoints (push) Blocked by required conditions
unit / integrations (push) Blocked by required conditions
unit / llms-bedrock (push) Blocked by required conditions
unit / llms-providers (push) Blocked by required conditions
unit / mcp-elicitation (push) Blocked by required conditions
unit / proxy-feature-endpoints (push) Blocked by required conditions
unit / router (push) Blocked by required conditions
unit / endpoints-and-responses (push) Blocked by required conditions
unit / enterprise-routing (push) Blocked by required conditions
unit / guardrails-hooks (push) Blocked by required conditions
unit / logging-misc (push) Blocked by required conditions
unit / root (push) Blocked by required conditions
unit / proxy-utils (push) Blocked by required conditions
unit / assert-shard-coverage (push) Waiting to run
unit / enterprise-managed-files (push) Blocked by required conditions
unit / proxy-server (push) Blocked by required conditions
unit / rust-bridge (push) Waiting to run
unit / helm (push) Waiting to run
unit / OpenAI 2.20.0 compatibility (push) Waiting to run
unit / OpenAI 3.0.0 compatibility (push) Waiting to run
unit / coverage (push) Blocked by required conditions
unit / unit passed (push) Blocked by required conditions
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
Terraform Provider / gofmt, vet, build, test (push) Waiting to run
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Waiting to run
unit / enterprise-package (push) Blocked by required conditions
unit / enterprise-repositories-secrets (push) Blocked by required conditions
unit / llms-anthropic (push) Blocked by required conditions
unit / llms-openai-meta (push) Blocked by required conditions
unit / llms-vertex-ai (push) Blocked by required conditions
unit / mcp-server (push) Blocked by required conditions
unit / proxy-auth (push) Blocked by required conditions
unit / proxy-endpoints (push) Blocked by required conditions
unit / proxy-extras (push) Blocked by required conditions
unit / proxy-hooks-client (push) Blocked by required conditions
unit / proxy-infra (push) Blocked by required conditions
unit / proxy-infra-root (push) Blocked by required conditions
unit / responses-caching-types (push) Blocked by required conditions
unit / rust-bridge-harness (push) Blocked by required conditions
unit / auth-checks (push) Blocked by required conditions
unit / budgets (push) Blocked by required conditions
unit / custom-logging (push) Blocked by required conditions
unit / db-and-spend (push) Blocked by required conditions
unit / jwt-and-keys (push) Blocked by required conditions
unit / key-generation (push) Blocked by required conditions
unit / mcp-oauth (push) Blocked by required conditions
unit / proxy-runtime (push) Blocked by required conditions
unit / proxy-server-core (push) Blocked by required conditions
unit / ui-unit (push) Waiting to run
unit / docs (push) Waiting to run
unit / OpenAI 3.25.0 compatibility (push) Waiting to run
unit / redis-py 5.3.1 compatibility (push) Blocked by required conditions
unit / redis-py 6.4.0 compatibility (push) Blocked by required conditions
unit / redis-py 7.4.1 compatibility (push) Blocked by required conditions
unit / redis-py 8.0.1 compatibility (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run
* fix(router): keep the prompt caching affinity pin alive as long as the prefix's cache_control ttl A prefix marked cache_control ttl 1h lost its pinned deployment after the fixed 300 s, so the next request went to another deployment and paid a cold cache. The pin TTL now follows the longest ephemeral breakpoint in the cacheable prefix, tools included: 3600 s for a 1h breakpoint and 300 s otherwise. * fix(router): give prompt cache pins their own tier and honour any declared ttl Pins now live in a DualCache of their own instead of the router's 200-slot in-memory tier, so an hour-long pin no longer evicts cooldowns and rate windows. The pin lifetime follows any <n>s|m|h cache_control ttl, floored at 300 s, not only the literal "1h". * fix(router): hash the pin ttl off the event loop and flush prompt cache pins with the router cache Compute the cache_control ttl scan through offload_token_count beside the prefix hash, clear the dedicated pin cache in Router.flush_cache, and point the order-fallback and priority-group pin tests at the pin cache they read * fix(router): pin 1h prefixes only on 1h-capable models, cap the pin at 1h, and give pins 10k in-memory slots * fix(router): match the 1h pin gate to what Bedrock forwards Bedrock deployments now decide the 1h pin with is_claude_4_5_on_bedrock, the same check that decides whether the 1h ttl reaches Bedrock, so an application inference profile ARN keeps a 5 minute pin whatever its base_model prices * test(router): give the anthropic_messages pin fixture its provider * fix(router): let each pin write set its own lifetime and require a positive 1h price In a group mixing 1h and 5m deployments the in-memory pin kept the lifetime of its first write, so a 5m deployment could hold an hour-long pin. Each write now clears the in-memory entry first, matching Redis. The Bedrock gate uses supports_bedrock_prompt_cache_ttl after #46058, and a zero 1h price no longer counts as 1h support. * test(router): integration cells for the one-hour prompt cache pin lifetime Cover every surface the pin lifetime touches: chat and /v1/messages through both SDKs, the responses bridge, response-cache hits, each provider family's 1h gate, malformed and duplicated ttl values, rewrites of one prefix, mixed and cross-group routing, concurrency, every Redis attach path, the in-memory slot pressure without Redis reads, a Redis outage and a killed worker * fix(caching): bound the in-memory expiry heap and count prompt cache pins in the memory debug routes * test(integration): cover prompt cache pins in the memory debug routes and the bounded expiration heap * fix(caching): serialize in-memory cache writes so a heap rebuild cannot lose another thread's expiry entry * fix(caching): keep the in-memory cache bounded when entries have no usable expiry * test(caching): cover the response cache bound when cached entries have no usable ttl --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
200 lines
7.2 KiB
Python
200 lines
7.2 KiB
Python
import json
|
|
import os
|
|
import socket
|
|
from collections.abc import Callable, Iterator, Mapping
|
|
from dataclasses import asdict
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from litellm.proxy import proxy_server
|
|
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
|
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
|
from litellm.proxy.bug_report_config import build_proxy_bug_report
|
|
from litellm.proxy.common_utils.debug_utils import (
|
|
PSUTIL_MISSING_ERROR,
|
|
_ProcFilesystemProcess,
|
|
_summary_process_memory,
|
|
get_memory_summary,
|
|
)
|
|
from litellm.proxy.common_utils.debug_utils import router as debug_router
|
|
from litellm.router import Router
|
|
from litellm.router_utils.prompt_caching_cache import PromptCachingCache
|
|
|
|
PAGE_SIZE = 4096
|
|
STATM_SIZE_PAGES = 100_000
|
|
STATM_RESIDENT_PAGES = 30_000
|
|
MEMINFO_TOTAL_KB = 1_000_000
|
|
|
|
|
|
@pytest.fixture
|
|
def proc_process(tmp_path: Path) -> _ProcFilesystemProcess:
|
|
statm = tmp_path / "statm"
|
|
statm.write_text(f"{STATM_SIZE_PAGES} {STATM_RESIDENT_PAGES} 5000 1 0 20000 0\n")
|
|
meminfo = tmp_path / "meminfo"
|
|
meminfo.write_text(
|
|
f"MemTotal: {MEMINFO_TOTAL_KB} kB\nMemFree: 400000 kB\nMemAvailable: 600000 kB\n"
|
|
)
|
|
return _ProcFilesystemProcess(statm_path=str(statm), meminfo_path=str(meminfo), page_size=PAGE_SIZE)
|
|
|
|
|
|
def test_proc_filesystem_process_reads_resident_and_virtual_bytes_from_statm(
|
|
proc_process: _ProcFilesystemProcess,
|
|
) -> None:
|
|
memory_info = proc_process.memory_info()
|
|
|
|
assert memory_info.rss == STATM_RESIDENT_PAGES * PAGE_SIZE
|
|
assert memory_info.vms == STATM_SIZE_PAGES * PAGE_SIZE
|
|
|
|
|
|
def test_proc_filesystem_process_reports_share_of_meminfo_total(proc_process: _ProcFilesystemProcess) -> None:
|
|
expected_percent = STATM_RESIDENT_PAGES * PAGE_SIZE / (MEMINFO_TOTAL_KB * 1024) * 100
|
|
|
|
assert proc_process.memory_percent() == pytest.approx(expected_percent)
|
|
|
|
|
|
def test_summary_reports_rss_from_the_proc_filesystem(proc_process: _ProcFilesystemProcess) -> None:
|
|
memory, health_status = _summary_process_memory(proc_process)
|
|
|
|
assert memory["ram_usage_mb"] == round(STATM_RESIDENT_PAGES * PAGE_SIZE / (1024 * 1024), 2)
|
|
assert memory["system_memory_percent"] == pytest.approx(12.0)
|
|
assert health_status == "healthy"
|
|
assert "error" not in memory
|
|
|
|
|
|
def test_summary_without_any_memory_source_names_psutil_and_reports_no_rss() -> None:
|
|
memory, health_status = _summary_process_memory(None)
|
|
|
|
assert memory == {"error": PSUTIL_MISSING_ERROR}
|
|
assert health_status == "healthy"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_memory_summary_names_the_host_and_worker_that_answered() -> None:
|
|
summary = await get_memory_summary(UserAPIKeyAuth())
|
|
|
|
assert summary["hostname"] == socket.gethostname()
|
|
assert summary["worker_pid"] == os.getpid()
|
|
assert summary["memory"]["ram_usage_mb"] > 0
|
|
|
|
|
|
HOSTILE_CONFIG: Mapping[str, object] = {
|
|
"model_list": [
|
|
{
|
|
"model_name": "acme-prod-gpt4",
|
|
"litellm_params": {
|
|
"model": "azure/acme-gpt4o-deployment",
|
|
"api_base": "https://acme-eastus.openai.azure.com",
|
|
"api_key": "sk-live-secret-1",
|
|
},
|
|
}
|
|
],
|
|
"litellm_settings": {"drop_params": True, "callbacks": ["langfuse", "acme_hooks.audit_logger"]},
|
|
}
|
|
|
|
HOSTILE_GENERAL_SETTINGS: Mapping[str, object] = {
|
|
"master_key": "sk-live-secret-master",
|
|
"database_url": "postgres://user:hunter2@10.0.0.7/litellm",
|
|
"store_model_in_db": True,
|
|
}
|
|
|
|
HOSTILE_STRINGS = ("acme", "sk-live-secret", "hunter2", "10.0.0.7", "azure.com")
|
|
|
|
|
|
@pytest.fixture
|
|
def hostile_proxy_config(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
|
previous_config = proxy_server.proxy_config.get_config_state()
|
|
proxy_server.proxy_config.update_config_state(config=HOSTILE_CONFIG)
|
|
monkeypatch.setattr(proxy_server, "general_settings", dict(HOSTILE_GENERAL_SETTINGS))
|
|
yield
|
|
proxy_server.proxy_config.update_config_state(config=previous_config)
|
|
|
|
|
|
def _debug_client(caller: UserAPIKeyAuth) -> TestClient:
|
|
app = FastAPI()
|
|
app.include_router(debug_router)
|
|
app.dependency_overrides[user_api_key_auth] = lambda: caller
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"caller",
|
|
[
|
|
UserAPIKeyAuth(),
|
|
UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER),
|
|
UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("hostile_proxy_config")
|
|
def test_debug_report_refuses_everyone_but_proxy_admins(caller: UserAPIKeyAuth) -> None:
|
|
response = _debug_client(caller).get("/debug/report")
|
|
|
|
assert response.status_code == 403, response.text
|
|
assert "litellm_version" not in response.text
|
|
|
|
|
|
@pytest.mark.usefixtures("hostile_proxy_config")
|
|
def test_debug_report_returns_what_the_bug_report_link_carries_and_nothing_from_the_operator() -> None:
|
|
response = _debug_client(UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)).get("/debug/report")
|
|
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == json.loads(json.dumps(asdict(build_proxy_bug_report(RuntimeError("boom")).environment)))
|
|
assert response.json()["config_lines"] == [
|
|
"general_settings.store_model_in_db = true",
|
|
"litellm_settings.drop_params = true",
|
|
"litellm_settings.callbacks = [langfuse]",
|
|
"model_list[*].provider = [azure]",
|
|
]
|
|
assert not any(hostile in response.text for hostile in HOSTILE_STRINGS), response.text
|
|
|
|
|
|
PINNED_PREFIXES = 3
|
|
|
|
|
|
@pytest.fixture
|
|
def router_with_pins(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
router = Router(model_list=[{"model_name": "claude", "litellm_params": {"model": "anthropic/claude-sonnet-4-5"}}])
|
|
pins = PromptCachingCache(cache=router.prompt_caching_pin_cache)
|
|
for prefix in range(PINNED_PREFIXES):
|
|
pins.add_model_id(
|
|
model_id=f"deployment-{prefix}",
|
|
messages=[
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": f"prefix {prefix} " * 300, "cache_control": {"type": "ephemeral"}}
|
|
],
|
|
}
|
|
],
|
|
tools=None,
|
|
)
|
|
monkeypatch.setattr(proxy_server, "llm_router", router)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("path", "pin_count", "expected"),
|
|
[
|
|
(
|
|
"/memory-usage-in-mem-cache",
|
|
lambda body: body["num_items_in_prompt_caching_pin_cache"],
|
|
2 * PINNED_PREFIXES,
|
|
),
|
|
("/memory-usage-in-mem-cache-items", lambda body: len(body["prompt_caching_pin_cache"]), PINNED_PREFIXES),
|
|
("/debug/memory/summary", lambda body: body["caches"]["breakdown"]["prompt_caching_pins"]["count"], PINNED_PREFIXES),
|
|
(
|
|
"/debug/memory/details?include_process_info=false",
|
|
lambda body: body["cache_memory"]["prompt_caching_pin_cache"]["num_items"],
|
|
PINNED_PREFIXES,
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("router_with_pins")
|
|
def test_memory_routes_count_prompt_caching_pins(
|
|
path: str, pin_count: Callable[[dict[str, object]], int], expected: int
|
|
) -> None:
|
|
response = _debug_client(UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)).get(path)
|
|
|
|
assert response.status_code == 200
|
|
assert pin_count(response.json()) == expected
|