refactor(s3-log): move the rust-backed s3 logger from s3_v3 to s3_v2 (#51893)

This commit is contained in:
Nate Armstrong 2026-11-14 22:24:23 -08:00 • committed by GitHub
parent b90ac69841
commit 3bcd77954e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 521 additions and 524 deletions

View file

@ -24,7 +24,7 @@ from litellm.types.utils import (
)
if TYPE_CHECKING:
from litellm.integrations.s3_v3 import S3Logger
from litellm.integrations.s3_v2 import S3Logger
BatchFileProvider: TypeAlias = Literal["openai", "azure", "vertex_ai", "hosted_vllm", "anthropic", "mistral"]
@ -242,7 +242,7 @@ async def _deliver(
def _resolve_logger(callback: "litellm.CALLBACK_TYPES | S3Logger") -> "CustomLogger | S3Logger | None":
from litellm.integrations.s3_v3 import S3Logger
from litellm.integrations.s3_v2 import S3Logger
match callback:
case CustomLogger() | S3Logger():

View file

@ -1,3 +1,481 @@
from litellm.integrations.s3_v3 import S3BatchUploadError, S3Logger
import asyncio
import copy
import json
import os
from collections.abc import Callable, Collection, Mapping
from typing import TYPE_CHECKING, Final
__all__ = ["S3BatchUploadError", "S3Logger"]
from typing_extensions import Unpack
import litellm
from litellm._logging import verbose_logger
from litellm.constants import (
COMPLETION_HTTP_FALLBACK_SECONDS,
DEFAULT_S3_BATCH_SIZE,
DEFAULT_S3_FLUSH_INTERVAL_SECONDS,
DEFAULT_S3_MAX_ADAPTIVE_CONCURRENCY,
DEFAULT_S3_MAX_CONCURRENT_UPLOADS,
HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS,
S3_UPLOAD_ATTEMPT_TIMEOUT_SECONDS,
)
from litellm.integrations.batch_line_item_objects import batch_line_items_ndjson, batch_line_items_object_name
from litellm.integrations.custom_batch_logger import CustomBatchLogger
from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.custom_logger_v2.call import CallContext
from litellm.integrations.custom_logger_v2.derivations import (
cold_storage_object_key,
log_id,
logged_key_identity,
mapping,
payload_copy,
)
from litellm.integrations.custom_logger_v2.enums import Field, Hook
from litellm.integrations.custom_logger_v2.field_sources import HookArgs
from litellm.integrations.custom_logger_v2.logger import CustomLoggerV2, on
from litellm.integrations.custom_logger_v2.outcomes import Abort, Patch
from litellm.integrations.custom_logger_v2.rust import RustLogger
from litellm.integrations.custom_logger_v2.selection import Selection
from litellm.integrations.s3 import resolve_s3_max_queue_size, resolve_s3_max_retry_age_seconds
from litellm.llms.bedrock.web_identity_session_policy import S3_LOGGING_SESSION_POLICY, session_policy_document
from litellm.secret_managers.main import get_secret, get_secret_str
from litellm.types.integrations.s3_v2 import S3LoggerSettings, S3PartitionGranularity
from litellm.types.utils import StandardAuditLogPayload, StandardBatchLineItemsPayload, StandardLoggingPayload
if TYPE_CHECKING:
from litellm.rust_bridge._native import NativeS3Log
CALLBACK_NAME: Final = "s3_v2"
STRIP_CALLBACK_NAME: Final = "s3_v2_strip_base64_files"
_WEB_IDENTITY_TOKEN_MIN_LENGTH: Final = 4
_DEFAULT_MAX_RETRY_AGE_SECONDS: Final = 3600
class S3BatchUploadError(Exception):
def __init__(self, failed: int, total: int) -> None:
self.failed: Final = failed
self.total: Final = total
super().__init__(f"{failed} of {total} S3 uploads failed; transient failures kept in queue for the next flush")
def _raise_batch_upload_error(failed: int, total: int) -> None:
raise S3BatchUploadError(failed=failed, total=total)
def batch_upload_failed(failure: tuple[int, int, int] | None) -> None:
if failure is None:
return
queued, failed, total = failure
try:
_raise_batch_upload_error(failed, total)
except S3BatchUploadError:
verbose_logger.exception(
"CustomLogger: async_send_batch raised; preserving %s events in queue for retry", queued
)
def _cold_storage_is_s3_v2() -> bool:
return litellm.cold_storage_custom_logger == "s3_v2"
def _region_from_environment() -> str | None:
region_name: Final = get_secret_str("AWS_REGION_NAME", None)
region: Final = get_secret_str("AWS_REGION", None)
return region if region is not None else region_name
class _Host:
def __init__(self) -> None:
self.failures: Final = CustomLogger()
def callback_failed(self) -> None:
self.failures.handle_callback_failure(callback_name="S3Logger")
def web_identity_token(self, reference: str) -> str:
if reference.startswith("os.environ/") or reference in os.environ:
raise ValueError("Invalid web identity token reference.")
match _read_secret(get_secret, reference):
case None:
raise ValueError("OIDC token could not be retrieved from secret manager.")
case str() as token if len(token) < _WEB_IDENTITY_TOKEN_MIN_LENGTH:
raise ValueError(
"Parameter validation failed:\nInvalid length for parameter WebIdentityToken, "
f"value: {len(token)}, valid min length: {_WEB_IDENTITY_TOKEN_MIN_LENGTH}"
)
case str() as token:
return token
case other:
raise ValueError(
"Parameter validation failed:\nInvalid type for parameter WebIdentityToken, "
f"value: {other}, type: {type(other)}, valid types: {str}"
)
def _read_secret(read: Callable[[str], object], reference: str) -> object:
return read(reference)
def _resolved_params(params_source: Mapping[str, object] | None) -> dict[str, object]: # mutable-ok: handed to Rust
configured: Final[Mapping[object, object] | None] = getattr(litellm, "s3_callback_params", None)
source: Final[Mapping[str, object] | Mapping[object, object]] = (
params_source if params_source is not None else (configured or {})
)
return {
key: (litellm.get_secret(value) if isinstance(value, str) and value.startswith("os.environ/") else value)
for key, value in source.items()
if isinstance(key, str)
}
def _constants(batch_size: int | None = DEFAULT_S3_BATCH_SIZE) -> Mapping[str, object]:
return {
"batch_size": batch_size or litellm.DEFAULT_BATCH_SIZE,
"max_concurrent_uploads": DEFAULT_S3_MAX_CONCURRENT_UPLOADS,
"max_adaptive_concurrency": DEFAULT_S3_MAX_ADAPTIVE_CONCURRENCY,
"upload_attempt_timeout_seconds": S3_UPLOAD_ATTEMPT_TIMEOUT_SECONDS,
"download_read_timeout_seconds": COMPLETION_HTTP_FALLBACK_SECONDS,
"download_connect_timeout_seconds": HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS,
"web_identity_session_policy": session_policy_document(S3_LOGGING_SESSION_POLICY),
}
def _flush_interval(flush_interval: float | None = DEFAULT_S3_FLUSH_INTERVAL_SECONDS) -> float:
return flush_interval or litellm.DEFAULT_FLUSH_INTERVAL_SECONDS
_CONFIGURED_FIRST: Final = frozenset(
{
"s3_bucket_name",
"s3_path",
"s3_region_name",
"s3_api_version",
"s3_endpoint_url",
"s3_aws_access_key_id",
"s3_aws_secret_access_key",
"s3_aws_session_token",
"s3_aws_session_name",
"s3_aws_profile_name",
"s3_aws_role_name",
"s3_aws_web_identity_token",
"s3_aws_sts_endpoint",
"s3_config",
"s3_server_side_encryption",
"s3_sse_kms_key_id",
}
)
_CONSTRUCTOR_ENABLES: Final = frozenset(
{
"s3_use_team_prefix",
"s3_strip_base64_files",
"s3_use_key_prefix",
"s3_use_virtual_hosted_style",
"s3_adaptive_concurrency",
"s3_batch_file_upload",
}
)
_CONFIGURED_UNLESS_NONE: Final = frozenset({"s3_use_ssl", "s3_verify", "s3_drop_on_terminal_error"})
_CONSTRUCTOR_UNLESS_NONE: Final = frozenset({"s3_log_prompts_only", "s3_partition_granularity"})
_CONFIGURED_UNLESS_BLANK: Final = frozenset({"s3_max_concurrent_uploads", "s3_max_adaptive_concurrency"})
_CONSTRUCTOR_DEFAULTS: Final[Mapping[str, object]] = {
"s3_use_ssl": True,
"s3_drop_on_terminal_error": True,
"s3_max_concurrent_uploads": DEFAULT_S3_MAX_CONCURRENT_UPLOADS,
"s3_max_retry_age_seconds": _DEFAULT_MAX_RETRY_AGE_SECONDS,
"s3_flush_interval": DEFAULT_S3_FLUSH_INTERVAL_SECONDS,
"s3_batch_size": DEFAULT_S3_BATCH_SIZE,
}
def _blank(value: object) -> bool:
return value is None or value == ""
def _constructor_wins(key: str, configured: object, value: object) -> bool:
return (
(key in _CONFIGURED_FIRST and not configured)
or (key in _CONFIGURED_UNLESS_NONE and configured is None)
or (key in _CONFIGURED_UNLESS_BLANK and _blank(configured))
or (key in _CONSTRUCTOR_UNLESS_NONE and value is not None)
)
_KEEP_CONFIGURED: Final = object()
def _constructor_value(key: str, configured: object, value: object) -> object:
if _constructor_wins(key, configured, value):
return value
if key in _CONSTRUCTOR_ENABLES and value:
return True
if key == "s3_max_queue_size" and value is not None:
return resolve_s3_max_queue_size(
configured, resolve_s3_max_queue_size(value, CustomBatchLogger.DEFAULT_MAX_QUEUE_SIZE)
)
if key == "s3_max_retry_age_seconds":
fallback: Final = resolve_s3_max_retry_age_seconds(value, _DEFAULT_MAX_RETRY_AGE_SECONDS)
resolved: Final = fallback if _blank(configured) else resolve_s3_max_retry_age_seconds(configured, fallback)
return 0 if resolved is None else resolved
return _KEEP_CONFIGURED
def _constructed_params(
params_source: Mapping[str, object] | None, settings: S3LoggerSettings
) -> dict[str, object]: # mutable-ok: handed to Rust
params: Final = _resolved_params(params_source)
overrides: Final = {
key: replaced
for key, value in settings.items()
if value != _CONSTRUCTOR_DEFAULTS.get(key, False if key in _CONSTRUCTOR_ENABLES else None)
and (replaced := _constructor_value(key, params.get(key), value)) is not _KEEP_CONFIGURED
}
return {**params, **overrides}
class _Base64Stripper(CustomLogger):
def strip(self, payload: StandardLoggingPayload) -> None:
_ = self._strip_base64_from_messages_sync(payload)
def stripped(self, messages: list[object]) -> list[dict[str, object]]:
return self._process_messages(messages=[_shallow_copy(message) for message in messages])
def _shallow_copy(message: object) -> object:
entries: Final = mapping(message)
return copy.copy(entries) if entries is not None and isinstance(message, dict) else message
_STRIPPER: Final = _Base64Stripper()
def legacy_should_log(kwargs: Mapping[str, object]) -> bool:
from litellm.litellm_core_utils import litellm_logging
if litellm.global_disable_no_log_param:
return True
litellm_params: Final = kwargs.get("litellm_params", {})
lookup: Final = getattr(litellm_params, "get", None)
if callable(lookup) and lookup("no-log", False) is True:
return False
return not litellm_logging.callback_disabled_dynamically(
callback=CALLBACK_NAME,
litellm_params=litellm_params,
standard_callback_dynamic_params=kwargs.get("standard_callback_dynamic_params") or {}, # pyright: ignore[reportArgumentType] # the dynamic params dict the legacy path hands the same check
)
def _payload_messages(payload: Mapping[str, object]) -> list[object]:
raw: Final = payload.get("messages")
return [*raw] if isinstance(raw, list) else []
class S3StripBase64Files(CustomLoggerV2):
selection = Selection(name=STRIP_CALLBACK_NAME)
def __init__(self, s3_logger_key: str) -> None:
self.s3_logger_key: Final = s3_logger_key
@on(
Hook.log_prepare,
reads={Field.deprecated_kwargs, Field.standard_logging_payload},
writes={Field.standard_logging_payload},
)
async def on_log_prepare(self, view: object, ctx: CallContext[object]) -> Patch | None:
from litellm.litellm_core_utils.litellm_logging import V2_ASYNC_LOGGING_LINE
kwargs: Final = mapping(getattr(view, Field.deprecated_kwargs.value, None))
payload: Final = mapping(getattr(view, Field.standard_logging_payload.value, None))
if not V2_ASYNC_LOGGING_LINE.get() or kwargs is None or payload is None or not legacy_should_log(kwargs):
return None
messages: Final = _payload_messages(payload)
if not messages:
return None
return Patch().merge(Field.standard_logging_payload, {"messages": _STRIPPER.stripped(messages)})
async def _periodic_flush(sink: "NativeS3Log", interval: float) -> None:
while True:
await asyncio.sleep(interval)
try:
batch_upload_failed(await sink.flush())
except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
raise
except BaseException as e: # noqa: BLE001 # a failed flush keeps the queue and the next interval tries again, as flush_queue does
verbose_logger.exception("CustomLogger: periodic flush failed: %s", e)
def _excluding_fields(kwargs: object) -> Mapping[str, object] | None:
excluded: Final[object] = getattr(litellm, "standard_logging_payload_excluded_fields", None)
details: Final = mapping(kwargs)
logged: Final = None if details is None else mapping(details.get("standard_logging_object"))
if details is None or logged is None or not isinstance(excluded, Collection) or not excluded:
return None
if logged.get("status") == "failure":
return None
return {**details, "standard_logging_object": {key: value for key, value in logged.items() if key not in excluded}}
def _logged_payload(kwargs: object, details: Mapping[str, object] | None) -> object:
source: Final = mapping(kwargs) if details is None else details
return None if source is None else source.get("standard_logging_object")
def _legacy_kwargs(view: object) -> object:
return getattr(view, Field.deprecated_kwargs.value, None)
_read_attribute: Final[Callable[[object, str], object]] = getattr
class _ExcludedView:
def __init__(self, view: object, details: Mapping[str, object]) -> None:
args: Final = HookArgs(request=details)
self.view: Final = view
self.fields: Final[Mapping[str, object]] = {
Field.deprecated_kwargs.value: details,
Field.standard_logging_payload_ref.value: payload_copy(args),
Field.log_id.value: log_id(args),
Field.cold_storage_object_key.value: cold_storage_object_key(args),
Field.logged_key_identity.value: logged_key_identity(args),
}
def __getattr__(self, name: str) -> object:
if name not in self.fields:
return _read_attribute(self.view, name)
value: Final = self.fields[name]
if value is None:
raise AttributeError(name)
return value
_ATTEMPT_ENDED_READS: Final = frozenset(
{
Field.deprecated_kwargs,
Field.attempt_timing,
Field.log_id,
Field.cold_storage_object_key,
Field.standard_logging_payload_ref,
Field.logged_key_identity,
}
)
class S3Logger(RustLogger):
selection = Selection(name=CALLBACK_NAME, honors_no_log=True)
def __init__(
self,
s3_callback_params_override: Mapping[str, object] | None = None,
**settings: Unpack[S3LoggerSettings], # kwargs-ok: the S3Logger constructor keywords
) -> None:
from litellm.rust_bridge._native import NativeS3Log
host: Final = _Host()
params: Final = _constructed_params(s3_callback_params_override, settings)
sink: Final = NativeS3Log(
params,
_constants(settings.get("s3_batch_size", DEFAULT_S3_BATCH_SIZE)),
host,
_cold_storage_is_s3_v2(),
None if params.get("s3_region_name") else _region_from_environment(),
)
self.s3_region_name: Final[object] = params.get("s3_region_name") or settings.get("s3_region_name")
self.sink: Final = sink
self.s3_host: Final = host
self.periodic_flush_task: Final = asyncio.create_task(
_periodic_flush(sink, _flush_interval(settings.get("s3_flush_interval", DEFAULT_S3_FLUSH_INTERVAL_SECONDS)))
)
super().__init__(sink.callback(), {Field.deprecated_kwargs: lambda _kwargs: None})
self.v2_companion: Final = (
S3StripBase64Files(litellm.logging_callback_manager.get_custom_logger_key(self))
if sink.strip_base64_files
else None
)
@on(Hook.attempt_ended, reads=_ATTEMPT_ENDED_READS, awaited=True)
async def on_attempt_ended(self, view: object, ctx: CallContext[object]) -> Patch | Abort | None:
return await self.run_rust(Hook.attempt_ended, _ATTEMPT_ENDED_READS, view, ctx)
async def run_rust(
self, hook: Hook, reads: frozenset[Field], view: object, ctx: CallContext[object]
) -> Patch | Abort | None:
from litellm.litellm_core_utils.litellm_logging import V2_ASYNC_LOGGING_LINE
kwargs: Final = getattr(view, Field.deprecated_kwargs.value, None)
if not V2_ASYNC_LOGGING_LINE.get() or not isinstance(kwargs, Mapping) or not legacy_should_log(kwargs): # pyright: ignore[reportUnknownArgumentType] # the legacy kwargs dict
return None
payload: Final[object] = kwargs.get("standard_logging_object") # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # the legacy kwargs dict
if payload is not None and not isinstance(payload, Mapping):
return self._failed(AttributeError(f"'{type(payload).__name__}' object has no attribute 'get'")) # pyright: ignore[reportUnknownArgumentType] # any non-mapping payload
try:
raw: Final = _legacy_kwargs(view)
details: Final = _excluding_fields(raw)
logged: Final = _logged_payload(raw, details)
if self.sink.strip_base64_files and isinstance(logged, Mapping):
_STRIPPER.strip(logged) # pyright: ignore[reportArgumentType] # the shared StandardLoggingPayload, stripped in place as S3Logger does
return await super().run_rust(hook, reads, view if details is None else _ExcludedView(view, details), ctx)
except Exception as e: # noqa: BLE001 # S3Logger counts a failure to build the element and moves on
return self._failed(e)
def _failed(self, error: Exception) -> None:
verbose_logger.error("s3 Layer Error - %s", error, exc_info=error)
self.s3_host.callback_failed()
@property
def s3_path(self) -> str | None:
return self.sink.s3_path
@property
def s3_bucket_name(self) -> str | None:
return self.sink.s3_bucket_name
def resolve_partition_granularity(self) -> S3PartitionGranularity:
return "hour" if self.sink.partition_granularity() == "hour" else "day"
async def flush_queue(self) -> None:
batch_upload_failed(await self.sink.flush())
async def async_log_audit_log_event(self, audit_log: StandardAuditLogPayload) -> None:
try:
audit_log_id: Final = audit_log.get("id", "unknown")
batch_upload_failed(await self.sink.submit_audit(dict(audit_log), f"{audit_log_id}"))
except Exception as e: # noqa: BLE001 # an audit log failure is logged and swallowed, as in S3Logger
verbose_logger.exception("S3 audit log error: %s", e)
async def async_log_batch_line_items(self, payload: StandardBatchLineItemsPayload) -> None:
object_name: Final = batch_line_items_object_name(payload)
s3_path: Final = self.s3_path
s3_object_key: Final = (f"{s3_path.rstrip('/')}/" if s3_path else "") + object_name
body: Final = await asyncio.to_thread(batch_line_items_ndjson, payload)
delivered: Final = await self.sink.upload_line_items(
s3_object_key, body, "-".join(object_name.rsplit("/", 2)[-2:])
)
if not delivered:
verbose_logger.error(
"s3 logging: the %d line items of part %d of batch %s were not stored at %s",
len(payload["lines"]),
payload["part"],
payload["batch_id"],
s3_object_key,
)
async def _download_object_from_s3(self, s3_object_key: str) -> object:
try:
downloaded: Final = await self.sink.download(s3_object_key)
return None if downloaded is None else json.loads(downloaded)
except Exception as e: # noqa: BLE001 # a failed read is None, as in S3Logger
verbose_logger.exception("Error downloading from S3: %s", e)
return None
async def get_proxy_server_request_from_cold_storage_with_object_key(self, object_key: str) -> object:
try:
return await self._download_object_from_s3(object_key)
except Exception as e: # noqa: BLE001 # a failed read is None, as in S3Logger
verbose_logger.exception("Error retrieving object %s from cold storage: %s", object_key, e)
return None
__all__ = [
"S3BatchUploadError",
"S3Logger",
"S3StripBase64Files",
"legacy_should_log",
]

View file

@ -1,481 +0,0 @@
import asyncio
import copy
import json
import os
from collections.abc import Callable, Collection, Mapping
from typing import TYPE_CHECKING, Final
from typing_extensions import Unpack
import litellm
from litellm._logging import verbose_logger
from litellm.constants import (
COMPLETION_HTTP_FALLBACK_SECONDS,
DEFAULT_S3_BATCH_SIZE,
DEFAULT_S3_FLUSH_INTERVAL_SECONDS,
DEFAULT_S3_MAX_ADAPTIVE_CONCURRENCY,
DEFAULT_S3_MAX_CONCURRENT_UPLOADS,
HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS,
S3_UPLOAD_ATTEMPT_TIMEOUT_SECONDS,
)
from litellm.integrations.batch_line_item_objects import batch_line_items_ndjson, batch_line_items_object_name
from litellm.integrations.custom_batch_logger import CustomBatchLogger
from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.custom_logger_v2.call import CallContext
from litellm.integrations.custom_logger_v2.derivations import (
cold_storage_object_key,
log_id,
logged_key_identity,
mapping,
payload_copy,
)
from litellm.integrations.custom_logger_v2.enums import Field, Hook
from litellm.integrations.custom_logger_v2.field_sources import HookArgs
from litellm.integrations.custom_logger_v2.logger import CustomLoggerV2, on
from litellm.integrations.custom_logger_v2.outcomes import Abort, Patch
from litellm.integrations.custom_logger_v2.rust import RustLogger
from litellm.integrations.custom_logger_v2.selection import Selection
from litellm.integrations.s3 import resolve_s3_max_queue_size, resolve_s3_max_retry_age_seconds
from litellm.llms.bedrock.web_identity_session_policy import S3_LOGGING_SESSION_POLICY, session_policy_document
from litellm.secret_managers.main import get_secret, get_secret_str
from litellm.types.integrations.s3_v2 import S3LoggerSettings, S3PartitionGranularity
from litellm.types.utils import StandardAuditLogPayload, StandardBatchLineItemsPayload, StandardLoggingPayload
if TYPE_CHECKING:
from litellm.rust_bridge._native import NativeS3Log
CALLBACK_NAME: Final = "s3_v2"
STRIP_CALLBACK_NAME: Final = "s3_v2_strip_base64_files"
_WEB_IDENTITY_TOKEN_MIN_LENGTH: Final = 4
_DEFAULT_MAX_RETRY_AGE_SECONDS: Final = 3600
class S3BatchUploadError(Exception):
def __init__(self, failed: int, total: int) -> None:
self.failed: Final = failed
self.total: Final = total
super().__init__(f"{failed} of {total} S3 uploads failed; transient failures kept in queue for the next flush")
def _raise_batch_upload_error(failed: int, total: int) -> None:
raise S3BatchUploadError(failed=failed, total=total)
def batch_upload_failed(failure: tuple[int, int, int] | None) -> None:
if failure is None:
return
queued, failed, total = failure
try:
_raise_batch_upload_error(failed, total)
except S3BatchUploadError:
verbose_logger.exception(
"CustomLogger: async_send_batch raised; preserving %s events in queue for retry", queued
)
def _cold_storage_is_s3_v2() -> bool:
return litellm.cold_storage_custom_logger == "s3_v2"
def _region_from_environment() -> str | None:
region_name: Final = get_secret_str("AWS_REGION_NAME", None)
region: Final = get_secret_str("AWS_REGION", None)
return region if region is not None else region_name
class _Host:
def __init__(self) -> None:
self.failures: Final = CustomLogger()
def callback_failed(self) -> None:
self.failures.handle_callback_failure(callback_name="S3Logger")
def web_identity_token(self, reference: str) -> str:
if reference.startswith("os.environ/") or reference in os.environ:
raise ValueError("Invalid web identity token reference.")
match _read_secret(get_secret, reference):
case None:
raise ValueError("OIDC token could not be retrieved from secret manager.")
case str() as token if len(token) < _WEB_IDENTITY_TOKEN_MIN_LENGTH:
raise ValueError(
"Parameter validation failed:\nInvalid length for parameter WebIdentityToken, "
f"value: {len(token)}, valid min length: {_WEB_IDENTITY_TOKEN_MIN_LENGTH}"
)
case str() as token:
return token
case other:
raise ValueError(
"Parameter validation failed:\nInvalid type for parameter WebIdentityToken, "
f"value: {other}, type: {type(other)}, valid types: {str}"
)
def _read_secret(read: Callable[[str], object], reference: str) -> object:
return read(reference)
def _resolved_params(params_source: Mapping[str, object] | None) -> dict[str, object]: # mutable-ok: handed to Rust
configured: Final[Mapping[object, object] | None] = getattr(litellm, "s3_callback_params", None)
source: Final[Mapping[str, object] | Mapping[object, object]] = (
params_source if params_source is not None else (configured or {})
)
return {
key: (litellm.get_secret(value) if isinstance(value, str) and value.startswith("os.environ/") else value)
for key, value in source.items()
if isinstance(key, str)
}
def _constants(batch_size: int | None = DEFAULT_S3_BATCH_SIZE) -> Mapping[str, object]:
return {
"batch_size": batch_size or litellm.DEFAULT_BATCH_SIZE,
"max_concurrent_uploads": DEFAULT_S3_MAX_CONCURRENT_UPLOADS,
"max_adaptive_concurrency": DEFAULT_S3_MAX_ADAPTIVE_CONCURRENCY,
"upload_attempt_timeout_seconds": S3_UPLOAD_ATTEMPT_TIMEOUT_SECONDS,
"download_read_timeout_seconds": COMPLETION_HTTP_FALLBACK_SECONDS,
"download_connect_timeout_seconds": HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS,
"web_identity_session_policy": session_policy_document(S3_LOGGING_SESSION_POLICY),
}
def _flush_interval(flush_interval: float | None = DEFAULT_S3_FLUSH_INTERVAL_SECONDS) -> float:
return flush_interval or litellm.DEFAULT_FLUSH_INTERVAL_SECONDS
_CONFIGURED_FIRST: Final = frozenset(
{
"s3_bucket_name",
"s3_path",
"s3_region_name",
"s3_api_version",
"s3_endpoint_url",
"s3_aws_access_key_id",
"s3_aws_secret_access_key",
"s3_aws_session_token",
"s3_aws_session_name",
"s3_aws_profile_name",
"s3_aws_role_name",
"s3_aws_web_identity_token",
"s3_aws_sts_endpoint",
"s3_config",
"s3_server_side_encryption",
"s3_sse_kms_key_id",
}
)
_CONSTRUCTOR_ENABLES: Final = frozenset(
{
"s3_use_team_prefix",
"s3_strip_base64_files",
"s3_use_key_prefix",
"s3_use_virtual_hosted_style",
"s3_adaptive_concurrency",
"s3_batch_file_upload",
}
)
_CONFIGURED_UNLESS_NONE: Final = frozenset({"s3_use_ssl", "s3_verify", "s3_drop_on_terminal_error"})
_CONSTRUCTOR_UNLESS_NONE: Final = frozenset({"s3_log_prompts_only", "s3_partition_granularity"})
_CONFIGURED_UNLESS_BLANK: Final = frozenset({"s3_max_concurrent_uploads", "s3_max_adaptive_concurrency"})
_CONSTRUCTOR_DEFAULTS: Final[Mapping[str, object]] = {
"s3_use_ssl": True,
"s3_drop_on_terminal_error": True,
"s3_max_concurrent_uploads": DEFAULT_S3_MAX_CONCURRENT_UPLOADS,
"s3_max_retry_age_seconds": _DEFAULT_MAX_RETRY_AGE_SECONDS,
"s3_flush_interval": DEFAULT_S3_FLUSH_INTERVAL_SECONDS,
"s3_batch_size": DEFAULT_S3_BATCH_SIZE,
}
def _blank(value: object) -> bool:
return value is None or value == ""
def _constructor_wins(key: str, configured: object, value: object) -> bool:
return (
(key in _CONFIGURED_FIRST and not configured)
or (key in _CONFIGURED_UNLESS_NONE and configured is None)
or (key in _CONFIGURED_UNLESS_BLANK and _blank(configured))
or (key in _CONSTRUCTOR_UNLESS_NONE and value is not None)
)
_KEEP_CONFIGURED: Final = object()
def _constructor_value(key: str, configured: object, value: object) -> object:
if _constructor_wins(key, configured, value):
return value
if key in _CONSTRUCTOR_ENABLES and value:
return True
if key == "s3_max_queue_size" and value is not None:
return resolve_s3_max_queue_size(
configured, resolve_s3_max_queue_size(value, CustomBatchLogger.DEFAULT_MAX_QUEUE_SIZE)
)
if key == "s3_max_retry_age_seconds":
fallback: Final = resolve_s3_max_retry_age_seconds(value, _DEFAULT_MAX_RETRY_AGE_SECONDS)
resolved: Final = fallback if _blank(configured) else resolve_s3_max_retry_age_seconds(configured, fallback)
return 0 if resolved is None else resolved
return _KEEP_CONFIGURED
def _constructed_params(
params_source: Mapping[str, object] | None, settings: S3LoggerSettings
) -> dict[str, object]: # mutable-ok: handed to Rust
params: Final = _resolved_params(params_source)
overrides: Final = {
key: replaced
for key, value in settings.items()
if value != _CONSTRUCTOR_DEFAULTS.get(key, False if key in _CONSTRUCTOR_ENABLES else None)
and (replaced := _constructor_value(key, params.get(key), value)) is not _KEEP_CONFIGURED
}
return {**params, **overrides}
class _Base64Stripper(CustomLogger):
def strip(self, payload: StandardLoggingPayload) -> None:
_ = self._strip_base64_from_messages_sync(payload)
def stripped(self, messages: list[object]) -> list[dict[str, object]]:
return self._process_messages(messages=[_shallow_copy(message) for message in messages])
def _shallow_copy(message: object) -> object:
entries: Final = mapping(message)
return copy.copy(entries) if entries is not None and isinstance(message, dict) else message
_STRIPPER: Final = _Base64Stripper()
def legacy_should_log(kwargs: Mapping[str, object]) -> bool:
from litellm.litellm_core_utils import litellm_logging
if litellm.global_disable_no_log_param:
return True
litellm_params: Final = kwargs.get("litellm_params", {})
lookup: Final = getattr(litellm_params, "get", None)
if callable(lookup) and lookup("no-log", False) is True:
return False
return not litellm_logging.callback_disabled_dynamically(
callback=CALLBACK_NAME,
litellm_params=litellm_params,
standard_callback_dynamic_params=kwargs.get("standard_callback_dynamic_params") or {}, # pyright: ignore[reportArgumentType] # the dynamic params dict the legacy path hands the same check
)
def _payload_messages(payload: Mapping[str, object]) -> list[object]:
raw: Final = payload.get("messages")
return [*raw] if isinstance(raw, list) else []
class S3StripBase64Files(CustomLoggerV2):
selection = Selection(name=STRIP_CALLBACK_NAME)
def __init__(self, s3_logger_key: str) -> None:
self.s3_logger_key: Final = s3_logger_key
@on(
Hook.log_prepare,
reads={Field.deprecated_kwargs, Field.standard_logging_payload},
writes={Field.standard_logging_payload},
)
async def on_log_prepare(self, view: object, ctx: CallContext[object]) -> Patch | None:
from litellm.litellm_core_utils.litellm_logging import V2_ASYNC_LOGGING_LINE
kwargs: Final = mapping(getattr(view, Field.deprecated_kwargs.value, None))
payload: Final = mapping(getattr(view, Field.standard_logging_payload.value, None))
if not V2_ASYNC_LOGGING_LINE.get() or kwargs is None or payload is None or not legacy_should_log(kwargs):
return None
messages: Final = _payload_messages(payload)
if not messages:
return None
return Patch().merge(Field.standard_logging_payload, {"messages": _STRIPPER.stripped(messages)})
async def _periodic_flush(sink: "NativeS3Log", interval: float) -> None:
while True:
await asyncio.sleep(interval)
try:
batch_upload_failed(await sink.flush())
except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
raise
except BaseException as e: # noqa: BLE001 # a failed flush keeps the queue and the next interval tries again, as flush_queue does
verbose_logger.exception("CustomLogger: periodic flush failed: %s", e)
def _excluding_fields(kwargs: object) -> Mapping[str, object] | None:
excluded: Final[object] = getattr(litellm, "standard_logging_payload_excluded_fields", None)
details: Final = mapping(kwargs)
logged: Final = None if details is None else mapping(details.get("standard_logging_object"))
if details is None or logged is None or not isinstance(excluded, Collection) or not excluded:
return None
if logged.get("status") == "failure":
return None
return {**details, "standard_logging_object": {key: value for key, value in logged.items() if key not in excluded}}
def _logged_payload(kwargs: object, details: Mapping[str, object] | None) -> object:
source: Final = mapping(kwargs) if details is None else details
return None if source is None else source.get("standard_logging_object")
def _legacy_kwargs(view: object) -> object:
return getattr(view, Field.deprecated_kwargs.value, None)
_read_attribute: Final[Callable[[object, str], object]] = getattr
class _ExcludedView:
def __init__(self, view: object, details: Mapping[str, object]) -> None:
args: Final = HookArgs(request=details)
self.view: Final = view
self.fields: Final[Mapping[str, object]] = {
Field.deprecated_kwargs.value: details,
Field.standard_logging_payload_ref.value: payload_copy(args),
Field.log_id.value: log_id(args),
Field.cold_storage_object_key.value: cold_storage_object_key(args),
Field.logged_key_identity.value: logged_key_identity(args),
}
def __getattr__(self, name: str) -> object:
if name not in self.fields:
return _read_attribute(self.view, name)
value: Final = self.fields[name]
if value is None:
raise AttributeError(name)
return value
_ATTEMPT_ENDED_READS: Final = frozenset(
{
Field.deprecated_kwargs,
Field.attempt_timing,
Field.log_id,
Field.cold_storage_object_key,
Field.standard_logging_payload_ref,
Field.logged_key_identity,
}
)
class S3Logger(RustLogger):
selection = Selection(name=CALLBACK_NAME, honors_no_log=True)
def __init__(
self,
s3_callback_params_override: Mapping[str, object] | None = None,
**settings: Unpack[S3LoggerSettings], # kwargs-ok: the S3Logger constructor keywords
) -> None:
from litellm.rust_bridge._native import NativeS3Log
host: Final = _Host()
params: Final = _constructed_params(s3_callback_params_override, settings)
sink: Final = NativeS3Log(
params,
_constants(settings.get("s3_batch_size", DEFAULT_S3_BATCH_SIZE)),
host,
_cold_storage_is_s3_v2(),
None if params.get("s3_region_name") else _region_from_environment(),
)
self.s3_region_name: Final[object] = params.get("s3_region_name") or settings.get("s3_region_name")
self.sink: Final = sink
self.s3_host: Final = host
self.periodic_flush_task: Final = asyncio.create_task(
_periodic_flush(sink, _flush_interval(settings.get("s3_flush_interval", DEFAULT_S3_FLUSH_INTERVAL_SECONDS)))
)
super().__init__(sink.callback(), {Field.deprecated_kwargs: lambda _kwargs: None})
self.v2_companion: Final = (
S3StripBase64Files(litellm.logging_callback_manager.get_custom_logger_key(self))
if sink.strip_base64_files
else None
)
@on(Hook.attempt_ended, reads=_ATTEMPT_ENDED_READS, awaited=True)
async def on_attempt_ended(self, view: object, ctx: CallContext[object]) -> Patch | Abort | None:
return await self.run_rust(Hook.attempt_ended, _ATTEMPT_ENDED_READS, view, ctx)
async def run_rust(
self, hook: Hook, reads: frozenset[Field], view: object, ctx: CallContext[object]
) -> Patch | Abort | None:
from litellm.litellm_core_utils.litellm_logging import V2_ASYNC_LOGGING_LINE
kwargs: Final = getattr(view, Field.deprecated_kwargs.value, None)
if not V2_ASYNC_LOGGING_LINE.get() or not isinstance(kwargs, Mapping) or not legacy_should_log(kwargs): # pyright: ignore[reportUnknownArgumentType] # the legacy kwargs dict
return None
payload: Final[object] = kwargs.get("standard_logging_object") # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # the legacy kwargs dict
if payload is not None and not isinstance(payload, Mapping):
return self._failed(AttributeError(f"'{type(payload).__name__}' object has no attribute 'get'")) # pyright: ignore[reportUnknownArgumentType] # any non-mapping payload
try:
raw: Final = _legacy_kwargs(view)
details: Final = _excluding_fields(raw)
logged: Final = _logged_payload(raw, details)
if self.sink.strip_base64_files and isinstance(logged, Mapping):
_STRIPPER.strip(logged) # pyright: ignore[reportArgumentType] # the shared StandardLoggingPayload, stripped in place as S3Logger does
return await super().run_rust(hook, reads, view if details is None else _ExcludedView(view, details), ctx)
except Exception as e: # noqa: BLE001 # S3Logger counts a failure to build the element and moves on
return self._failed(e)
def _failed(self, error: Exception) -> None:
verbose_logger.error("s3 Layer Error - %s", error, exc_info=error)
self.s3_host.callback_failed()
@property
def s3_path(self) -> str | None:
return self.sink.s3_path
@property
def s3_bucket_name(self) -> str | None:
return self.sink.s3_bucket_name
def resolve_partition_granularity(self) -> S3PartitionGranularity:
return "hour" if self.sink.partition_granularity() == "hour" else "day"
async def flush_queue(self) -> None:
batch_upload_failed(await self.sink.flush())
async def async_log_audit_log_event(self, audit_log: StandardAuditLogPayload) -> None:
try:
audit_log_id: Final = audit_log.get("id", "unknown")
batch_upload_failed(await self.sink.submit_audit(dict(audit_log), f"{audit_log_id}"))
except Exception as e: # noqa: BLE001 # an audit log failure is logged and swallowed, as in S3Logger
verbose_logger.exception("S3 audit log error: %s", e)
async def async_log_batch_line_items(self, payload: StandardBatchLineItemsPayload) -> None:
object_name: Final = batch_line_items_object_name(payload)
s3_path: Final = self.s3_path
s3_object_key: Final = (f"{s3_path.rstrip('/')}/" if s3_path else "") + object_name
body: Final = await asyncio.to_thread(batch_line_items_ndjson, payload)
delivered: Final = await self.sink.upload_line_items(
s3_object_key, body, "-".join(object_name.rsplit("/", 2)[-2:])
)
if not delivered:
verbose_logger.error(
"s3 logging: the %d line items of part %d of batch %s were not stored at %s",
len(payload["lines"]),
payload["part"],
payload["batch_id"],
s3_object_key,
)
async def _download_object_from_s3(self, s3_object_key: str) -> object:
try:
downloaded: Final = await self.sink.download(s3_object_key)
return None if downloaded is None else json.loads(downloaded)
except Exception as e: # noqa: BLE001 # a failed read is None, as in S3Logger
verbose_logger.exception("Error downloading from S3: %s", e)
return None
async def get_proxy_server_request_from_cold_storage_with_object_key(self, object_key: str) -> object:
try:
return await self._download_object_from_s3(object_key)
except Exception as e: # noqa: BLE001 # a failed read is None, as in S3Logger
verbose_logger.exception("Error retrieving object %s from cold storage: %s", object_key, e)
return None
__all__ = [
"S3BatchUploadError",
"S3Logger",
"S3StripBase64Files",
"legacy_should_log",
]

View file

@ -46,7 +46,7 @@ from litellm.integrations.opik.opik import OpikLogger
from litellm.integrations.pointfive import PointFiveLogger
from litellm.integrations.posthog import PostHogLogger
from litellm.integrations.prometheus import PrometheusLogger
from litellm.integrations.s3_v3 import S3Logger
from litellm.integrations.s3_v2 import S3Logger
from litellm.integrations.sqs import SQSLogger
from litellm.integrations.ternary.ternary_logger import TernaryLogger
from litellm.integrations.vantage.vantage_logger import VantageLogger

View file

@ -6536,7 +6536,7 @@ def build_custom_logger_compatible_class(
_in_memory_loggers.append(_gcs_bucket_logger)
return _gcs_bucket_logger
elif logging_integration == "s3_v2":
from litellm.integrations.s3_v3 import S3Logger as S3V2Logger
from litellm.integrations.s3_v2 import S3Logger as S3V2Logger
for callback in _in_memory_loggers:
if isinstance(callback, S3V2Logger):
@ -7360,7 +7360,7 @@ def get_custom_logger_compatible_class(
if isinstance(callback, GCSBucketLogger):
return callback
elif logging_integration == "s3_v2":
from litellm.integrations.s3_v3 import S3Logger as S3V2Logger
from litellm.integrations.s3_v2 import S3Logger as S3V2Logger
for callback in _in_memory_loggers:
if isinstance(callback, S3V2Logger):
@ -8067,7 +8067,7 @@ class StandardLoggingPayloadSetup:
)
if custom_logger and hasattr(custom_logger, "s3_path") and getattr(custom_logger, "s3_path"):
s3_path = getattr(custom_logger, "s3_path")
from litellm.integrations.s3_v3 import S3Logger as S3V2Logger
from litellm.integrations.s3_v2 import S3Logger as S3V2Logger
if isinstance(custom_logger, S3V2Logger):
partition_granularity = custom_logger.resolve_partition_granularity()

View file

@ -22,7 +22,7 @@ from litellm.repositories.table_repositories import AuditLogRepository
from litellm.types.utils import StandardAuditLogPayload
if TYPE_CHECKING:
from litellm.integrations.s3_v3 import S3Logger
from litellm.integrations.s3_v2 import S3Logger
_audit_log_callback_cache: Final[dict[str, "CustomLogger | S3Logger"]] = {}
ALLOW_LITELLM_CHANGED_BY_HEADER_METADATA_KEY: Final = "allow_litellm_changed_by_header"
@ -78,9 +78,9 @@ def _resolve_audit_log_callback(name: str) -> "CustomLogger | S3Logger | None":
instance: CustomLogger | S3Logger | None
audit_params: Final[Mapping[str, object] | None] = getattr(litellm, "s3_audit_callback_params", None)
if name == "s3_v2" and audit_params is not None:
from litellm.integrations import s3_v3
from litellm.integrations import s3_v2
instance = s3_v3.S3Logger(s3_callback_params_override=audit_params)
instance = s3_v2.S3Logger(s3_callback_params_override=audit_params)
else:
from litellm.litellm_core_utils.litellm_logging import (
_init_custom_logger_compatible_class,

View file

@ -7452,7 +7452,7 @@ class ProxyConfig:
if key == "request_timeout":
litellm.request_timeout_explicitly_set = True
if key in {"s3_audit_callback_params", "s3_callback_params"}:
from litellm.integrations.s3_v3 import S3Logger as S3V2Logger
from litellm.integrations.s3_v2 import S3Logger as S3V2Logger
from litellm.litellm_core_utils.litellm_logging import (
_in_memory_loggers,
)

View file

@ -1,7 +1,7 @@
# Logging integration delivery (behavior features). Grounded in litellm/integrations/.
- {id: logging.s3.success.writes_object, module: logging, tier: P0, event: success, assertions: [writes_object], exercised_on: [chat_completions, messages, embeddings], source: "integrations/s3_v3.py", rationale: "Primary audit trail; batch flush no-drop"}
- {id: logging.s3.failure.writes_object, module: logging, tier: P0, event: failure, assertions: [writes_object], exercised_on: [chat_completions, messages], source: "integrations/s3_v3.py", rationale: "Failed calls persisted for compliance"}
- {id: logging.s3.success.partition_layout, module: logging, tier: P1, event: success, assertions: [object_key_layout], exercised_on: [chat_completions], source: "integrations/s3_v3.py / LIT-8985", rationale: "s3_partition_granularity picks the date or date/hour folder every downstream query and lifecycle rule reads"}
- {id: logging.s3.success.writes_object, module: logging, tier: P0, event: success, assertions: [writes_object], exercised_on: [chat_completions, messages, embeddings], source: "integrations/s3_v2.py", rationale: "Primary audit trail; batch flush no-drop"}
- {id: logging.s3.failure.writes_object, module: logging, tier: P0, event: failure, assertions: [writes_object], exercised_on: [chat_completions, messages], source: "integrations/s3_v2.py", rationale: "Failed calls persisted for compliance"}
- {id: logging.s3.success.partition_layout, module: logging, tier: P1, event: success, assertions: [object_key_layout], exercised_on: [chat_completions], source: "integrations/s3_v2.py / LIT-8985", rationale: "s3_partition_granularity picks the date or date/hour folder every downstream query and lifecycle rule reads"}
- {id: logging.gcs_bucket.success.writes_object, module: logging, tier: P0, event: success, assertions: [writes_object], exercised_on: [chat_completions, messages, embeddings], source: "integrations/gcs_bucket/gcs_bucket.py", rationale: "GCS parallel to S3"}
- {id: logging.datadog.success.exports_metric, module: logging, tier: P0, event: success, assertions: [exports_metric], exercised_on: [chat_completions, messages, responses, embeddings], source: "integrations/datadog/datadog.py", rationale: "Powers dashboards/alerts; cardinality regressions common"}
- {id: logging.datadog.stream.exports_metric, module: logging, tier: P0, event: stream, assertions: [exports_metric], exercised_on: [chat_completions, messages, responses], source: "integrations/datadog/datadog.py", rationale: "Streaming aggregates usage after the last chunk; delivery and cost must survive that path"}

View file

@ -16,7 +16,7 @@ from litellm.integrations.custom_logger_v2.enforcement import FieldView
from litellm.integrations.custom_logger_v2.field_sources import HookArgs, with_sources
from litellm.integrations.custom_logger_v2.legacy import KwargsFields
from litellm.integrations.custom_logger_v2.rust import WireManifest
from litellm.integrations.s3_v3 import S3Logger
from litellm.integrations.s3_v2 import S3Logger
from litellm.litellm_core_utils.litellm_logging import LOGGING_KEYS, V2_ASYNC_LOGGING_LINE
from litellm.types.integrations.custom_logger_v2 import Outcome

View file

@ -21,7 +21,7 @@ import pytest
from pydantic import BaseModel
import litellm
from litellm.integrations import s3_v3
from litellm.integrations import s3_v2
from litellm.integrations.custom_logger_v2.derivations import DERIVED_ATTEMPT_FIELDS
from litellm.integrations.custom_logger_v2.enforcement import FieldView
from litellm.integrations.custom_logger_v2.enums import Field, Hook
@ -29,7 +29,7 @@ from litellm.integrations.custom_logger_v2.host import request_scope
from litellm.integrations.custom_logger_v2.legacy import KwargsFields
from litellm.integrations.custom_logger_v2.field_sources import HookArgs, with_sources
from litellm.integrations.custom_logger_v2.selection import RequestSelection
from litellm.integrations.s3_v3 import S3Logger
from litellm.integrations.s3_v2 import S3Logger
from litellm.litellm_core_utils.litellm_logging import LOGGING_KEYS, V2_ASYNC_LOGGING_LINE
from litellm.llms.bedrock import base_aws_llm
from litellm.types.integrations.custom_logger_v2 import Outcome
@ -505,7 +505,7 @@ async def test_both_loggers_skip_and_count_the_same_bad_payloads(
golden: Golden, bucket: FakeBucket, monkeypatch: pytest.MonkeyPatch, payload: object
) -> None:
logger = await _logger(monkeypatch, bucket, {"s3_use_team_prefix": True})
with patch.object(s3_v3._Host, "callback_failed") as rust_failures:
with patch.object(s3_v2._Host, "callback_failed") as rust_failures:
await _log(logger, payload)
await logger.flush_queue()
await asyncio.sleep(0.2)
@ -703,7 +703,7 @@ def test_both_loggers_need_a_running_event_loop(golden: Golden, monkeypatch: pyt
async def test_the_native_logger_flushes_on_its_own_after_the_interval(
golden: Golden, bucket: FakeBucket, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(s3_v3, "DEFAULT_S3_FLUSH_INTERVAL_SECONDS", 0.05)
monkeypatch.setattr(s3_v2, "DEFAULT_S3_FLUSH_INTERVAL_SECONDS", 0.05)
logger = await _logger(monkeypatch, bucket, {})
await _log_native(logger, {"standard_logging_object": _payload()})
for _ in range(100):
@ -735,7 +735,7 @@ async def test_without_a_region_param_both_sign_with_the_same_environment_region
for name, value in environment.items():
monkeypatch.setenv(name, value)
logger = await _logger(monkeypatch, bucket, {"s3_region_name": None})
with patch.object(s3_v3._Host, "callback_failed") as rust_failures:
with patch.object(s3_v2._Host, "callback_failed") as rust_failures:
await _log(logger, _payload())
await logger.flush_queue()
await asyncio.sleep(0.2)
@ -852,7 +852,7 @@ async def test_a_web_identity_token_botocore_rejects_fails_both_before_sts(
return token if name == reference else default_value
monkeypatch.setattr(base_aws_llm, "get_secret", secret)
monkeypatch.setattr(s3_v3, "get_secret", secret)
monkeypatch.setattr(s3_v2, "get_secret", secret)
identity: Final = {
"s3_aws_access_key_id": None,
"s3_aws_secret_access_key": None,
@ -863,7 +863,7 @@ async def test_a_web_identity_token_botocore_rejects_fails_both_before_sts(
"s3_aws_sts_endpoint": f"http://127.0.0.1:{sts.port}",
}
logger = await _logger(monkeypatch, bucket, identity)
with patch.object(s3_v3._Host, "callback_failed") as rust_failures:
with patch.object(s3_v2._Host, "callback_failed") as rust_failures:
await _log(logger, _payload())
await logger.flush_queue()
assert sts.calls == []
@ -873,7 +873,7 @@ async def test_a_web_identity_token_botocore_rejects_fails_both_before_sts(
assert logger.sink.queued_keys() == golden.expected("queue")
python_error: Final = golden.expected("error")
with pytest.raises(ValueError, match=r"^Parameter validation failed:") as rust_error:
s3_v3._Host().web_identity_token(reference)
s3_v2._Host().web_identity_token(reference)
assert str(rust_error.value) == python_error
@ -918,7 +918,7 @@ async def test_the_native_opt_out_matches_should_run_callback(
"standard_callback_dynamic_params": logging_obj.standard_callback_dynamic_params,
"standard_logging_object": _payload(),
}
assert s3_v3.legacy_should_log(kwargs) is expected
assert s3_v2.legacy_should_log(kwargs) is expected
await _log_native(logger, kwargs)
await logger.flush_queue()
assert len(bucket.puts()) == int(expected)
@ -979,7 +979,7 @@ async def test_a_payload_that_changes_while_it_is_rendered_is_kept_for_retry_by_
metadata: Final = racing["metadata"]
assert isinstance(metadata, dict)
metadata["mutating"] = _Mutating(metadata) # pyright: ignore[reportUnknownArgumentType] # the metadata dict _payload builds
with patch.object(s3_v3._Host, "callback_failed") as rust_failures:
with patch.object(s3_v2._Host, "callback_failed") as rust_failures:
await _log_native(logger, {"standard_logging_object": racing})
await _log(logger, _payload("steady"))
await logger.flush_queue()
@ -1010,7 +1010,7 @@ class _Panic(BaseException):
@pytest.mark.asyncio
async def test_the_periodic_flush_keeps_running_after_a_flush_fails() -> None:
sink: Final = _FlakySink((RuntimeError("bucket gone"), _Panic("a PanicException is a BaseException too")))
task: Final = asyncio.create_task(s3_v3._periodic_flush(sink, 0.01)) # pyright: ignore[reportArgumentType] # the loop only calls flush
task: Final = asyncio.create_task(s3_v2._periodic_flush(sink, 0.01)) # pyright: ignore[reportArgumentType] # the loop only calls flush
for _ in range(100):
if sink.flushed >= 3:
break
@ -1200,7 +1200,7 @@ async def test_both_loggers_widen_the_adaptive_upload_limit_alike(
async def test_audit_events_flush_at_the_same_batch_size(
golden: Golden, bucket: FakeBucket, monkeypatch: pytest.MonkeyPatch, batch_size: int, puts: int
) -> None:
monkeypatch.setattr(s3_v3, "DEFAULT_S3_BATCH_SIZE", batch_size)
monkeypatch.setattr(s3_v2, "DEFAULT_S3_BATCH_SIZE", batch_size)
logger = await _logger(monkeypatch, bucket, {})
for number in (1, 2):
audit = {"id": f"audit-{number}", "action": "created", "updated_at": "2026-11-13"}
@ -1216,13 +1216,13 @@ async def test_audit_events_flush_at_the_same_batch_size(
async def test_both_loggers_keep_uploads_that_fail_in_transport(
golden: Golden, bucket: FakeBucket, monkeypatch: pytest.MonkeyPatch, failure: str
) -> None:
monkeypatch.setattr(s3_v3, "S3_UPLOAD_ATTEMPT_TIMEOUT_SECONDS", 0.3)
monkeypatch.setattr(s3_v2, "S3_UPLOAD_ATTEMPT_TIMEOUT_SECONDS", 0.3)
if failure == "timeout":
bucket.delays.append(("_a\\.json", 1.0))
else:
bucket.failures.append(("_a\\.json", 0, ""))
logger = await _logger(monkeypatch, bucket, {})
with patch.object(s3_v3._Host, "callback_failed") as rust_failures:
with patch.object(s3_v2._Host, "callback_failed") as rust_failures:
for response_id in ("a", "b"):
await _log(logger, _payload(response_id))
await logger.flush_queue()
@ -1262,7 +1262,7 @@ async def test_cold_storage_handles_odd_metadata_like_python(
del payload["metadata"]
else:
payload["metadata"] = metadata
with patch.object(s3_v3._Host, "callback_failed") as rust_failures:
with patch.object(s3_v2._Host, "callback_failed") as rust_failures:
await _log(logger, payload)
await logger.flush_queue()
await asyncio.sleep(0.2)
@ -1345,7 +1345,7 @@ class _AliasLookupFails(dict[str, object]):
async def test_the_attempt_ended_hook_only_enqueues_while_s3_is_slow(
golden: Golden, bucket: FakeBucket, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(s3_v3, "DEFAULT_S3_BATCH_SIZE", 1)
monkeypatch.setattr(s3_v2, "DEFAULT_S3_BATCH_SIZE", 1)
logger = await _logger(monkeypatch, bucket, {})
bucket.delays.append((".", 3.0))
started: Final = time.monotonic()
@ -1369,7 +1369,7 @@ async def test_a_payload_whose_alias_lookup_fails_is_counted_and_released(
golden: Golden, bucket: FakeBucket, monkeypatch: pytest.MonkeyPatch
) -> None:
logger = await _logger(monkeypatch, bucket, {"s3_use_team_prefix": True})
with patch.object(s3_v3._Host, "callback_failed") as rust_failures:
with patch.object(s3_v2._Host, "callback_failed") as rust_failures:
await _log(logger, _payload(metadata=_AliasLookupFails(team="t")))
await logger.flush_queue()
await asyncio.sleep(0.2)

View file

@ -16,7 +16,7 @@ from urllib.parse import quote, unquote, urlsplit, urlunsplit
import pytest
import requests
from tests.unit.integrations.s3_v3_rust_support import (
from tests.unit.integrations.s3_v2_rust_support import (
DROP,
Received,
ScriptedBucket,
@ -27,8 +27,8 @@ from tests.unit.integrations.s3_v3_rust_support import (
)
import litellm
from litellm.integrations import s3_v3
from litellm.integrations.s3_v3 import S3Logger
from litellm.integrations import s3_v2
from litellm.integrations.s3_v2 import S3Logger
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.rust_bridge import _native
from litellm.types.integrations.s3_v2 import S3LoggerSettings
@ -83,7 +83,7 @@ async def _upload(logger: S3Logger, payload: Mapping[str, object], start_time: d
def _failures() -> object:
return patch.object(s3_v3._Host, "callback_failed")
return patch.object(s3_v2._Host, "callback_failed")
def _uploaded(put: Received) -> dict[str, object]:
@ -2117,14 +2117,14 @@ async def test_batch_line_items_are_serialized_and_hashed_off_the_event_loop(
import threading
logger: Final = _s3_logger_answering(monkeypatch, bucket, 200, "cold/")
serialize: Final = s3_v3.batch_line_items_ndjson
serialize: Final = s3_v2.batch_line_items_ndjson
threads: Final[dict[str, threading.Thread]] = {}
def _serialize(payload: object) -> str:
threads["serialize"] = threading.current_thread()
return serialize(payload)
monkeypatch.setattr(s3_v3, "batch_line_items_ndjson", _serialize)
monkeypatch.setattr(s3_v2, "batch_line_items_ndjson", _serialize)
await logger.async_log_batch_line_items(_batch_line_items_payload())

View file

@ -14,13 +14,13 @@ from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.custom_logger_v2.enums import Field, Hook
from litellm.integrations.custom_logger_v2.field_sources import HookArgs
from litellm.integrations.custom_logger_v2.manifest import build_manifest
from litellm.integrations.s3_v3 import STRIP_CALLBACK_NAME, S3Logger, S3StripBase64Files
from litellm.integrations.s3_v2 import STRIP_CALLBACK_NAME, S3Logger, S3StripBase64Files
from litellm.litellm_core_utils import litellm_logging
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.rust_bridge import _native
from litellm.types.integrations.custom_logger_v2 import Outcome
from litellm.types.utils import ModelResponse
from tests.unit.integrations.s3_v3_rust_support import ScriptedBucket, scripted_bucket
from tests.unit.integrations.s3_v2_rust_support import ScriptedBucket, scripted_bucket
pytestmark = pytest.mark.skipif(not hasattr(_native, "NativeS3Log"), reason="the built _native has no NativeS3Log")

View file

@ -22,7 +22,7 @@ from litellm.proxy.management_helpers.audit_logs import (
is_audit_logging_enabled,
)
from litellm.types.utils import StandardAuditLogPayload
from tests.unit.integrations.s3_v3_rust_support import scripted_bucket
from tests.unit.integrations.s3_v2_rust_support import scripted_bucket
@pytest.fixture(autouse=True)

View file

@ -24,7 +24,7 @@ from litellm.responses.litellm_completion_transformation.session_handler import
ResponsesSessionHandler,
)
from litellm.types.utils import StandardLoggingMetadata
from tests.unit.integrations.s3_v3_rust_support import scripted_bucket
from tests.unit.integrations.s3_v2_rust_support import scripted_bucket
class TestColdStorageObjectKeyIntegration: