litellm/tests/unit/proxy/lens/test_endpoints.py
moe-berri 4ed0267b5a
fix(lens): prevent progress updates from starving analysis budget reservations (#45432)
* fix(lens): serialize analysis budget reservations with progress updates

* test(lens): verify budget reservations block competing progress writes
2026-10-08 21:08:55 +00:00

1169 lines
48 KiB
Python

import asyncio
from collections.abc import AsyncGenerator, Callable, Mapping
from contextlib import asynccontextmanager
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from typing import Final
import httpx
import pytest
from fastapi import HTTPException
from pydantic import TypeAdapter, ValidationError
import litellm
from litellm import Router
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.lens.endpoints import (
claim_due,
get_signals,
list_agents,
put_signals,
read_reviews,
result,
run_settings,
run_window,
trace_findings,
trace_signal_statuses,
user_scope,
validate_model,
validate_signal_model,
watchable,
watching,
worker_supports_model,
)
from litellm.proxy.lens.endpoints import (
sample as worker_sample,
)
from litellm.proxy.lens.models import (
ActivitySelection,
Coverage,
Execution,
Lens,
LensSettings,
Result,
ReviewVersion,
RunAssessment,
RunRequest,
Sample,
Scope,
TraceFindingsRequest,
TraceIdentity,
Worker,
)
from litellm.proxy.lens.repository import Database, DueLens, Row
from litellm.proxy.lens.signals import SignalConfig, StoredTraceSignal
from litellm.proxy.lens.state import claim_job, queue_job, replace_job
from litellm.rust_bridge.trace.generated.models import ExecutionRow, LensSampleParams
from litellm.rust_bridge.trace.storage import ClickHouseStorage
from litellm.tracing.remote import RemoteTraceStore
from tests.unit.proxy.lens.test_state import NOW, lens, worker
def execution(identity: str) -> Execution:
return Execution(
id=identity, source="traces", trace_id=identity, team_id="", name=identity, start_time="", span_count=1
)
class ResultDatabase:
def __init__(self, stored: Lens) -> None:
self.stored = stored
self.completed: tuple[ReviewVersion, ...] = ()
@asynccontextmanager
async def transaction(self) -> AsyncGenerator[Database]:
yield self
async def query_raw(self, query: str, *args: object) -> tuple[Row, ...]:
if query.startswith("SELECT data FROM"):
return (Row(data=self.stored.model_dump(mode="json")),)
payload: Final = args[0]
assert isinstance(payload, str)
self.stored = Lens.model_validate_json(payload)
return (Row(data=1),)
async def execute_raw(self, query: str, *args: object) -> int:
from pydantic import TypeAdapter
payload: Final = args[2]
assert isinstance(payload, str)
self.completed = TypeAdapter(tuple[ReviewVersion, ...]).validate_json(payload)
return len(self.completed)
class SignalStatusDatabase:
def __init__(self, config: SignalConfig, rows: Mapping[str, StoredTraceSignal]) -> None:
self.config: Final = config
self.rows: Final = rows
self.saved: Final[asyncio.Queue[tuple[object, ...]]] = asyncio.Queue()
async def query_raw(self, query: str, *args: object) -> object:
if '"LiteLLM_LensSignalConfig"' in query:
return ({"data": self.config.model_dump(mode="json")},)
payload: Final = args[0]
assert isinstance(payload, str)
requested: Final = TypeAdapter(tuple[TraceIdentity, ...]).validate_json(payload)
return tuple(
{"data": row.model_dump(mode="json")}
for identity in requested
if (row := self.rows.get(identity.trace_id)) is not None
)
async def execute_raw(self, query: str, *args: object) -> int:
await self.saved.put(args)
return 1
def signal_router() -> Router:
return Router(
model_list=[
{
"model_name": "decision",
"litellm_params": {"model": "openai/test-decision", "api_key": "test-key"},
"model_info": {"mode": "evaluation"},
},
{
"model_name": "chat",
"litellm_params": {"model": "openai/test-chat", "api_key": "test-key"},
"model_info": {"mode": "chat"},
},
]
)
@pytest.mark.asyncio
@pytest.mark.parametrize("change", ("cancelled", "expired", "reclaimed", "reassigned"))
async def test_result_cannot_commit_after_losing_ownership_during_evidence_validation(
monkeypatch: pytest.MonkeyPatch, change: str
) -> None:
from litellm.proxy import proxy_server
from tests.unit.proxy.lens.test_state import finding
claimed: Final = claim_job(queue_job(lens(), NOW, "job"), worker(), NOW)
active: Final = claimed.jobs[0].model_copy(
update={
"lease_until": datetime.max.replace(tzinfo=timezone.utc),
"sample": Sample(executions=(execution("run"),), eligible=1),
}
)
competing: Final = active.model_copy(
update={
"status": "cancelled" if change == "cancelled" else "running",
"lease_until": NOW if change == "expired" else active.lease_until,
"attempts": 2 if change == "reclaimed" else 1,
"worker_id": "other-worker" if change == "reassigned" else active.worker_id,
}
)
db: Final = ResultDatabase(replace_job(claimed, active))
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
async def evidence(request: httpx.Request) -> httpx.Response:
db.stored = replace_job(db.stored, competing)
return httpx.Response(200, json={"data": [{"count": 1}]})
async with httpx.AsyncClient(base_url="http://lens.test", transport=httpx.MockTransport(evidence)) as client:
saved: Final = await result(
"lens",
"job",
Result(
coverage=Coverage(screened=1, investigated=1),
findings=(finding("run"),),
assessments=(RunAssessment(execution_id="run"),),
review_versions=(ReviewVersion(execution_id="run", content_version="v1"),),
),
worker(),
ClickHouseStorage(RemoteTraceStore(client)),
)
assert saved.jobs[0] == competing
assert saved.findings == ()
assert db.completed == ()
@pytest.mark.asyncio
async def test_worker_sample_retries_oversized_pages_and_keeps_all_executions(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from litellm.proxy import proxy_server
claimed: Final = claim_job(queue_job(lens(), NOW, "job"), worker(), NOW)
active: Final = claimed.jobs[0].model_copy(update={"lease_until": datetime.max.replace(tzinfo=timezone.utc)})
db: Final = ResultDatabase(replace_job(claimed, active))
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
rows: Final = tuple(
ExecutionRow(
source="traces",
trace_id=trace_id,
team_id="team",
name=trace_id,
start_time="",
span_count=1,
root_seen=1,
eligible=3,
selected=3,
selection_key=trace_id,
)
for trace_id in ("trace-1", "trace-2", "trace-3")
)
class SampleStorage:
def __init__(self) -> None:
self.limits: tuple[int, ...] = ()
async def lens_sample(self, parameters: LensSampleParams) -> tuple[ExecutionRow, ...]:
self.limits = (*self.limits, parameters.limit)
if parameters.limit > 2_500:
raise RuntimeError("ClickHouse query exceeded the response size limit")
return rows
storage: Final = SampleStorage()
selected: Final = await worker_sample("lens", "job", worker(), storage)
assert storage.limits == (10_000, 5_000, 2_500)
assert tuple(execution.trace_id for execution in selected.executions) == ("trace-1", "trace-2", "trace-3")
assert selected.selected == 3
@pytest.mark.asyncio
async def test_worker_sample_propagates_response_too_large_at_minimum_page_size(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from litellm.proxy import proxy_server
claimed: Final = claim_job(queue_job(lens(), NOW, "job"), worker(), NOW)
active: Final = claimed.jobs[0].model_copy(update={"lease_until": datetime.max.replace(tzinfo=timezone.utc)})
db: Final = ResultDatabase(replace_job(claimed, active))
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
class SampleStorage:
def __init__(self) -> None:
self.limits: tuple[int, ...] = ()
async def lens_sample(self, parameters: LensSampleParams) -> tuple[ExecutionRow, ...]:
self.limits = (*self.limits, parameters.limit)
raise RuntimeError("ClickHouse query exceeded the response size limit")
storage: Final = SampleStorage()
with pytest.raises(RuntimeError, match="response size limit"):
await worker_sample("lens", "job", worker(), storage)
assert storage.limits == (10_000, 5_000, 2_500, 1_250, 625, 312, 156, 100)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"selected,check_id,quoted",
((False, "retries", "run"), (True, "disabled", "run"), (True, "retries", "other")),
)
async def test_checkpoint_rejects_unselected_traces_disabled_checks_and_foreign_evidence(
monkeypatch: pytest.MonkeyPatch, selected: bool, check_id: str, quoted: str
) -> None:
from litellm.proxy import proxy_server
from litellm.proxy.lens.endpoints import progress
from litellm.proxy.lens.models import Evidence, Extraction, Observation, Progress, Review
claimed: Final = claim_job(queue_job(lens(), NOW, "job"), worker(), NOW)
active: Final = claimed.jobs[0].model_copy(
update={
"lease_until": datetime.max.replace(tzinfo=timezone.utc),
"sample": Sample(executions=(execution("run"),), eligible=1) if selected else None,
}
)
stored: Final = replace_job(claimed, active)
db: Final = ResultDatabase(stored)
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
review: Final = Review(
execution_id="run",
trace_id="run",
agent="agent",
name="run",
model="test",
duration_ms=1,
at=NOW,
content_version="v1",
extraction=Extraction(
observations=(
Observation(
check_id=check_id,
summary="Failure",
evidence=(Evidence(execution_id=quoted, span_id="s", quote="failed"),),
),
)
),
)
with pytest.raises(HTTPException) as error:
await progress("lens", "job", Progress(review=review), worker())
assert error.value.status_code == 422
assert db.stored == stored
assert db.completed == ()
@pytest.mark.asyncio
@pytest.mark.parametrize("reference", ("existing", "merged"))
@pytest.mark.parametrize("foreign_kind", (False, True))
async def test_findings_cannot_merge_missing_ids_or_positive_patterns_into_issues(
reference: str, foreign_kind: bool
) -> None:
from litellm.proxy.lens.endpoints import validate_finding
from litellm.proxy.lens.state import merge_finding
from tests.unit.proxy.lens.test_state import finding
saved: Final = merge_finding(lens(), finding("old"), 1, NOW, "previous").model_copy(update={"kind": "pattern"})
identity: Final = saved.id if foreign_kind else "missing"
draft: Final = finding("new").model_copy(
update={
"existing_finding_id": identity if reference == "existing" else None,
"merged_finding_ids": (identity,) if reference == "merged" else (),
}
)
with pytest.raises(HTTPException) as error:
await validate_finding(
lens().model_copy(update={"findings": (saved,)}), Sample(executions=(), eligible=0), draft, None
)
assert error.value.status_code == 422
assert "finding must belong" in error.value.detail
@pytest.mark.asyncio
@pytest.mark.parametrize("selected", ("old-trace", "selected-without-quote"))
async def test_unchanged_rerun_does_not_rediscover_old_or_quoteless_occurrences(
monkeypatch: pytest.MonkeyPatch, selected: str
) -> None:
from litellm.proxy import proxy_server
from litellm.proxy.lens.state import merge_finding
from tests.unit.proxy.lens.test_state import finding
saved_finding: Final = merge_finding(lens(), finding("old-trace"), 1, NOW, "original-run").model_copy(
update={"occurrences": ("old-trace", "selected-without-quote")}
)
stored: Final = lens().model_copy(update={"findings": (saved_finding,)})
claimed: Final = claim_job(queue_job(stored, NOW, "job"), worker(), NOW)
active: Final = claimed.jobs[0].model_copy(
update={
"lease_until": datetime.max.replace(tzinfo=timezone.utc),
"sample": Sample(executions=(execution(selected),), eligible=1),
}
)
db: Final = ResultDatabase(replace_job(claimed, active))
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
body: Final = Result(coverage=Coverage(reused=1), assessments=(RunAssessment(execution_id=selected),))
completed: Final = await result("lens", "job", body, worker(), None)
assert completed.jobs[0].findings == ()
assert completed.findings == (saved_finding,)
assert Lens.model_validate_json(completed.model_dump_json()) == completed
assert await result("lens", "job", body, worker(), None) == completed
@pytest.mark.asyncio
async def test_completed_checkpoints_are_sealed_despite_an_unrelated_trace_failure(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from litellm.proxy import proxy_server
claimed: Final = claim_job(queue_job(lens(), NOW, "job"), worker(), NOW)
active: Final = claimed.jobs[0].model_copy(
update={
"lease_until": datetime.max.replace(tzinfo=timezone.utc),
"sample": Sample(executions=(execution("valid"), execution("failed")), eligible=2),
}
)
db: Final = ResultDatabase(replace_job(claimed, active))
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
versions: Final = (ReviewVersion(execution_id="valid", content_version="v1"),)
body: Final = Result(
coverage=Coverage(failed_tasks=1),
assessments=(RunAssessment(execution_id="valid"), RunAssessment(execution_id="failed", cannot_assess=True)),
review_versions=versions,
error="Another trace failed",
)
completed: Final = await result("lens", "job", body, worker(), None)
assert completed.jobs[0].status == "completed"
assert db.completed == versions
assert await result("lens", "job", body, worker(), None) == completed
assert db.completed == versions
@pytest.mark.parametrize(
"final_coverage,error,expected",
(
(
Coverage(eligible=2, selected=2, screened=2, partial=1, unassessable=1),
"Source unavailable during session review",
Coverage(eligible=2, selected=2, screened=2, partial=1, unassessable=1),
),
(
Coverage(eligible=2, selected=2, screened=2, investigated=1, candidates=1, partial=1),
"Source unavailable during investigation",
Coverage(eligible=2, selected=2, screened=2, investigated=1, candidates=1, partial=1),
),
(
Coverage(),
"Worker interrupted",
Coverage(eligible=2, selected=2, screened=1),
),
(Coverage(), "", Coverage()),
),
ids=("review-diagnostic", "investigation-diagnostic", "interrupted-worker", "empty-success"),
)
@pytest.mark.asyncio
@pytest.mark.parametrize("assessed", (False, True))
async def test_result_persists_final_coverage_but_keeps_progress_when_worker_is_interrupted(
monkeypatch: pytest.MonkeyPatch, final_coverage: Coverage, error: str, expected: Coverage, assessed: bool
) -> None:
from litellm.proxy import proxy_server
assigned: Final = claim_job(queue_job(lens(), NOW, "job"), worker(), NOW)
active: Final = assigned.jobs[0].model_copy(
update={
"lease_until": datetime.max.replace(tzinfo=timezone.utc),
"coverage": Coverage(eligible=2, selected=2, screened=1),
"sample": Sample(executions=(execution("run"),), eligible=1),
}
)
db: Final = ResultDatabase(replace_job(assigned, active))
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
assessments: Final = (RunAssessment(execution_id="run"),) if assessed else ()
saved: Final = await result(
"lens", "job", Result(coverage=final_coverage, error=error, assessments=assessments), worker(), None
)
assert saved == db.stored
assert saved.jobs[0].coverage == expected
assert saved.jobs[0].error == error
assert saved.jobs[0].status == ("failed" if error and not assessed else "completed")
assert saved.jobs[0].assessments == assessments
assert saved.last_scan_at == (None if error else active.end)
@pytest.mark.asyncio
async def test_result_rejects_checkpoints_for_traces_outside_frozen_sample(monkeypatch: pytest.MonkeyPatch) -> None:
from litellm.proxy import proxy_server
assigned: Final = claim_job(queue_job(lens(), NOW, "job"), worker(), NOW)
active: Final = assigned.jobs[0].model_copy(
update={
"lease_until": datetime.max.replace(tzinfo=timezone.utc),
"sample": Sample(executions=(execution("selected"),), eligible=1),
}
)
db: Final = ResultDatabase(replace_job(assigned, active))
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
with pytest.raises(HTTPException) as raised:
await result(
"lens",
"job",
Result(coverage=Coverage(), review_versions=(ReviewVersion(execution_id="outside", content_version="v1"),)),
worker(),
None,
)
assert raised.value.status_code == 422
assert db.stored.jobs[0] == active
@pytest.fixture
def analysis_router(monkeypatch: pytest.MonkeyPatch) -> Router:
from litellm.proxy import proxy_server
monkeypatch.setattr(litellm, "model_cost", {**litellm.model_cost})
router: Final = Router(
model_list=[
{
"model_name": "openai/*",
"litellm_params": {
"model": "openai/*",
"api_key": "test-key",
"input_cost_per_token": 0.001,
"output_cost_per_token": 0.002,
},
},
{
"model_name": "analysis",
"litellm_params": {
"model": "openai/test-analysis",
"api_key": "test-key",
"input_cost_per_token": 0.001,
"output_cost_per_token": 0.002,
},
},
{"model_name": "unpriced/*", "litellm_params": {"model": "openai/*", "api_key": "test-key"}},
],
model_group_alias={"analysis-alias": "analysis"},
)
monkeypatch.setattr(proxy_server, "llm_router", router)
return router
@pytest.mark.parametrize("model", ("openai/test-analysis", "analysis", "analysis-alias"))
@pytest.mark.asyncio
async def test_analysis_accepts_models_served_by_configured_routes(analysis_router: Router, model: str) -> None:
settings: Final = LensSettings(name="Research", model=model, context="Answer using cited sources")
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
assert analysis_router.get_model_list(model_name=model)
await validate_model(settings, auth)
@pytest.mark.parametrize("model", ("unconfigured", "anthropic/test-analysis"))
@pytest.mark.asyncio
async def test_analysis_rejects_models_without_a_configured_route(analysis_router: Router, model: str) -> None:
settings: Final = LensSettings(name="Research", model=model, context="Answer using cited sources")
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
assert not analysis_router.get_model_list(model_name=model)
with pytest.raises(HTTPException) as error:
await validate_model(settings, auth)
assert error.value.status_code == 400
@pytest.mark.asyncio
async def test_analysis_route_resolution_preserves_key_model_restrictions(analysis_router: Router) -> None:
settings: Final = LensSettings(name="Research", model="openai/test-analysis", context="Answer using cited sources")
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER, models=["analysis"])
assert analysis_router.get_model_list(model_name=settings.model)
with pytest.raises(HTTPException) as error:
await validate_model(settings, auth)
assert error.value.status_code == 403
@pytest.mark.asyncio
async def test_analysis_rejects_unpriced_wildcard_before_creating_a_run(analysis_router: Router) -> None:
settings: Final = LensSettings(name="Research", model="unpriced/lens-unpriced-test", context="Answer questions")
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
assert analysis_router.get_model_list(model_name=settings.model)
with pytest.raises(HTTPException) as error:
await validate_model(settings, auth)
assert error.value.status_code == 400
assert "Pricing is not configured" in error.value.detail
@pytest.mark.parametrize("model,allowed", (("openai/test-analysis", "openai/*"), ("analysis-alias", "analysis")))
@pytest.mark.asyncio
async def test_analysis_key_accepts_wildcard_and_alias_access(
analysis_router: Router, model: str, allowed: str
) -> None:
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER, models=[allowed])
assert analysis_router.get_model_list(model_name=model)
await validate_model(LensSettings(name="Research", model=model, context="Answer questions"), auth)
@pytest.mark.parametrize("revoked,key_id", ((True, "a" * 64), (False, None)))
@pytest.mark.asyncio
async def test_worker_without_active_billing_cannot_take_work(revoked: bool, key_id: str | None) -> None:
from tests.unit.proxy.lens.test_state import worker
inactive: Final = worker().model_copy(update={"revoked": revoked, "analysis_key_id": key_id})
settings: Final = LensSettings(name="Research", model="analysis", context="Answer questions")
assert not await worker_supports_model(inactive, settings)
@pytest.mark.parametrize("role", (LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY))
@pytest.mark.asyncio
async def test_agent_discovery_without_trace_storage_is_empty(role: LitellmUserRoles) -> None:
auth: Final = UserAPIKeyAuth(user_role=role)
assert await list_agents(auth, None) == ()
@pytest.mark.asyncio
async def test_agent_discovery_without_trace_storage_still_requires_admin_access() -> None:
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER)
with pytest.raises(HTTPException) as error:
await list_agents(auth, None)
assert error.value.status_code == 403
@pytest.mark.asyncio
async def test_trace_finding_counts_require_investigation_read_access() -> None:
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER)
request: Final = TraceFindingsRequest(traces=(TraceIdentity(trace_id="trace"),))
with pytest.raises(HTTPException) as error:
await trace_findings(request, auth)
assert error.value.status_code == 403
@pytest.mark.asyncio
async def test_signal_endpoints_return_statuses_in_request_order_for_admin_viewers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from litellm.proxy import proxy_server
config: Final = SignalConfig(model="decision")
rows: Final = {
"pending": StoredTraceSignal(
trace_id="pending",
config_key=config.key(),
span_count=1,
claimed_until=NOW + timedelta(minutes=1),
data={"status": "pending", "scores": {}, "model": "decision", "error": ""},
),
"classified": StoredTraceSignal(
trace_id="classified",
config_key=config.key(),
span_count=1,
classified_at=NOW,
data={
"status": "classified",
"scores": {"user_frustration": 0.7, "missing_capability": 0.8},
"model": "decision",
"error": "",
},
),
"failed": StoredTraceSignal(
trace_id="failed",
config_key=config.key(),
span_count=1,
classified_at=NOW,
data={"status": "failed", "scores": {}, "model": "decision", "error": "classification failed"},
),
"stale": StoredTraceSignal(
trace_id="stale",
config_key="old-config",
span_count=1,
classified_at=NOW,
data={
"status": "classified",
"scores": {"user_frustration": 1.0},
"model": "old",
"error": "",
},
),
}
database: Final = SignalStatusDatabase(config, rows)
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=database))
viewer: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY)
request: Final = TraceFindingsRequest(
traces=tuple(
TraceIdentity(trace_id=trace_id) for trace_id in ("failed", "classified", "missing", "pending", "stale")
)
)
assert await get_signals(viewer) == config
results: Final = await trace_signal_statuses(request, viewer)
assert tuple((result.trace_id, result.status) for result in results) == (
("failed", "failed"),
("classified", "classified"),
("missing", "unclassified"),
("pending", "pending"),
("stale", "unclassified"),
)
assert tuple((flag.signal_id, flag.name, flag.score) for flag in results[1].flags) == (
("missing_capability", "Missing capability", 0.8),
("user_frustration", "User frustration", 0.7),
)
@pytest.mark.asyncio
async def test_signal_endpoints_require_connected_postgres(monkeypatch: pytest.MonkeyPatch) -> None:
from litellm.proxy import proxy_server
monkeypatch.setattr(proxy_server, "prisma_client", None)
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY)
with pytest.raises(HTTPException) as error:
await get_signals(auth)
assert error.value.status_code == 503
@pytest.mark.asyncio
async def test_put_signals_saves_config_for_admin(monkeypatch: pytest.MonkeyPatch) -> None:
from litellm.proxy import proxy_server
database: Final = SignalStatusDatabase(SignalConfig(), {})
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=database))
monkeypatch.setattr(proxy_server, "llm_router", signal_router())
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
body: Final = SignalConfig(model="decision", threshold=0.7)
assert await put_signals(body, auth) == body
saved: Final = await database.saved.get()
assert saved[0] == "global"
assert isinstance(saved[1], str)
assert SignalConfig.model_validate_json(saved[1]) == body
def test_signal_model_requires_a_ready_router() -> None:
with pytest.raises(HTTPException) as error:
validate_signal_model(SignalConfig(model="decision"), None)
assert error.value.status_code == 400
@pytest.mark.asyncio
@pytest.mark.parametrize("role", (LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY))
async def test_put_signals_rejects_non_admin_roles(role: LitellmUserRoles) -> None:
auth: Final = UserAPIKeyAuth(user_role=role)
with pytest.raises(HTTPException) as error:
await put_signals(SignalConfig(model="decision"), auth)
assert error.value.status_code == 403
@pytest.mark.asyncio
@pytest.mark.parametrize("model", ("chat", "unconfigured"))
async def test_put_signals_rejects_chat_and_unknown_model_groups(monkeypatch: pytest.MonkeyPatch, model: str) -> None:
from litellm.proxy import proxy_server
monkeypatch.setattr(proxy_server, "llm_router", signal_router())
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
with pytest.raises(HTTPException) as error:
await put_signals(SignalConfig(model=model), auth)
assert error.value.status_code == 400
assert error.value.detail == "Choose a System 1 model (evaluation mode) configured on this proxy"
def test_signal_model_accepts_only_evaluation_mode_groups() -> None:
assert validate_signal_model(SignalConfig(model="decision"), signal_router()) is None
@pytest.mark.parametrize(
"role",
(LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, LitellmUserRoles.TEAM),
)
def test_non_admin_cannot_start_analysis_spending(role: LitellmUserRoles) -> None:
auth: Final = UserAPIKeyAuth(user_role=role, team_id="team", token="hashed-test-key")
with pytest.raises(HTTPException) as error:
user_scope(auth, write=True)
assert error.value.status_code == 403
def test_admin_can_configure_lens_and_viewer_can_only_read() -> None:
admin: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
viewer: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY)
assert user_scope(admin, write=True).all_teams
assert user_scope(viewer).all_teams
@pytest.mark.parametrize("identity", ("not-an-execution", "W10=", "WyJvdGhlciIsICIiLCAiaWQiXQ=="))
def test_invalid_explicit_execution_ids_are_rejected(identity: str) -> None:
from litellm.proxy.lens.endpoints import validate_selection
from tests.unit.proxy.lens.test_state import lens
settings: Final = lens().settings.model_copy(update={"execution_ids": (identity,)})
with pytest.raises(HTTPException) as error:
validate_selection(settings)
assert error.value.status_code == 422
@pytest.mark.parametrize("protocol_version", (1, 2, 3))
@pytest.mark.asyncio
async def test_incompatible_worker_is_rejected_before_claiming_work(
protocol_version: int, monkeypatch: pytest.MonkeyPatch
) -> None:
from litellm.proxy.lens.endpoints import claim
from tests.unit.proxy.lens.test_state import worker
monkeypatch.setenv("LITELLM_RELEASE_TAG", "v1.2.3")
with pytest.raises(HTTPException) as error:
await claim(worker(), protocol_version=protocol_version)
assert error.value.status_code == 409
assert "Upgrade" in error.value.detail
@pytest.mark.parametrize("role", (LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.TEAM, None))
@pytest.mark.asyncio
async def test_regular_keys_cannot_poll_live_reviews(role: LitellmUserRoles | None) -> None:
auth: Final = UserAPIKeyAuth(user_role=role, team_id="team", token="hashed-test-key")
with pytest.raises(HTTPException) as error:
await read_reviews("lens", "job", auth)
assert error.value.status_code == 403
@pytest.mark.parametrize("role", (LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.TEAM, None))
def test_regular_keys_cannot_read_lens_results(role: LitellmUserRoles | None) -> None:
auth: Final = UserAPIKeyAuth(user_role=role, team_id="team", token="hashed-test-key")
with pytest.raises(HTTPException) as error:
user_scope(auth)
assert error.value.status_code == 403
def saved_lens() -> Lens:
now: Final = datetime(2026, 1, 15, tzinfo=timezone.utc)
return Lens(
id="lens",
scope=Scope(all_teams=True),
settings=LensSettings(name="Support", model="analysis", context="Answer questions", agent_name="support"),
created_at=now,
next_run_at=now,
budget_month="2026-01",
)
def test_run_now_agent_override_only_changes_the_agent_for_that_run() -> None:
lens: Final = saved_lens()
overridden: Final = run_settings(lens, RunRequest(agent_name="billing"))
assert overridden is not None
assert overridden.agent_name == "billing"
assert overridden.model_copy(update={"agent_name": "support"}) == lens.settings
def test_run_now_without_overrides_keeps_the_saved_settings() -> None:
assert run_settings(saved_lens(), RunRequest()) is None
def test_run_now_rejects_a_window_that_is_missing_an_edge_or_backwards() -> None:
now: Final = datetime(2026, 1, 15, tzinfo=timezone.utc)
with pytest.raises(ValidationError, match="both a start and an end"):
RunRequest(start=now)
with pytest.raises(ValidationError, match="before end"):
RunRequest(start=now, end=now - timedelta(hours=1))
def test_watching_switches_a_paused_investigation_on_and_records_the_change() -> None:
paused: Final = saved_lens().model_copy(
update={"settings": saved_lens().settings.model_copy(update={"enabled": False})}
)
watched: Final = watching(paused)
assert watched.settings.enabled is True
assert watched.revision == paused.revision + 1
assert watched.settings.model_copy(update={"enabled": False}) == paused.settings
def test_watching_leaves_an_investigation_that_is_already_on_untouched() -> None:
on: Final = saved_lens()
assert watching(on) is on
async def test_watch_all_skips_an_investigation_whose_model_is_gone_instead_of_failing_them_all(
analysis_router: Router,
) -> None:
stale: Final = saved_lens().model_copy(
update={"settings": saved_lens().settings.model_copy(update={"model": "retired-model", "enabled": False})}
)
skipped: Final = await watchable(stale, UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN))
assert skipped is not None
assert skipped.id == stale.id
assert skipped.reason
def test_run_now_since_last_run_keeps_scanning_only_new_traces_even_with_an_agent_override() -> None:
now: Final = datetime(2026, 1, 15, 12, tzinfo=timezone.utc)
resumed: Final = saved_lens().model_copy(update={"last_scan_at": now - timedelta(hours=1)})
window: Final = run_window(resumed, RunRequest(agent_name="billing"), now)
assert window is not None
assert window[0] == now - timedelta(hours=1)
def test_run_now_with_a_lookback_scans_that_lookback_instead_of_since_last_run() -> None:
now: Final = datetime(2026, 1, 15, 12, tzinfo=timezone.utc)
resumed: Final = saved_lens().model_copy(update={"last_scan_at": now - timedelta(hours=1)})
assert run_window(resumed, RunRequest(lookback_hours=24), now) is None
@pytest.mark.parametrize("provider", (False, True))
def test_model_errors_reach_worker_with_status_and_redacted_provider_message(provider: bool) -> None:
from litellm.proxy._types import ProxyException
from litellm.proxy.lens.endpoints import model_failure
message: Final = "Token rate limit exceeded. api_key=secret-example-value-123456 Retry in 60 seconds."
error: Final = model_failure(
ProxyException(message, "rate_limit_error", None, 429, headers={"retry-after": "60"})
if provider
else HTTPException(429, message, headers={"retry-after": "60"})
)
assert error.status_code == 429
assert "Token rate limit exceeded." in error.detail["lens_error"]
assert "Retry in 60 seconds." in error.detail["lens_error"]
assert "secret-example" not in error.detail["lens_error"]
assert error.headers == {"retry-after": "60"}
@pytest.mark.asyncio
async def test_preview_samples_a_selection_without_investigation_settings() -> None:
from litellm.proxy.lens.endpoints import Preview, preview_sample
class SelectionStorage:
async def lens_sample(self, parameters):
assert (parameters.source, parameters.agent_name, parameters.selected_team) == ("requests", "billing", "t1")
assert parameters.preview == 1 and parameters.offset == 3
return []
body: Final = Preview.model_validate(
{"selection": {"source": "requests", "agent_name": "billing", "team_id": "t1"}, "offset": 3}
)
sample: Final = await preview_sample(
body, UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), SelectionStorage()
)
assert sample.eligible == 0 and not sample.executions
@pytest.mark.asyncio
async def test_preview_reports_calendar_overflow_as_a_validation_error() -> None:
from datetime import datetime, timezone
from litellm.proxy.lens.endpoints import Preview, preview_sample
body: Final = Preview(
selection=ActivitySelection(),
as_of=datetime.min.replace(tzinfo=timezone.utc),
)
with pytest.raises(HTTPException) as error:
await preview_sample(body, UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), None)
assert error.value.status_code == 422
assert "supported calendar range" in error.value.detail
@pytest.mark.asyncio
@pytest.mark.parametrize("worker_release", ("", "v1.2.2", "branch-main-old"))
async def test_different_release_is_rejected_before_accessing_jobs(
monkeypatch: pytest.MonkeyPatch, worker_release: str
) -> None:
from litellm.proxy.lens.endpoints import claim
from litellm.proxy.lens.release import PROTOCOL_VERSION
from tests.unit.proxy.lens.test_state import worker
monkeypatch.setenv("LITELLM_RELEASE_TAG", "v1.2.3")
monkeypatch.delenv("LENS_WORKER_IMAGE", raising=False)
with pytest.raises(HTTPException) as error:
await claim(worker(), protocol_version=PROTOCOL_VERSION, worker_release=worker_release)
assert error.value.status_code == 409
assert "ghcr.io/berriai/litellm-lens-worker:v1.2.3" in error.value.detail
@pytest.mark.asyncio
async def test_unknown_gateway_release_refuses_registration_and_claims(monkeypatch: pytest.MonkeyPatch) -> None:
from litellm.proxy.lens.endpoints import WorkerName, claim, register_worker
from litellm.proxy.lens.release import PROTOCOL_VERSION
from tests.unit.proxy.lens.test_state import worker
monkeypatch.setenv("LITELLM_RELEASE_TAG", "")
monkeypatch.setenv("LENS_WORKER_IMAGE", "registry.example/lens-worker:old")
with pytest.raises(HTTPException) as registration_error:
await register_worker(
WorkerName(analysis_key_id="a" * 64), UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
)
assert registration_error.value.status_code == 503
assert "LITELLM_RELEASE_TAG" in registration_error.value.detail
with pytest.raises(HTTPException) as claim_error:
await claim(worker(), protocol_version=PROTOCOL_VERSION, worker_release="")
assert claim_error.value.status_code == 503
assert claim_error.value.detail == registration_error.value.detail
@pytest.mark.asyncio
async def test_claim_due_pages_through_more_than_a_thousand_full_pages() -> None:
candidate_lens: Final = lens()
def candidate_page(page_number: int, size: int) -> tuple[DueLens, ...]:
return tuple(
DueLens(
lens=candidate_lens.model_copy(update={"id": f"lens-{page_number * 20 + offset:05}"}),
due_at=NOW,
)
for offset in range(size)
)
full_pages: Final = tuple(candidate_page(page_number, 20) for page_number in range(1_200))
pages: Final = (*full_pages, candidate_page(1_200, 1))
assigned_worker: Final = worker()
class PagingRepository:
def __init__(self) -> None:
self.after_calls: tuple[DueLens | None, ...] = ()
async def due(
self, scope: Scope, now: datetime, limit: int, after: DueLens | None = None
) -> tuple[DueLens, ...]:
assert scope == assigned_worker.scope
assert now == NOW
assert limit == 20
self.after_calls = (*self.after_calls, after)
return pages[len(self.after_calls) - 1]
async def sync_due(self, lens: Lens) -> None:
return None
async def update(
self, lens_id: str, transform: Callable[[Lens], Lens], attempts: int, *, changed_only: bool
) -> Lens | None:
raise AssertionError("Unsupported models must not update candidates")
async def reject_model(_worker: Worker, _settings: LensSettings) -> bool:
return False
repository: Final = PagingRepository()
claim: Final = await claim_due(assigned_worker, NOW, repository, reject_model)
expected_after: Final = (None, *(page[-1] for page in pages[:-1]))
assert claim is None
assert len(repository.after_calls) == 1_201
assert repository.after_calls == expected_after
@pytest.mark.asyncio
async def test_compatible_worker_without_an_analysis_key_waits_without_claiming_jobs(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from litellm.proxy import proxy_server
from litellm.proxy.lens.endpoints import claim
from litellm.proxy.lens.release import PROTOCOL_VERSION
monkeypatch.setenv("LITELLM_RELEASE_TAG", "v1.2.3")
monkeypatch.setattr(proxy_server, "prisma_client", None)
unassigned: Final = worker().model_copy(update={"analysis_key_id": None})
assert await claim(unassigned, protocol_version=PROTOCOL_VERSION, worker_release="v1.2.3") is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
"configured,credential,expected", ((False, "x" * 32, 503), (True, "wrong", 401), (True, "x" * 32, None))
)
async def test_internal_service_authentication_is_separate_from_gateway_keys(
monkeypatch: pytest.MonkeyPatch, configured: bool, credential: str, expected: int | None
) -> None:
from fastapi.security import HTTPAuthorizationCredentials
from litellm.proxy.lens.endpoints import service_auth
monkeypatch.setenv("LITELLM_LENS_URL", "http://lens" if configured else "")
monkeypatch.setenv("LITELLM_LENS_SERVICE_TOKEN", "x" * 32)
credentials: Final = HTTPAuthorizationCredentials(scheme="Bearer", credentials=credential)
if expected is None:
assert await service_auth(credentials) is None
else:
with pytest.raises(HTTPException) as failure:
await service_auth(credentials)
assert failure.value.status_code == expected
@pytest.mark.asyncio
@pytest.mark.parametrize(
"status,content,connected",
(
(200, b'{"storage_ready":true,"credentials_ready":true,"release":"v1.2.3","protocol_version":2}', True),
(503, b"private storage details", False),
(200, b"invalid JSON", False),
(200, b"x" * 17000, False),
),
ids=("ready", "unavailable", "invalid-json", "oversized-response"),
)
@pytest.mark.usefixtures("httpx_transport")
async def test_service_status_uses_internal_auth_and_only_advertises_the_public_url(
monkeypatch: pytest.MonkeyPatch, status: int, content: bytes, connected: bool
) -> None:
import respx
from litellm.proxy.lens.endpoints import service_connection, user_scope
monkeypatch.setenv("LITELLM_LENS_URL", "http://lens/private-prefix")
monkeypatch.setenv("LITELLM_LENS_PUBLIC_URL", "https://traces.example/lens-ingest/")
monkeypatch.setenv("LITELLM_LENS_SERVICE_TOKEN", "x" * 32)
with respx.mock as network:
route: Final = network.get("http://lens/private-prefix/internal/status").respond(status, content=content)
result: Final = await service_connection(UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER))
assert result.url == "https://traces.example/lens-ingest"
assert result.connected is connected
assert result.configured is True
assert result.status.storage_ready is connected
assert route.calls[0].request.headers["Authorization"] == "Bearer " + "x" * 32
assert "private storage details" not in result.model_dump_json()
with pytest.raises(HTTPException) as denied:
user_scope(UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER))
assert denied.value.status_code == 403
@pytest.mark.asyncio
@pytest.mark.parametrize("url,configured", (("", False), ("http://lens", True)))
async def test_service_setup_distinguishes_missing_installation_from_incomplete_configuration(
monkeypatch: pytest.MonkeyPatch, url: str, configured: bool
) -> None:
from litellm.proxy.lens.endpoints import service_connection
monkeypatch.setenv("LITELLM_LENS_URL", url)
monkeypatch.delenv("LITELLM_LENS_SERVICE_TOKEN", raising=False)
monkeypatch.setenv("LITELLM_RELEASE_TAG", "v1.2.3")
result: Final = await service_connection(UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER))
assert result.configured is configured
assert result.connected is False
assert result.release == "v1.2.3"
assert result.status.storage_ready is False
@pytest.mark.asyncio
async def test_credential_snapshot_excludes_expired_keys_and_disables_caching(monkeypatch: pytest.MonkeyPatch) -> None:
from unittest.mock import AsyncMock
from fastapi import Response
from litellm.proxy import proxy_server
from litellm.proxy.lens.endpoints import ingestion_credentials
from litellm.proxy.lens.ingestion import IngestionCredential, IngestionKeyCreated, IngestionKeyRequest, new_key
created: Final = new_key(IngestionKeyRequest(team_id="team"), "owner")
assert isinstance(created, IngestionKeyCreated)
current: Final = created.record
expired: Final = current.model_copy(update={"id": "expired", "expires_at": 1})
db: Final = SimpleNamespace(
query_raw=AsyncMock(return_value=tuple(Row(data=key.model_dump(mode="json")) for key in (current, expired)))
)
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
response: Final = Response()
snapshot: Final = await ingestion_credentials(None, response)
assert snapshot.keys == (
IngestionCredential(token_hash=current.tenant.api_key_hash, tenant=current.tenant, expires_at=None),
)
assert response.headers["Cache-Control"] == "no-store"
assert snapshot.issued_at >= int(current.created_at.timestamp())
@pytest.mark.asyncio
@pytest.mark.parametrize("accepted", (True, False))
@pytest.mark.usefixtures("httpx_transport")
async def test_created_ingestion_keys_report_activation_only_after_the_service_acknowledges(
monkeypatch: pytest.MonkeyPatch, accepted: bool
) -> None:
import hashlib
import json
from unittest.mock import AsyncMock, MagicMock
import respx
from litellm.proxy import proxy_server
from litellm.proxy.lens.endpoints import create_ingestion_key, list_ingestion_keys, revoke_ingestion_key
from litellm.proxy.lens.ingestion import IngestionKey, IngestionKeyRequest
db: Final = SimpleNamespace(query_raw=AsyncMock(return_value=()), execute_raw=AsyncMock(return_value=1))
context: Final = AsyncMock()
context.__aenter__.return_value = db
db.tx = MagicMock(return_value=context)
monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=db))
monkeypatch.setenv("LITELLM_LENS_URL", "http://lens")
monkeypatch.setenv("LITELLM_LENS_SERVICE_TOKEN", "x" * 32)
auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN, user_id="owner")
with respx.mock as network:
route: Final = network.post("http://lens/internal/credentials").respond(204 if accepted else 503)
created: Final = await create_ingestion_key(IngestionKeyRequest(name="Agent", team_id="team"), auth)
assert created.active is accepted
persisted: Final = IngestionKey.model_validate_json(db.execute_raw.call_args.args[2])
assert persisted == created.record
assert persisted.tenant.api_key_hash == hashlib.sha256(created.key.encode()).hexdigest()
assert persisted.tenant.user_id == "owner"
assert persisted.tenant.team_id == "team"
assert created.key not in persisted.model_dump_json()
db.query_raw.return_value = (Row(data=persisted.model_dump(mode="json")),)
assert await list_ingestion_keys(auth) == (persisted,)
db.query_raw.return_value = ()
assert await revoke_ingestion_key(persisted.id, auth)
assert db.execute_raw.call_args.args == ('DELETE FROM "LiteLLM_LensIngestionKey" WHERE id=$1', persisted.id)
assert json.loads(route.calls[-1].request.content)["keys"] == []
@pytest.mark.asyncio
async def test_ingestion_keys_reject_expired_requests_and_read_only_admins(monkeypatch: pytest.MonkeyPatch) -> None:
from litellm.proxy import proxy_server
from litellm.proxy.lens.endpoints import create_ingestion_key
from litellm.proxy.lens.ingestion import IngestionKeyRequest
monkeypatch.setattr(proxy_server, "prisma_client", None)
with pytest.raises(HTTPException) as expired:
await create_ingestion_key(
IngestionKeyRequest(expires_at=datetime(2000, 1, 1, tzinfo=timezone.utc)),
UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
)
assert expired.value.status_code == 422
with pytest.raises(HTTPException) as forbidden:
await create_ingestion_key(
IngestionKeyRequest(), UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY)
)
assert forbidden.value.status_code == 403