mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
* 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
109 lines
4.8 KiB
Python
109 lines
4.8 KiB
Python
import base64
|
|
import gzip
|
|
import json
|
|
import time
|
|
from types import MappingProxyType
|
|
from typing import Final
|
|
from urllib.parse import parse_qs, urlsplit
|
|
|
|
import pytest
|
|
|
|
from litellm.rust_bridge._native import NativeClickHouseSpendConfig, NativeClickHouseSpendStorage
|
|
from tests.test_litellm_rust.support.recording_server import RecordingServer, ResponseSpec
|
|
|
|
pytestmark = pytest.mark.requires_rust_extension
|
|
|
|
|
|
def _storage(url: str, retention_days: int = 14) -> NativeClickHouseSpendStorage:
|
|
return NativeClickHouseSpendStorage(NativeClickHouseSpendConfig("spend_test", url, retention_days))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_spend_writer_preserves_python_mappings(recording_server: RecordingServer) -> None:
|
|
recording_server.enqueue(ResponseSpec(body=""))
|
|
storage: Final = _storage(recording_server.base_url)
|
|
attributes: Final = MappingProxyType({"label": "雪"})
|
|
row: Final = MappingProxyType(
|
|
{
|
|
"start_time": 1_234,
|
|
"end_time": 2_345,
|
|
"completion_start_time": None,
|
|
"metadata": attributes,
|
|
"request_tags": ("first", "second"),
|
|
"EngineReceivedMs": -1,
|
|
}
|
|
)
|
|
before: Final = time.time_ns() // 1_000_000
|
|
await storage.insert_rows((row,))
|
|
after: Final = time.time_ns() // 1_000_000
|
|
request: Final = recording_server.requests[0]
|
|
stored: Final = json.loads(gzip.decompress(request.raw_body))
|
|
assert before <= stored["EngineReceivedMs"] <= after
|
|
assert stored == {
|
|
"start_time": "1970-01-01T00:00:01.234Z",
|
|
"end_time": "1970-01-01T00:00:02.345Z",
|
|
"completion_start_time": None,
|
|
"metadata": attributes,
|
|
"request_tags": ["first", "second"],
|
|
"EngineReceivedMs": stored["EngineReceivedMs"],
|
|
}
|
|
assert row["EngineReceivedMs"] == -1
|
|
assert parse_qs(urlsplit(request.path).query)["query"] == ["INSERT INTO `spend_test`.spend_logs FORMAT JSONEachRow"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_spend_writer_rejects_python_objects_before_io(recording_server: RecordingServer) -> None:
|
|
recording_server.expected_requests = 0
|
|
invalid: Final = object()
|
|
with pytest.raises(ValueError, match=type(invalid).__name__):
|
|
await _storage(recording_server.base_url).insert_rows(({"metadata": invalid},))
|
|
assert recording_server.requests == []
|
|
|
|
|
|
@pytest.mark.parametrize("status", (400, 503), ids=("bad-row", "unavailable"))
|
|
@pytest.mark.asyncio
|
|
async def test_native_spend_writer_maps_insert_failures(recording_server: RecordingServer, status: int) -> None:
|
|
recording_server.enqueue(ResponseSpec(status=status, body="denied"))
|
|
with pytest.raises(RuntimeError, match=f"insert failed with HTTP status {status}"):
|
|
await _storage(recording_server.base_url).insert_rows(({"request_id": "request"},))
|
|
|
|
|
|
@pytest.mark.parametrize("limit", ("0", "invalid"))
|
|
@pytest.mark.asyncio
|
|
async def test_native_spend_writer_validates_configured_limit(
|
|
recording_server: RecordingServer, monkeypatch: pytest.MonkeyPatch, limit: str
|
|
) -> None:
|
|
recording_server.expected_requests = 0
|
|
monkeypatch.setenv("CLICKHOUSE_TRACE_MAX_INSERT_BYTES", limit)
|
|
with pytest.raises(ValueError, match="CLICKHOUSE_TRACE_MAX_INSERT_BYTES must be a positive integer"):
|
|
await _storage(recording_server.base_url).insert_rows(({"request_id": "request"},))
|
|
assert recording_server.requests == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_spend_writer_bounds_encoded_bytes(
|
|
recording_server: RecordingServer, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
recording_server.expected_requests = 0
|
|
monkeypatch.setenv("CLICKHOUSE_TRACE_MAX_INSERT_BYTES", "64")
|
|
with pytest.raises(OverflowError, match="encoded size limit"):
|
|
await _storage(recording_server.base_url).insert_rows(({"metadata": "雪" * 64},))
|
|
assert recording_server.requests == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_native_spend_schema_uses_writer_credentials_and_surfaces_failure(
|
|
recording_server: RecordingServer,
|
|
) -> None:
|
|
recording_server.expected_requests = 2
|
|
recording_server.enqueue(ResponseSpec(body=b""))
|
|
recording_server.enqueue(ResponseSpec(status=403, body="denied"))
|
|
writer_url: Final = recording_server.base_url.replace("http://", "http://writer:p%40ss%2Fword%25@")
|
|
with pytest.raises(RuntimeError, match="schema setup failed with HTTP status 403"):
|
|
await _storage(writer_url + "?database=wrong&readonly=1", 7).ensure_schema()
|
|
assert recording_server.requests[0].raw_body.startswith(b"CREATE DATABASE IF NOT EXISTS")
|
|
assert recording_server.requests[1].raw_body.startswith(b"CREATE TABLE IF NOT EXISTS")
|
|
assert "readonly" not in parse_qs(urlsplit(recording_server.requests[0].path).query)
|
|
assert recording_server.requests[0].headers["authorization"] == (
|
|
"Basic " + base64.b64encode(b"writer:p@ss/word%").decode()
|
|
)
|