litellm/tests/unit/proxy/lens/test_internal.py
moe-berri 22ad3c5fff
refactor(lens): connect LiteLLM to the independent Lens service (#45529)
* feat(lens): consume the independent Lens service and shared UI

* fix(lens): keep embedded setup stable and vendor UI before builds

* chore(lens): pin embedded UI provenance to published Lens source

* fix(lens): complete embedded UI extraction across CI builds

* docs(lens): record latest main and removal-boundary validation

* fix(lens): wire external chart credentials and ingestion modes

* test(lens): cover adapter failures and fix extraction CI

* docs(lens): refresh bundled chart setup instructions

* test(lens): restore gateway adapter test package marker

* build(lens): pin clean-cut UI and supported storage chart

* test(ui): await guardrail scope menu before checking options

* perf(lens): omit unused trace-team lookup from product requests

* docs(lens): record final-image adapter permission checks

* test(lens): assert delegated investigation authorization

* refactor(lens): remove copied trace runtime and preserve spend logging

* test(lens): verify independent gateway image upgrades and outages

* test(lens): pin the chart qualification database image

* fix(lens): update embedded UI metadata filters

* test: wait for Lens chart pod identity convergence

* fix(e2e): select Lens chart pods by deployment ownership

* fix(e2e): select Helm migration ownership for Lens upgrades

* test(lens): retain migration Jobs through rollout assertions

* docs(helm): require existing database for migration hooks

* test(lens): qualify release boundaries with embedded navigation update

* docs(lens): record browser qualification for embedded tabs

* fix(lens): remove unrelated CI and guardrail test changes

* test(lens): give qualification tenants unique key aliases

* feat(lens): embed the latest canonical Lens interface

* fix(lens): qualify split inference through the gateway

* fix(lens): preserve scoped auth and isolate delegated credentials

* chore(lens): remove unrelated documentation and lint changes

* fix(lens): bound concurrent forwarding buffers through response delivery

* fix(lens): preserve OAuth2 dispatch without inference policies

* chore(lens): adopt current shared onboarding UI

* fix(lens): refresh shared setup UI and review context

* fix(lens): bound uploads before gateway authentication

* docs(lens): remove extraction evidence from the gateway repo

* docs(lens): refresh shared setup prompt for paired releases
2026-10-09 16:17:05 -07:00

125 lines
4.8 KiB
Python

from collections.abc import Mapping
from typing import Final
import httpx
import jwt
import pytest
from fastapi import FastAPI, Header, HTTPException
from litellm.integrations.clickhouse.context import is_lens_analysis
from litellm.litellm_core_utils.initialize_dynamic_callback_params import initialize_standard_callback_dynamic_params
from litellm.proxy.lens.internal import LensInternalMiddleware, verified
SECRET: Final = "gateway-internal-identity-fixture-key-32"
NOW: Final = 1_800_000_000
def claims(**changes: object) -> Mapping[str, object]:
return {
"iss": "litellm-lens",
"aud": "litellm",
"sub": "lens-internal",
"purpose": "analysis",
"iat": NOW,
"exp": NOW + 30,
**changes,
}
@pytest.mark.parametrize(
"changes,valid",
(
({}, True),
({"purpose": "signals"}, True),
({"purpose": "billing"}, False),
({"iss": "unknown"}, False),
({"aud": "unknown"}, False),
({"sub": "admin"}, False),
({"exp": NOW}, False),
({"iat": NOW + 1, "exp": NOW + 31}, True),
({"iat": NOW + 5, "exp": NOW + 35}, True),
({"iat": NOW + 6, "exp": NOW + 36}, False),
({"iat": NOW + 5, "exp": NOW + 5}, False),
({"iat": NOW + 5, "exp": NOW + 4}, False),
({"exp": NOW + 61}, False),
({"iat": True}, False),
({"exp": str(NOW + 30)}, False),
({"unknown": "value"}, False),
({"iat": NOW - 59, "exp": NOW + 1}, True),
),
ids=(
"analysis",
"signals",
"purpose",
"issuer",
"audience",
"subject",
"expired",
"small_clock_skew",
"clock_skew_boundary",
"excessive_clock_skew",
"zero_lifetime",
"negative_lifetime",
"ttl",
"bool_timestamp",
"string_timestamp",
"unknown_claim",
"maximum_ttl",
),
)
def test_only_bounded_internal_identity_is_accepted(changes: Mapping[str, object], valid: bool) -> None:
token: Final = jwt.encode(dict(claims(**changes)), SECRET, algorithm="HS256")
assert verified(token, SECRET, NOW) is valid
@pytest.mark.parametrize("secret", ("", "short", SECRET + "wrong"), ids=("unset", "short", "wrong"))
def test_marker_does_not_authorize_itself(secret: str) -> None:
token: Final = jwt.encode(dict(claims()), SECRET, algorithm="HS256")
assert not verified(token, secret, NOW)
@pytest.mark.asyncio
@pytest.mark.parametrize("authenticated", (True, False), ids=("authenticated", "unauthenticated"))
async def test_internal_marker_preserves_authentication_and_is_private(authenticated: bool) -> None:
app: Final = FastAPI()
@app.post("/chat/completions")
async def completion(
authorization: str | None = Header(default=None), x_lens_internal: str | None = Header(default=None)
) -> Mapping[str, object]:
if authorization != "Bearer gateway-key":
raise HTTPException(401, "Key required")
return {
"internal": is_lens_analysis(),
"marker": x_lens_internal,
"privacy": initialize_standard_callback_dynamic_params({}).get("turn_off_message_logging"),
}
app.add_middleware(LensInternalMiddleware, environ={"LENS_GATEWAY_SECRET": SECRET}, now=lambda: NOW)
token: Final = jwt.encode(dict(claims()), SECRET, algorithm="HS256")
headers: Final = {"X-Lens-Internal": token, **({"Authorization": "Bearer gateway-key"} if authenticated else {})}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://gateway.test") as client:
response: Final = await client.post("/chat/completions", headers=headers)
ordinary: Final = await client.post("/chat/completions", headers={"Authorization": "Bearer gateway-key"})
assert response.status_code == (200 if authenticated else 401)
if authenticated:
assert response.json() == {"internal": True, "marker": None, "privacy": True}
assert ordinary.json() == {"internal": False, "marker": None, "privacy": None}
assert not is_lens_analysis()
@pytest.mark.asyncio
async def test_duplicate_markers_are_rejected_before_inference() -> None:
app: Final = FastAPI()
@app.post("/chat/completions")
async def completion() -> Mapping[str, object]:
pytest.fail("Invalid duplicate marker reached inference")
app.add_middleware(LensInternalMiddleware, environ={"LENS_GATEWAY_SECRET": SECRET}, now=lambda: NOW)
token: Final = jwt.encode(dict(claims()), SECRET, algorithm="HS256")
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://gateway.test") as client:
response: Final = await client.post(
"/chat/completions", headers=[("X-Lens-Internal", token), ("X-Lens-Internal", "forged")]
)
assert response.status_code == 401