litellm/tests/unit/proxy/lens/test_endpoints.py
moe-berri 5147aefa8b
fix(lens): reuse trace reviews and consolidate findings across runs (#44778)
* fix(lens): reuse trace reviews and consolidate findings across runs

* chore: sync schema.prisma copies from root

* fix(lens): preserve partial reviews and bound budget admission

* fix(lens): resolve CI regressions and clarify reused runs

* test(lens): exercise review reuse in worker container smoke

* fix(lens): show actual reviews when a run finishes

* fix(lens): distinguish reuse plans and stop blocked scans

* fix(lens): retain partial findings when model requests stop

* docs(lens): clarify budget edits during active runs

* fix(lens): publish findings only after reconciliation completes

* fix(lens): guide insufficient-budget runs to budget settings

* fix(lens): renew budget holds and bound admission waits

* fix(lens): preserve provider errors during budget cleanup

* fix(ci): update PgBouncer and sharp for current builds

* fix(lens): serialize settlement and fence review checkpoints

* fix(lens): defer generated Prisma client type import

* test(lens): verify checkpoint and progress rollback together

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-06 12:09:56 -07:00

626 lines
26 KiB
Python

from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from typing import Final
import pytest
from fastapi import HTTPException
from pydantic import ValidationError
import litellm
from litellm import Router
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.lens.endpoints import (
list_agents,
read_reviews,
result,
run_settings,
run_window,
trace_findings,
user_scope,
validate_model,
watchable,
watching,
worker_supports_model,
)
from litellm.proxy.lens.models import (
ActivitySelection,
Coverage,
Lens,
LensSettings,
Result,
ReviewVersion,
RunAssessment,
RunRequest,
Sample,
Scope,
TraceFindingsRequest,
TraceIdentity,
)
from litellm.proxy.lens.repository import Row
from litellm.proxy.lens.state import claim_job, queue_job, replace_job
from tests.unit.proxy.lens.test_agent_workspace import execution
from tests.unit.proxy.lens.test_state import NOW, lens, worker
class ResultDatabase:
def __init__(self, stored: Lens) -> None:
self.stored = stored
self.completed: tuple[ReviewVersion, ...] = ()
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)
@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.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:
import httpx
from litellm.proxy._types import ProxyException
from litellm.proxy.lens.endpoints import model_failure
from litellm.proxy.lens.worker import failure_message
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"})
)
request: Final = httpx.Request("POST", "https://proxy.test/lens/worker/lens/run/model")
response: Final = httpx.Response(error.status_code, json={"detail": error.detail}, request=request)
with pytest.raises(httpx.HTTPStatusError) as caught:
response.raise_for_status()
diagnostic: Final = failure_message(caught.value)
assert diagnostic.startswith("Model request failed (HTTP 429):")
assert "Token rate limit exceeded." in diagnostic
assert "Retry in 60 seconds." in diagnostic
assert "secret-example" not in diagnostic
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