mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
* test: add logging e2e coverage (s3_v2, gcs_bucket, team langfuse callback, datadog failure) Five new live e2e scenarios raising Logging & Guardrails registry coverage: s3_v2 success and failure objects read back from the real S3 bucket, gcs_bucket success record read back through the GCS JSON API (with nextPageToken pagination and per-request bearer minting), team-scoped Langfuse callback delivery with non-team isolation, and DataDog failure event delivery queried by indexed model_group. datadog_reader gains query-based variants of the marker search; the langfuse cell is a new registry row. Bucket readers settle past a full flush interval so a late duplicate cannot hide from the exactly-one assertions * test: cover clock-skew day prefix in gcs read-back and retry team callback propagation * test: key the s3 failure read-back on the provider error, not payload absence * chore: rerun ci * chore: rerun ci after config sync * chore: rerun ci with pr lane env * chore: rerun ci * chore: rerun ci * chore: rerun ci * chore: rerun ci * chore: rerun ci * test: add guardrail e2e coverage (presidio masking, bedrock post and during call, moderation on messages) (#38553) * test: add guardrail e2e coverage (presidio masking, bedrock post/during, moderation on messages) * test: require the phone placeholder positively in the presidio masking predicate * test: count only the 400 verdict body as a bedrock post_call block * test(e2e): exempt the guardrail config echo from the post_call leak assertion * test(e2e): pin the fail-closed contract for an unknown guardrail name (skipped, product gap) * test(e2e): tolerate the readiness 503 from a transient db blip in the callback-config probes
115 lines
5 KiB
Python
115 lines
5 KiB
Python
"""Read-back for the s3 logging tests against the real S3 bucket the proxy
|
|
ships StandardLoggingPayload objects to (litellm_settings.callbacks: ["s3_v2"]).
|
|
|
|
Delivery is judged on what actually landed in the bucket: the proxy writes
|
|
with its own credentials exactly as in production, and the tests list and
|
|
download the objects back with boto3 (already a litellm proxy dependency, so
|
|
the e2e runner image carries it; it is an AWS SDK, not a raw HTTP client, so
|
|
the e2e_http-only transport rule is untouched). The bucket comes from
|
|
S3_LOGS_BUCKET_NAME - on the cluster the secret manager injects it, locally
|
|
tests/e2e/.env provides it. Missing configuration is a hard failure, never a
|
|
skip.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
import boto3
|
|
import pytest
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
from e2e_config import POLL_INTERVAL, POLL_TIMEOUT
|
|
|
|
if TYPE_CHECKING:
|
|
from types_boto3_s3.client import S3Client
|
|
|
|
#: How long to keep re-reading after the first match before trusting the
|
|
#: exactly-one assertion: past one full s3_v2 flush interval (~10s), so a
|
|
#: duplicate shipped by a LATER flush is seen, plus listing-latency margin.
|
|
#: The DataDog reader settles the same way (DD_SETTLE_SECONDS).
|
|
S3_SETTLE_SECONDS = 25.0
|
|
|
|
|
|
class S3LogRecord(BaseModel):
|
|
"""The StandardLoggingPayload fields the s3 scenarios pin."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
id: str
|
|
status: str
|
|
model_group: str | None = None
|
|
response_cost: float | None = None
|
|
total_tokens: int | None = None
|
|
error_str: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class S3LogReader:
|
|
bucket: str
|
|
client: S3Client
|
|
|
|
def list_keys(self, prefix: str) -> list[str]:
|
|
response = self.client.list_objects_v2(Bucket=self.bucket, Prefix=prefix)
|
|
return [obj["Key"] for obj in response.get("Contents", []) if "Key" in obj]
|
|
|
|
def read_record(self, key: str) -> S3LogRecord:
|
|
body = self.client.get_object(Bucket=self.bucket, Key=key)["Body"].read()
|
|
return S3LogRecord.model_validate_json(body)
|
|
|
|
def records_matching(self, *, prefix: str, predicate: Callable[[S3LogRecord], bool]) -> list[S3LogRecord]:
|
|
return [record for record in map(self.read_record, self.list_keys(prefix)) if predicate(record)]
|
|
|
|
def poll_records(self, *, prefix: str, predicate: Callable[[S3LogRecord], bool]) -> list[S3LogRecord]:
|
|
"""Poll until at least one matching object is listed (the s3_v2
|
|
callback flushes on a ~10s timer), then keep re-reading for
|
|
S3_SETTLE_SECONDS - past a full flush interval - so a duplicate
|
|
shipped by a later flush cannot hide from the exactly-one assertion.
|
|
One blind spot is inherent: a duplicate write that reuses the exact
|
|
same object key overwrites the first object and no listing can see
|
|
it; distinct-key duplicates are what this catches. At the deadline an
|
|
empty list is returned and the caller's assertion carries the failure
|
|
message."""
|
|
deadline = time.monotonic() + POLL_TIMEOUT
|
|
while time.monotonic() < deadline:
|
|
records = self.records_matching(prefix=prefix, predicate=predicate)
|
|
if records:
|
|
return self._settled_records(prefix=prefix, predicate=predicate, first=records)
|
|
time.sleep(POLL_INTERVAL)
|
|
return []
|
|
|
|
def _settled_records(
|
|
self, *, prefix: str, predicate: Callable[[S3LogRecord], bool], first: list[S3LogRecord]
|
|
) -> list[S3LogRecord]:
|
|
"""Re-read at every poll interval until the settle window closes; a
|
|
duplicate ends the watch early because more waiting cannot clear it.
|
|
A transiently empty re-read never downgrades what was already seen."""
|
|
settle_deadline = time.monotonic() + S3_SETTLE_SECONDS
|
|
latest = first
|
|
while time.monotonic() < settle_deadline and len(latest) <= 1:
|
|
time.sleep(POLL_INTERVAL)
|
|
latest = self.records_matching(prefix=prefix, predicate=predicate) or latest
|
|
return latest
|
|
|
|
|
|
def build_s3_reader() -> S3LogReader:
|
|
bucket = os.environ.get("S3_LOGS_BUCKET_NAME", "")
|
|
if not bucket:
|
|
pytest.fail(
|
|
"S3_LOGS_BUCKET_NAME must be set: the s3 tests read the proxy's s3_v2 "
|
|
"delivery back from the real bucket (the cluster secret manager injects "
|
|
"it; locally set it in tests/e2e/.env to the same bucket "
|
|
"s3_callback_params.s3_bucket_name names)"
|
|
)
|
|
region = os.environ.get("AWS_REGION_NAME") or os.environ.get("AWS_REGION") or "us-east-1"
|
|
return S3LogReader(
|
|
bucket=bucket,
|
|
# boto3.client's overload set covers every AWS service; the ones without
|
|
# installed stubs type as Unknown, so the member is "partially unknown"
|
|
# even though the s3 overload itself resolves to S3Client.
|
|
client=boto3.client("s3", region_name=region), # pyright: ignore[reportUnknownMemberType]
|
|
)
|