From 60f4c01b741630efb08b451f8cbc6b625835064a Mon Sep 17 00:00:00 2001 From: ishaan-berri <155045088+ishaan-berri@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:17:22 -0700 Subject: [PATCH 1/4] fix(proxy): list public team model name in /v1/models (#30588) * fix(proxy): optionally surface public team model name in /v1/models Behind general_settings.use_team_public_model_name (default False). When enabled, /v1/models and /models surface the public team_public_model_name for team-scoped (BYOK) models instead of the internal routing key model_name_{team_id}_{uuid} -- consistent with /v1/model/info and OpenAI-compatible. Off by default so the listing's model ids stay backward-compatible for callers that scripted against the internal name; routing by the internal name is unchanged regardless of the flag. Presentation-layer only: access-group, auth, and routing semantics are unchanged; non-team models are pass-through. * fix(proxy): default team model listings to public names * test(proxy): cover team model listing metadata * test(proxy): cover empty team listing deployments * refactor(proxy): simplify team model listing translation * fix(proxy): resolve public team model name on GET /v1/models/{id} The listing endpoints advertise team_public_model_name, but the retrieve endpoint validated and looked up by the raw id, so a public name 404'd. Resolve the public name back to the internal routing key (scoped to the caller's accessible models so colliding names never cross teams), look up by it, and echo the public name back as the response id. * test(proxy): cover public-name resolution on model retrieve * refactor(proxy): extract team model-name translation into TeamModelNameTranslator Move the team-scoped (BYOK) listing/retrieve name translation out of proxy_server.py into a dedicated common_utils module. Static methods with general_settings injected so the logic is unit-testable without globals and proxy_server.py stays thin. * refactor(proxy): use TeamModelNameTranslator in model_list and model_info * test(proxy): target TeamModelNameTranslator for model-name translation * fix(proxy): type create_model_info_response return as dict[str, object] * fix(proxy): keep internal routing key for team model listing metadata lookup Add listing_entries returning (public response id, internal lookup id) so include_metadata=true resolves fallbacks against the routing key the router indexes by, instead of the translated public name (which never matches). * fix(proxy): build /v1/models metadata from internal key, show public id * test(proxy): cover team listing fallback metadata via internal key * fix(proxy): use builtin dict generics in create_model_info_response (UP006) --------- Co-authored-by: Tushar More Co-authored-by: Ishaan Jaffer --- .../proxy/common_utils/model_listing_utils.py | 167 +++++ litellm/proxy/proxy_server.py | 67 +- litellm/proxy/utils.py | 58 +- litellm/types/proxy/model_listing.py | 21 + tests/llm_translation/base_llm_unit_tests.py | 5 +- .../test_team_model_name_translation.py | 662 ++++++++++++++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 18 +- 7 files changed, 946 insertions(+), 52 deletions(-) create mode 100644 litellm/proxy/common_utils/model_listing_utils.py create mode 100644 litellm/types/proxy/model_listing.py diff --git a/litellm/proxy/common_utils/model_listing_utils.py b/litellm/proxy/common_utils/model_listing_utils.py new file mode 100644 index 00000000000..3a70377037d --- /dev/null +++ b/litellm/proxy/common_utils/model_listing_utils.py @@ -0,0 +1,167 @@ +"""Team-scoped (BYOK) model-name translation for the model listing endpoints. + +`/v1/models`, `/models`, and `GET /v1/models/{id}` should surface the public +`team_public_model_name` rather than the internal routing key +`model_name_{team_id}_{uuid}`, consistent with `/v1/model/info`. The internal +key still routes regardless; this is a presentation-layer swap only and does not +touch access-group or auth semantics (see issue #28382). Operators can pin the +legacy internal names with `general_settings.use_team_public_model_name: false`. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from litellm.router import Router + + +class TeamModelNameTranslator: + """Translates internal team routing keys to their public names for the model + listing/retrieve responses. Stateless; the live router and general_settings + are injected per call so the unit tests can drive it without globals. + """ + + @staticmethod + def _internal_public_pair(model: object) -> tuple[str, str] | None: + """`(internal_routing_key, public_name)` for a team-scoped row, else None.""" + if not isinstance(model, dict): + return None + model_dict = cast(dict[str, object], model) # any-ok: checked + model_info_raw: object = model_dict.get("model_info") + if not isinstance(model_info_raw, Mapping): + return None + model_info = cast(Mapping[str, object], model_info_raw) # any-ok: checked + team_id = model_info.get("team_id") + team_public = model_info.get("team_public_model_name") + name = model_dict.get("model_name") + if ( + isinstance(team_id, str) + and isinstance(team_public, str) + and isinstance(name, str) + and team_id + and team_public + and name.startswith(f"model_name_{team_id}_") + ): + return name, team_public + return None + + @staticmethod + def _is_enabled(general_settings: Mapping[str, object]) -> bool: + return general_settings.get("use_team_public_model_name", True) is not False + + @staticmethod + def build_internal_to_public_map( + llm_router: "Router | None", + general_settings: Mapping[str, object], + ) -> dict[str, str]: + """Internal team routing key -> public `team_public_model_name`. + + Empty when disabled via the legacy flag, the router is absent, or the + router model list is malformed. + """ + if llm_router is None or not TeamModelNameTranslator._is_enabled( + general_settings + ): + return {} + router_model_list = llm_router.get_model_list() + if not isinstance(router_model_list, list): + return {} + return dict( + pair + for pair in ( + TeamModelNameTranslator._internal_public_pair(model) + for model in router_model_list + ) + if pair is not None + ) + + @staticmethod + def _response_to_lookup_map( + model_names: list[str], + internal_to_public: dict[str, str], + ) -> dict[str, str]: + """Map each public response id to the first internal lookup id seen in + `model_names`, preserving first-occurrence order. First-wins keeps list + and retrieve in agreement on which accessible deployment a shared public + id resolves to: a global iterated before a colliding team alias stays + the listed entry, and sibling team rows collapse to their first + occurrence. + """ + result: dict[str, str] = {} + for name in model_names: + result.setdefault(internal_to_public.get(name, name), name) + return result + + @staticmethod + def listing_entries( + model_names: list[str], + llm_router: "Router | None", + general_settings: Mapping[str, object], + ) -> list[tuple[str, str]]: + """`(response_id, metadata_lookup_id)` for each listed model, de-duplicated + by response_id while preserving order. + + For team-scoped rows `response_id` is the public name shown to the client, + while `metadata_lookup_id` stays the internal routing key so downstream + metadata/fallback lookups (keyed by the routing name) still resolve. The + lookup id is always one of `model_names` (the caller's accessible set), so + a public name shared across teams never resolves to another team's + internal key. Both ids are identical for unmapped names (globals, + access-group keys). + """ + internal_to_public = TeamModelNameTranslator.build_internal_to_public_map( + llm_router, general_settings + ) + if not internal_to_public: + return [(name, name) for name in model_names] + return list( + TeamModelNameTranslator._response_to_lookup_map( + model_names, internal_to_public + ).items() + ) + + @staticmethod + def translate_listing( + model_names: list[str], + llm_router: "Router | None", + general_settings: Mapping[str, object], + ) -> list[str]: + """Public-name view of `model_names` (the `response_id` of each listing + entry). Sibling deployments sharing a public name collapse to one entry + while preserving order; unmapped names pass through. + """ + return [ + entry[0] + for entry in TeamModelNameTranslator.listing_entries( + model_names, llm_router, general_settings + ) + ] + + @staticmethod + def resolve_public_name( + model_id: str, + available_models: list[str], + llm_router: "Router | None", + general_settings: Mapping[str, object], + ) -> str: + """Resolve a public team name back to the internal routing key the router + indexes by, so `GET /v1/models/{id}` accepts the name the listing returns. + + Resolution is restricted to `available_models` (the caller's accessible + set) so colliding public names across teams never resolve across an access + boundary. Uses the same first-occurrence dedup as `listing_entries` so a + public id advertised by `/v1/models` resolves to the same internal + deployment that the listing's metadata was built from. Returns `model_id` + unchanged when it is not an accessible public team name (already-internal + names and globals pass through). + """ + internal_to_public = TeamModelNameTranslator.build_internal_to_public_map( + llm_router, general_settings + ) + if not internal_to_public: + return model_id + return TeamModelNameTranslator._response_to_lookup_map( + available_models, internal_to_public + ).get(model_id, model_id) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 873831af833..8de369efbde 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -15,6 +15,7 @@ import threading import time import traceback import warnings +from collections.abc import Mapping from datetime import datetime, timedelta, timezone from typing import ( TYPE_CHECKING, @@ -301,6 +302,7 @@ from litellm.proxy.common_utils.load_config_utils import ( get_config_file_contents_from_gcs, get_file_contents_from_s3, ) +from litellm.proxy.common_utils.model_listing_utils import TeamModelNameTranslator from litellm.proxy.common_utils.openai_endpoint_utils import ( remove_sensitive_info_from_deployment, ) @@ -8376,6 +8378,8 @@ async def model_list( """ global llm_model_list, general_settings, llm_router, prisma_client, user_api_key_cache, proxy_logging_obj + settings = cast(dict[str, object], general_settings) # any-ok: legacy settings + from litellm.proxy.management_endpoints.common_utils import ( _user_has_admin_privileges, ) @@ -8455,16 +8459,21 @@ async def model_list( if hidden_names: all_models = [m for m in all_models if m not in hidden_names] - # Build response data with all proxy models + # Surface the public team name by default; legacy internal keys via flag. + # The internal routing key drives the metadata/fallback lookup, while the + # public name is what the client sees as the model id. model_data = [] - for model in all_models: + for response_id, lookup_id in TeamModelNameTranslator.listing_entries( + all_models, llm_router, settings + ): model_info = create_model_info_response( - model_id=model, + model_id=lookup_id, provider="openai", include_metadata=include_metadata or False, fallback_type=fallback_type, llm_router=llm_router, ) + model_info["id"] = response_id model_data.append(model_info) return dict( @@ -8492,16 +8501,21 @@ async def model_list( if hidden_names: all_models = [m for m in all_models if m not in hidden_names] - # Build response data + # Surface the public team name by default; legacy internal keys via flag. + # The internal routing key drives the metadata/fallback lookup, while the + # public name is what the client sees as the model id. model_data = [] - for model in all_models: + for response_id, lookup_id in TeamModelNameTranslator.listing_entries( + all_models, llm_router, settings + ): model_info = create_model_info_response( - model_id=model, + model_id=lookup_id, provider="openai", include_metadata=include_metadata or False, fallback_type=fallback_type, llm_router=llm_router, ) + model_info["id"] = response_id model_data.append(model_info) return dict( @@ -8523,6 +8537,8 @@ async def model_list( async def model_info( model_id: str, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + team_id: Optional[str] = None, + healthy_only: Optional[bool] = False, ): """ Retrieve information about a specific model accessible to your API key. @@ -8532,16 +8548,21 @@ async def model_info( Follows OpenAI API specification for individual model retrieval. https://platform.openai.com/docs/api-reference/models/retrieve + + Query parameters mirror `/v1/models` so the same caller context (team + scoping, health filtering, paused deployments) drives both endpoints; the + listing's public id must resolve to the same internal deployment here. """ global llm_model_list, general_settings, llm_router, prisma_client, user_api_key_cache, proxy_logging_obj + settings = cast(dict[str, object], general_settings) # any-ok: legacy settings + from litellm.proxy.utils import ( create_model_info_response, get_available_models_for_user, validate_model_access, ) - # Get available models for the user all_models = await get_available_models_for_user( user_api_key_dict=user_api_key_dict, llm_router=llm_router, @@ -8549,21 +8570,43 @@ async def model_info( user_model=user_model, prisma_client=prisma_client, proxy_logging_obj=proxy_logging_obj, - team_id=None, + team_id=team_id, include_model_access_groups=False, only_model_access_groups=False, return_wildcard_routes=False, user_api_key_cache=user_api_key_cache, ) + # Mirror /v1/models' visibility filter so first-occurrence resolution + # cannot land on a deployment the listing had hidden. + blocked_names = ( + llm_router.get_fully_blocked_model_names() if llm_router is not None else set() + ) + unhealthy_names: set[str] = set() + if healthy_only and llm_router is not None: + unhealthy_names = await llm_router.async_get_fully_unhealthy_model_names() + hidden_names = blocked_names | unhealthy_names + if hidden_names: + all_models = [m for m in all_models if m not in hidden_names] + + internal_to_public = TeamModelNameTranslator.build_internal_to_public_map( + llm_router, settings + ) + resolved_model_id = TeamModelNameTranslator.resolve_public_name( + model_id=model_id, + available_models=all_models, + llm_router=llm_router, + general_settings=settings, + ) + # Validate that the requested model is accessible - validate_model_access(model_id=model_id, available_models=all_models) + validate_model_access(model_id=resolved_model_id, available_models=all_models) # Get provider information from the router deployment if llm_router is None: raise HTTPException(status_code=500, detail="Router not initialized") - deployment = llm_router.get_deployment_by_model_group_name(model_id) + deployment = llm_router.get_deployment_by_model_group_name(resolved_model_id) if deployment is None: raise HTTPException( status_code=404, @@ -8573,9 +8616,9 @@ async def model_info( # Use the actual litellm model from the deployment to get provider info _, provider, _, _ = litellm.get_llm_provider(model=deployment.litellm_params.model) - # Return the model information in the same format as the list endpoint + response_id = internal_to_public.get(resolved_model_id, model_id) return create_model_info_response( - model_id=model_id, + model_id=response_id, provider=provider, include_metadata=False, fallback_type=None, diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 7e225c6cd1c..451c32b334d 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -46,6 +46,7 @@ from litellm.proxy._types import ( ) from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error from litellm.types.guardrails import GuardrailEventHooks +from litellm.types.proxy.model_listing import ModelInfoResponse from litellm.types.utils import CallTypes, CallTypesLiteral try: @@ -6311,56 +6312,39 @@ def create_model_info_response( include_metadata: bool = False, fallback_type: Optional[str] = None, llm_router: Optional["Router"] = None, -) -> dict: +) -> ModelInfoResponse: """ - Create a standardized model info response. + Create a standardized OpenAI-compatible model object. - Args: - model_id: The model ID - provider: The model provider - include_metadata: Whether to include metadata - fallback_type: Type of fallbacks to include - llm_router: LiteLLM router instance - - Returns: - Dictionary containing model information + When include_metadata is true, attaches the model's configured fallbacks + (resolved via the router under fallback_type, defaulting to "general"). + Raises HTTPException(400) for an unknown fallback_type. """ from litellm.proxy.auth.model_checks import get_all_fallbacks - model_info = { + base: ModelInfoResponse = { "id": model_id, "object": "model", "created": DEFAULT_MODEL_CREATED_AT_TIME, "owned_by": provider, } + if not include_metadata: + return base - # Add metadata if requested - if include_metadata: - metadata = {} - - # Default fallback_type to "general" if include_metadata is true - effective_fallback_type = ( - fallback_type if fallback_type is not None else "general" + effective_fallback_type = fallback_type if fallback_type is not None else "general" + valid_fallback_types = ("general", "context_window", "content_policy") + if effective_fallback_type not in valid_fallback_types: + raise HTTPException( + status_code=400, + detail=f"Invalid fallback_type. Must be one of: {list(valid_fallback_types)}", ) - # Validate fallback_type - valid_fallback_types = ["general", "context_window", "content_policy"] - if effective_fallback_type not in valid_fallback_types: - raise HTTPException( - status_code=400, - detail=f"Invalid fallback_type. Must be one of: {valid_fallback_types}", - ) - - fallbacks = get_all_fallbacks( - model=model_id, - llm_router=llm_router, - fallback_type=effective_fallback_type, - ) - metadata["fallbacks"] = fallbacks - - model_info["metadata"] = metadata - - return model_info + fallbacks = get_all_fallbacks( + model=model_id, + llm_router=llm_router, + fallback_type=effective_fallback_type, + ) + return {**base, "metadata": {"fallbacks": fallbacks}} def validate_model_access( diff --git a/litellm/types/proxy/model_listing.py b/litellm/types/proxy/model_listing.py new file mode 100644 index 00000000000..c3330da0d66 --- /dev/null +++ b/litellm/types/proxy/model_listing.py @@ -0,0 +1,21 @@ +"""Response types for the model listing/retrieve endpoints (/v1/models, /models).""" + +from typing import Literal + +from typing_extensions import NotRequired, TypedDict + + +class ModelInfoMetadata(TypedDict): + fallbacks: list[str] + + +class ModelInfoResponse(TypedDict): + """OpenAI-compatible model object. `metadata` is present only when the + endpoint is called with include_metadata=true. + """ + + id: str + object: Literal["model"] + created: int + owned_by: str + metadata: NotRequired[ModelInfoMetadata] diff --git a/tests/llm_translation/base_llm_unit_tests.py b/tests/llm_translation/base_llm_unit_tests.py index fef1d23d867..a184798b503 100644 --- a/tests/llm_translation/base_llm_unit_tests.py +++ b/tests/llm_translation/base_llm_unit_tests.py @@ -906,7 +906,10 @@ class BaseLLMChatTest(ABC): { "type": "image_url", "image_url": { - "url": "https://www.gstatic.com/webp/gallery/1.webp", + # sha-pinned in-repo logo via jsdelivr; gstatic's + # robots.txt blocks server-side fetchers (e.g. + # Anthropic), which 400s the request. + "url": "https://cdn.jsdelivr.net/gh/BerriAI/litellm@d769e81c90d453240c61fc572cdb27fae06a89d0/ui/litellm-dashboard/public/assets/logos/litellm_logo.jpg", "detail": detail, }, }, diff --git a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py index 6a8e0d15d8b..0f87fcda588 100644 --- a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py +++ b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py @@ -15,6 +15,7 @@ import pytest import litellm.proxy.proxy_server as ps from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth +from litellm.proxy.common_utils.model_listing_utils import TeamModelNameTranslator from litellm.proxy.proxy_server import ( _get_proxy_model_info, _translate_model_name_for_response, @@ -593,3 +594,664 @@ async def test_model_info_v1_litellm_model_id_team_id_applies_team_filter(monkey team_filter.assert_awaited_once() assert team_filter.await_args.kwargs["team_id"] == "other-team" assert team_filter.await_args.kwargs["all_models"] == [team_row] + + +@pytest.mark.asyncio +async def test_v1_models_translates_team_model_for_access_group_key(monkeypatch): + """Regression (#28382 sibling leak): a virtual key whose model access group + resolves to a team BYOK deployment must list the PUBLIC name in /v1/models, + not the internal routing key model_name_{team_id}_{uuid}. + + The /model/info read-path fix did not cover /v1/models, which builds from + bare model-name strings via access-group expansion. + """ + team_dep = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "id1", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + # Default behavior: listing surfaces public names. + monkeypatch.setattr(ps, "general_settings", {}) + + # virtual key granted access via the access group (no team membership) + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key) + + ids = [d["id"] for d in resp["data"]] + assert "tushar-gpt-4.1" in ids + assert "model_name_teamX_uuid9" not in ids + + +@pytest.mark.asyncio +async def test_v1_models_keeps_internal_names_when_public_name_flag_disabled( + monkeypatch, +): + """Compatibility override: /v1/models can still list the internal routing + name for consumers that scripted against those ids. Translation is enabled + by default and disabled via general_settings['use_team_public_model_name']. + """ + team_dep = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "id1", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": False}) + + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key) + + ids = [d["id"] for d in resp["data"]] + assert "model_name_teamX_uuid9" in ids # internal id preserved (backward-compat) + assert "tushar-gpt-4.1" not in ids + + +@pytest.mark.asyncio +async def test_v1_models_translates_team_model_with_metadata(monkeypatch): + """include_metadata=true must build metadata for the public model id.""" + team_dep = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "id1", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {}) + + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key, include_metadata=True) + + assert resp["data"] == [ + { + "id": "tushar-gpt-4.1", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "metadata": {"fallbacks": []}, + } + ] + + +@pytest.mark.asyncio +async def test_v1_models_metadata_fallbacks_use_internal_routing_key(monkeypatch): + """Regression: with include_metadata=true, fallbacks configured for a team + model under its internal routing key must still surface. The metadata lookup + has to run against the internal name, not the translated public name (which + the router's fallback config never keys on) -- otherwise fallbacks silently + drop to [].""" + team_dep = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "id1", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + # Fallbacks are keyed on the internal routing name, as the router stores them. + router.fallbacks = [{"model_name_teamX_uuid9": ["gpt-4o-backup"]}] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {}) + + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key, include_metadata=True) + + assert resp["data"] == [ + { + "id": "tushar-gpt-4.1", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "metadata": {"fallbacks": ["gpt-4o-backup"]}, + } + ] + + +@pytest.mark.asyncio +async def test_v1_models_metadata_does_not_leak_other_team_fallbacks(monkeypatch): + """Regression: two teams can publish the same team_public_model_name. With + include_metadata=true a caller scoped to teamX must see teamX's fallbacks for + the shared public name, never teamY's. The metadata lookup has to stay within + the caller's accessible models; resolving the public name through a router-wide + reverse map could point it at another team's internal routing key.""" + team_x = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "idX", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + team_y = { + "model_name": "model_name_teamY_uuidZ", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "idY", + "team_id": "teamY", + "team_public_model_name": "tushar-gpt-4.1", # same public name, other team + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_x, team_y] + router.get_model_list.return_value = [team_x, team_y] + router.fallbacks = [ + {"model_name_teamX_uuid9": ["teamX-backup"]}, + {"model_name_teamY_uuidZ": ["teamY-backup"]}, + ] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {}) + + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key, include_metadata=True) + + assert resp["data"] == [ + { + "id": "tushar-gpt-4.1", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "metadata": {"fallbacks": ["teamX-backup"]}, + } + ] + + +def test_translate_team_model_names_for_listing_swaps_and_dedupes(): + """Internal team routing keys -> public name; sibling deployments sharing a + public name collapse to one entry (order preserved); globals untouched.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + { + "model_name": "model_name_teamX_uuidB", # sibling: same public name + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + {"model_name": "gpt-4o", "model_info": {"db_model": False}}, + ] + + out = TeamModelNameTranslator.translate_listing( + ["model_name_teamX_uuidA", "model_name_teamX_uuidB", "gpt-4o"], + router, + {}, + ) + assert out == ["tushar-gpt-4.1", "gpt-4o"] + + +def test_listing_entries_keep_internal_lookup_id_for_team_rows(): + """`listing_entries` returns (public response id, internal lookup id) so the + response shows the public name while metadata lookups keep the routing key. + Sibling deployments collapse to one entry; globals map to themselves.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + { + "model_name": "model_name_teamX_uuidB", # sibling: same public name + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + {"model_name": "gpt-4o", "model_info": {"db_model": False}}, + ] + + entries = TeamModelNameTranslator.listing_entries( + ["model_name_teamX_uuidA", "model_name_teamX_uuidB", "gpt-4o"], + router, + {}, + ) + # public id for the client; an internal routing key for the metadata lookup + assert entries[0][0] == "tushar-gpt-4.1" + assert entries[0][1].startswith("model_name_teamX_uuid") + assert entries[1] == ("gpt-4o", "gpt-4o") + assert len(entries) == 2 + + +def test_listing_entries_lookup_id_never_crosses_team_boundary(): + """Regression: when two teams share a team_public_model_name, the lookup id for + the shared public name must stay within the caller's accessible model_names and + never resolve to the other team's internal routing key (which would leak that + team's fallback metadata under include_metadata=true).""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "shared-name", + }, + }, + { + "model_name": "model_name_teamY_uuidB", # different team, same public name + "model_info": { + "team_id": "teamY", + "team_public_model_name": "shared-name", + }, + }, + ] + + # caller can only access teamX's internal key + entries = TeamModelNameTranslator.listing_entries( + ["model_name_teamX_uuidA"], router, {} + ) + + assert entries == [("shared-name", "model_name_teamX_uuidA")] + + +def test_listing_entries_global_wins_when_team_alias_collides_with_global(): + """Regression: when an accessible global model shares its name with a team + deployment's `team_public_model_name`, the listing must keep the global + entry rather than overwriting its lookup id with the colliding team's + internal routing key (which would surface the team's metadata under the + global id).""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "gpt-4o", + }, + }, + {"model_name": "gpt-4o", "model_info": {"db_model": False}}, + ] + + entries = TeamModelNameTranslator.listing_entries( + ["gpt-4o", "model_name_teamX_uuidA"], router, {} + ) + + assert entries == [("gpt-4o", "gpt-4o")] + + +def test_listing_and_resolve_agree_on_sibling_internal_key(): + """Regression: when two team deployments share a public name, listing and + retrieve must pick the same internal routing key, otherwise `/v1/models/{id}` + describes a different deployment than what the listing's metadata was built + from.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + { + "model_name": "model_name_teamX_uuidB", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + ] + available = ["model_name_teamX_uuidA", "model_name_teamX_uuidB"] + + [(_, listing_lookup)] = TeamModelNameTranslator.listing_entries( + available, router, {} + ) + resolve_lookup = TeamModelNameTranslator.resolve_public_name( + model_id="tushar-gpt-4.1", + available_models=available, + llm_router=router, + general_settings={}, + ) + + assert listing_lookup == resolve_lookup + + +def test_listing_entries_skips_empty_team_public_model_name(): + """Regression: a misconfigured row with `team_public_model_name: ""` must not + produce a listing entry with an empty `id`; the internal routing key should + pass through unchanged, matching `/v1/model/info`'s falsy-check behavior.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "", + }, + }, + ] + + entries = TeamModelNameTranslator.listing_entries( + ["model_name_teamX_uuidA"], router, {} + ) + + assert entries == [("model_name_teamX_uuidA", "model_name_teamX_uuidA")] + + +def test_listing_entries_passthrough_when_disabled(): + """Legacy flag / no router -> response id equals lookup id (no translation).""" + assert TeamModelNameTranslator.listing_entries(["a", "b"], None, {}) == [ + ("a", "a"), + ("b", "b"), + ] + + +def test_translate_team_model_names_for_listing_leaves_unmapped_names(): + """Names with no team mapping (globals, access-group keys) pass through.""" + router = MagicMock() + router.get_model_list.return_value = [ + {"model_name": "gpt-4o", "model_info": {"db_model": False}} + ] + + assert TeamModelNameTranslator.translate_listing( + ["gpt-4o", "beta-group"], router, {} + ) == ["gpt-4o", "beta-group"] + + +def test_translate_team_model_names_for_listing_none_router(): + """No router -> return the input list unchanged.""" + assert TeamModelNameTranslator.translate_listing(["a", "b"], None, {}) == ["a", "b"] + + +def test_translate_team_model_names_for_listing_respects_legacy_flag(): + """Operators can keep returning the legacy internal routing key.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + } + ] + + assert TeamModelNameTranslator.translate_listing( + ["model_name_teamX_uuidA"], router, {"use_team_public_model_name": False} + ) == ["model_name_teamX_uuidA"] + + +def _public_named_router(*team_rows: dict) -> MagicMock: + router = MagicMock() + router.get_model_list.return_value = list(team_rows) + return router + + +def test_resolve_public_name_to_internal_routing_key(): + """A public team name resolves back to the internal routing key the router + indexes by, so `GET /v1/models/{public_name}` can find the deployment.""" + router = _public_named_router(_team_row()) + + assert ( + TeamModelNameTranslator.resolve_public_name( + model_id="team-claude-sonnet", + available_models=["model_name_team-abc-123_4a6b8"], + llm_router=router, + general_settings={}, + ) + == "model_name_team-abc-123_4a6b8" + ) + + +def test_resolve_public_name_is_access_scoped_across_teams(): + """Two teams can publish the SAME public name. A caller's query must resolve + to the internal key they can actually access, never another team's.""" + # both rows share public name "team-claude-sonnet" + router = _public_named_router(_team_row(), _other_team_row()) + + # caller only has access to their own team's internal key + resolved = TeamModelNameTranslator.resolve_public_name( + model_id="team-claude-sonnet", + available_models=["model_name_team-abc-123_4a6b8"], + llm_router=router, + general_settings={}, + ) + assert resolved == "model_name_team-abc-123_4a6b8" + assert resolved != "model_name_team-other_9f2c1" + + +def test_resolve_public_name_unmapped_passes_through(): + """A public name with no accessible internal mapping is returned unchanged so + the caller hits the normal 404/access path; internal names pass through too.""" + router = _public_named_router(_team_row()) + + # not accessible -> unchanged (downstream validate_model_access will 404) + assert ( + TeamModelNameTranslator.resolve_public_name( + model_id="team-claude-sonnet", + available_models=[], + llm_router=router, + general_settings={}, + ) + == "team-claude-sonnet" + ) + # already an internal routing key -> unchanged + assert ( + TeamModelNameTranslator.resolve_public_name( + model_id="model_name_team-abc-123_4a6b8", + available_models=["model_name_team-abc-123_4a6b8"], + llm_router=router, + general_settings={}, + ) + == "model_name_team-abc-123_4a6b8" + ) + + +def test_resolve_public_name_respects_legacy_flag(): + """With the legacy flag set, no public-name resolution happens.""" + router = _public_named_router(_team_row()) + + assert ( + TeamModelNameTranslator.resolve_public_name( + model_id="team-claude-sonnet", + available_models=["model_name_team-abc-123_4a6b8"], + llm_router=router, + general_settings={"use_team_public_model_name": False}, + ) + == "team-claude-sonnet" + ) + + +@pytest.mark.asyncio +async def test_retrieve_model_by_public_name_returns_200(monkeypatch): + """Regression: `GET /v1/models/{public_name}` must NOT 404. The listing + advertises the public team name, so retrieve must accept the same name, + resolve it to the internal routing key for lookup, and echo the public name + back as the model id.""" + import litellm + import litellm.proxy.utils as proxy_utils + + team_row = _team_row() + router = _public_named_router(team_row) + deployment = MagicMock() + deployment.litellm_params.model = "azure/gpt-5.2-low-rpm-testing" + router.get_deployment_by_model_group_name.return_value = deployment + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "general_settings", {}) + monkeypatch.setattr( + proxy_utils, + "get_available_models_for_user", + AsyncMock(return_value=["model_name_team-abc-123_4a6b8"]), + ) + monkeypatch.setattr( + litellm, "get_llm_provider", lambda model: (model, "openai", None, None) + ) + + key = UserAPIKeyAuth(user_id="u", api_key="sk-test", team_models=[]) + resp = await ps.model_info(model_id="team-claude-sonnet", user_api_key_dict=key) + + assert resp["id"] == "team-claude-sonnet" + # lookup happened by the internal routing key, not the public name + router.get_deployment_by_model_group_name.assert_called_once_with( + "model_name_team-abc-123_4a6b8" + ) + + +@pytest.mark.asyncio +async def test_retrieve_model_by_internal_name_returns_public_id(monkeypatch): + """Regression: retrieving by the internal routing key must echo the SAME + public id `/v1/models` advertises for that deployment, not the path. Otherwise + a client iterating the listing's id and then retrieving each one would observe + a different id depending on which alias they queried by.""" + import litellm + import litellm.proxy.utils as proxy_utils + + router = _public_named_router(_team_row()) + deployment = MagicMock() + deployment.litellm_params.model = "azure/gpt-5.2-low-rpm-testing" + router.get_deployment_by_model_group_name.return_value = deployment + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "general_settings", {}) + monkeypatch.setattr( + proxy_utils, + "get_available_models_for_user", + AsyncMock(return_value=["model_name_team-abc-123_4a6b8"]), + ) + monkeypatch.setattr( + litellm, "get_llm_provider", lambda model: (model, "openai", None, None) + ) + + key = UserAPIKeyAuth(user_id="u", api_key="sk-test", team_models=[]) + resp = await ps.model_info( + model_id="model_name_team-abc-123_4a6b8", user_api_key_dict=key + ) + + assert resp["id"] == "team-claude-sonnet" + + +@pytest.mark.asyncio +async def test_retrieve_model_by_internal_name_keeps_internal_id_when_flag_disabled( + monkeypatch, +): + """With `use_team_public_model_name=false`, retrieve must keep the internal + routing key as the response id, mirroring `/v1/models`' legacy output.""" + import litellm + import litellm.proxy.utils as proxy_utils + + router = _public_named_router(_team_row()) + deployment = MagicMock() + deployment.litellm_params.model = "azure/gpt-5.2-low-rpm-testing" + router.get_deployment_by_model_group_name.return_value = deployment + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": False}) + monkeypatch.setattr( + proxy_utils, + "get_available_models_for_user", + AsyncMock(return_value=["model_name_team-abc-123_4a6b8"]), + ) + monkeypatch.setattr( + litellm, "get_llm_provider", lambda model: (model, "openai", None, None) + ) + + key = UserAPIKeyAuth(user_id="u", api_key="sk-test", team_models=[]) + resp = await ps.model_info( + model_id="model_name_team-abc-123_4a6b8", user_api_key_dict=key + ) + + assert resp["id"] == "model_name_team-abc-123_4a6b8" + + +@pytest.mark.asyncio +async def test_retrieve_model_by_inaccessible_public_name_404s(monkeypatch): + """A caller without access to a team model still gets 404 when retrieving by + its public name; resolution never crosses the access boundary.""" + import litellm + import litellm.proxy.utils as proxy_utils + + router = _public_named_router(_team_row()) + deployment = MagicMock() + deployment.litellm_params.model = "azure/gpt-5.2-low-rpm-testing" + router.get_deployment_by_model_group_name.return_value = deployment + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "general_settings", {}) + monkeypatch.setattr( + proxy_utils, + "get_available_models_for_user", + AsyncMock(return_value=[]), # caller has no access + ) + monkeypatch.setattr( + litellm, "get_llm_provider", lambda model: (model, "openai", None, None) + ) + + key = UserAPIKeyAuth(user_id="u", api_key="sk-test", team_models=[]) + with pytest.raises(ps.HTTPException) as exc_info: + await ps.model_info(model_id="team-claude-sonnet", user_api_key_dict=key) + + assert exc_info.value.status_code == 404 + router.get_deployment_by_model_group_name.assert_not_called() diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 100b7523830..13b735ddf7c 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -7827,6 +7827,10 @@ export interface paths { * * Follows OpenAI API specification for individual model retrieval. * https://platform.openai.com/docs/api-reference/models/retrieve + * + * Query parameters mirror `/v1/models` so the same caller context (team + * scoping, health filtering, paused deployments) drives both endpoints; the + * listing's public id must resolve to the same internal deployment here. */ get: operations["model_info_models__model_id__get"]; put?: never; @@ -16663,6 +16667,10 @@ export interface paths { * * Follows OpenAI API specification for individual model retrieval. * https://platform.openai.com/docs/api-reference/models/retrieve + * + * Query parameters mirror `/v1/models` so the same caller context (team + * scoping, health filtering, paused deployments) drives both endpoints; the + * listing's public id must resolve to the same internal deployment here. */ get: operations["model_info_v1_models__model_id__get"]; put?: never; @@ -42956,7 +42964,10 @@ export interface operations { }; model_info_models__model_id__get: { parameters: { - query?: never; + query?: { + team_id?: string | null; + healthy_only?: boolean | null; + }; header?: never; path: { model_id: string; @@ -53834,7 +53845,10 @@ export interface operations { }; model_info_v1_models__model_id__get: { parameters: { - query?: never; + query?: { + team_id?: string | null; + healthy_only?: boolean | null; + }; header?: never; path: { model_id: string; From b8d79d1e0c81ed2364950467aa9524ce18f9ac2f Mon Sep 17 00:00:00 2001 From: Mateo Wang <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:42:00 -0700 Subject: [PATCH 2/4] ci: drop mypy entirely, standardize type checking on basedpyright (#30648) * ci: drop redundant mypy type-check gate, standardize on basedpyright Type checking ran both mypy (via the pydantic.mypy plugin) and basedpyright. pydantic v2 emits dataclass_transform, so basedpyright understands models natively with no plugin, and its gated rules already cover what the mypy pass caught (no-untyped-def, no-any-return, valid-type, import-not-found all map to basedpyright equivalents). Running both meant two checkers, two budgets, and a plugin only mypy could load. This removes the mypy type-check gate: the lint-mypy/lint-mypy-budget-update Makefile targets, the CI MyPy step, mypy-code-budget.json, the budget-ratchet entry, and the vestigial [tool.mypy] pydantic plugin block (the gating pass used litellm/mypy.ini, which never loaded the plugin). type_check_gate.py is specialized to basedpyright since the mypy parsing path is now unused. mypy stays a dev dependency because the Any-discipline gate (scripts/check_any_discipline.py) imports it as a library to detect Any-typed values; it is no longer run as a type checker. * ci: remove the Any-discipline gate, rely on basedpyright's reportAny The Any-discipline gate (scripts/check_any_discipline.py) was the last consumer of mypy: it imported mypy as a library to detect values whose inferred type contains Any, gated per-file against any-discipline-budget.json. basedpyright already reports the same class of finding through reportAny/reportExplicitAny, which are gated tree-wide in basedpyright-code-budget.json, so the separate gate (and the mypy dependency behind it) is redundant. Removes the gate end to end: check_any_discipline.py and its test, the any-discipline CI job, the lint-any/lint-any-budget-update Makefile targets, any-discipline-budget.json, litellm/mypy.ini, the .mypy_cache_any references, and mypy from the dev dependencies. budget_ratchet_check.py drops the any-discipline entry and the now-unused zero-floor mechanism (rewritten as a comprehension). check_type_discipline.py drops the any-ok suppression token, since # any-ok suppressed only the deleted gate; the 134 now-orphaned # any-ok comments across 14 files are stripped (they never affected basedpyright, which uses # pyright: ignore). uv.lock is intentionally left untouched: uv still considers it consistent with the mypy-removed pyproject (uv lock --check and uv sync --frozen both pass), and a relock bumps 30+ unrelated packages because of the moving exclude-newer window. A future intentional relock will prune the now-unreferenced mypy entry. * build: relock to drop mypy from uv.lock CI's uv 0.10.9 honors the repo's exclude-newer window and correctly flags the lockfile as out of sync once mypy leaves pyproject; my earlier local uv 0.8.17 could not parse exclude-newer and silently passed --check. Relocking with the pinned CI version removes only mypy and its transitive librt, with no other version changes. --- .github/workflows/test-linting.yml | 57 +- .gitignore | 2 - CLAUDE.md | 6 +- CONTRIBUTING.md | 9 +- Makefile | 36 +- any-discipline-budget.json | 5974 ----------------- litellm/litellm_core_utils/litellm_logging.py | 14 +- litellm/llms/anthropic/chat/transformation.py | 2 +- .../llms/hosted_vllm/chat/transformation.py | 45 +- .../vertex_and_google_ai_studio_gemini.py | 24 +- litellm/mypy.ini | 22 - litellm/proxy/common_request_processing.py | 74 +- .../ui_discovery_endpoints.py | 14 +- litellm/proxy/google_endpoints/endpoints.py | 2 +- .../guardrails/guardrail_hooks/presidio.py | 6 +- .../key_management_endpoints.py | 74 +- litellm/proxy/management_endpoints/ui_sso.py | 6 +- litellm/proxy/proxy_server.py | 35 +- .../router_utils/fallback_event_handlers.py | 12 +- .../secret_managers/aws_secret_manager_v2.py | 18 +- litellm/utils.py | 12 +- mypy-code-budget.json | 18 - pyproject.toml | 6 - scripts/budget_ratchet_check.py | 38 +- scripts/check_any_discipline.py | 778 --- scripts/check_type_discipline.py | 17 +- scripts/type_check_gate.py | 90 +- .../test_litellm/test_budget_ratchet_check.py | 19 +- .../test_litellm/test_check_any_discipline.py | 90 - tests/test_litellm/test_type_check_gate.py | 33 +- uv.lock | 103 +- 31 files changed, 198 insertions(+), 7438 deletions(-) delete mode 100644 any-discipline-budget.json delete mode 100644 litellm/mypy.ini delete mode 100644 mypy-code-budget.json delete mode 100644 scripts/check_any_discipline.py delete mode 100644 tests/test_litellm/test_check_any_discipline.py diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index 0a80a65cbe6..de7e1b68346 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -87,14 +87,9 @@ jobs: run: | uv run --no-sync python -c "import openai; print(f'OpenAI version: {openai.__version__}')" - - name: Run MyPy type checking - run: | - cd litellm - (uv run --no-sync mypy . || true) | uv run --no-sync python ../scripts/type_check_gate.py --tool mypy - - name: Run basedpyright type checking run: | - (uv run --no-sync basedpyright --outputjson || true) | uv run --no-sync python scripts/type_check_gate.py --tool basedpyright + (uv run --no-sync basedpyright --outputjson || true) | uv run --no-sync python scripts/type_check_gate.py - name: Check for circular imports run: | @@ -133,56 +128,6 @@ jobs: run: | python scripts/budget_ratchet_check.py --base "$BASE_SHA" - any-discipline: - # Separate job: the first run cold-builds litellm's type cache (~2 min, ~3 GB), - # so keep it off the main lint job's time budget. Subsequent runs reuse the - # cached .mypy_cache_any and only re-type-check the changed files. - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - # Check out the PR head, not the default refs/pull/N/merge: the merge ref - # folds in newer base commits, which the diff-based gates (ruff delta, - # Any-discipline) would otherwise blame on this branch. - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - clean: true - persist-credentials: false - - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: "3.12" - - - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - with: - version: "0.10.9" - - - name: Install dependencies - run: | - uv sync --frozen - - # Keyed on deps + mypy config (which fix the type cache's validity), not on - # source content, so changed files always differ from the restored cache. - # The gate also defensively invalidates each target's cache entry, so - # correctness never depends on cache freshness -- this is purely for speed. - - name: Restore Any-gate type cache - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: .mypy_cache_any - key: any-mypy-cache-${{ runner.os }}-py3.12-${{ hashFiles('uv.lock', 'litellm/mypy.ini') }} - restore-keys: | - any-mypy-cache-${{ runner.os }}-py3.12- - - - name: Check Any discipline (per-file budget on changed files) - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - run: | - uv run --no-sync python scripts/check_any_discipline.py --changed --base "$BASE_SHA" - secret-scan: runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.gitignore b/.gitignore index 54ae53bb2c9..fda3311fe02 100644 --- a/.gitignore +++ b/.gitignore @@ -74,8 +74,6 @@ tests/local_testing/log.txt .codegpt litellm/proxy/_new_new_secret_config.yaml litellm/proxy/custom_guardrail.py -**/.mypy_cache/ -**/.mypy_cache_any/ litellm/proxy/application.log tests/llm_translation/vertex_test_account.json tests/llm_translation/test_vertex_key.json diff --git a/CLAUDE.md b/CLAUDE.md index 95904ef8abd..2070b6fcdd6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,11 +36,9 @@ Don't hesitate to use values in .env to get needed API keys and other secrets, a Run tests, format your code, and lint your code before each commit -When you fix violations gated by `ruff-strict-budget.json`, `mypy-code-budget.json`, `basedpyright-code-budget.json`, or `any-discipline-budget.json`, run `make lint-budget-update` and commit the lowered baselines so the ceilings ratchet down instead of leaving stale headroom +When you fix violations gated by `ruff-strict-budget.json` or `basedpyright-code-budget.json`, run `make lint-budget-update` and commit the lowered baselines so the ceilings ratchet down instead of leaving stale headroom -If you're trying to create a new function that relies on untyped stuff, instead of adding more Any's and bringing it closer to the max, just validate it in the caller with Pydantic (a model or `TypeAdapter` that returns the typed thing or raises will do) and then pass the now typed variable in - -The Any-discipline gate (`make lint-any`, also a CI job) fails when a changed file under `litellm/` carries more `Any`-typed values than its grandfathered ceiling in `any-discipline-budget.json` (each file's captured count plus 50% headroom). It flags values whose inferred type *contains* `Any`, including the `X | Any` unions mypy/basedpyright accept. Editing a legacy file is fine as long as you don't push its `Any` count past the ceiling; a brand-new file must be `Any`-free. Fix a value by giving it a concrete type (if you're given untyped input, validate with Pydantic). Ideally `# any-ok: ` is never used; treat it as a last resort for a genuine typed/untyped boundary that Pydantic truly can't model +If you're trying to create a new function that relies on untyped stuff, instead of adding more Any's and pushing `reportAny` / `reportExplicitAny` closer to their basedpyright ceilings, just validate it in the caller with Pydantic (a model or `TypeAdapter` that returns the typed thing or raises will do) and then pass the now typed variable in If you get an LIT001 or LIT002 fail, refactor the code to follow functional programming best practices rather than introducing mutable data structures. For example, build values in one shot with comprehensions or generators wrapped in `tuple()` / `frozenset()` instead of seeding an empty `list`/`dict`/`set` and mutating it over time. Ideally `# mutable-ok` is never used; reach for it only as a genuine last resort when an immutable rewrite is truly impossible, and always pair it with a real reason diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9643a58742c..1080579d0fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -154,8 +154,7 @@ Individual linting commands: ```bash make format-check # Check Black formatting make lint-ruff # Run Ruff linting -make lint-mypy # Run MyPy type checking -make lint-any # Gate changed files against their per-file Any budget +make lint-basedpyright # Run basedpyright type checking make check-circular-imports # Check for circular imports make check-import-safety # Check import safety ``` @@ -217,7 +216,7 @@ LiteLLM follows the [Google Python Style Guide](https://google.github.io/stylegu Our automated quality checks include: - **Black** for consistent code formatting - **Ruff** for linting and code quality -- **MyPy** for static type checking +- **basedpyright** for static type checking - **Circular import detection** - **Import safety validation** @@ -231,7 +230,7 @@ If `make lint` fails: 1. **Formatting issues**: Run `make format` to auto-fix 2. **Ruff issues**: Check the output and fix manually -3. **MyPy issues**: Add proper type hints +3. **basedpyright issues**: Add proper type hints 4. **Circular imports**: Refactor import dependencies 5. **Import safety**: Fix any unprotected imports @@ -246,7 +245,7 @@ If `make test-unit` fails: ### 3. Common Development Tips -- **Use type hints**: MyPy requires proper type annotations +- **Use type hints**: basedpyright requires proper type annotations - **Write descriptive commit messages**: Help reviewers understand your changes - **Keep PRs focused**: One feature/fix per PR - **Test edge cases**: Don't just test the happy path diff --git a/Makefile b/Makefile index 0a6d612e8b8..6183dff1556 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,8 @@ test-unit-integrations test-unit-core-utils test-unit-other test-unit-root \ test-proxy-unit-a test-proxy-unit-b test-integration test-unit-helm \ info lint lint-dev format \ - lint-mypy lint-mypy-budget-update lint-basedpyright lint-basedpyright-budget-update \ - lint-ruff-budget lint-any lint-ruff-budget-update lint-budget-update lint-any-budget-update \ + lint-basedpyright lint-basedpyright-budget-update \ + lint-ruff-budget lint-ruff-budget-update lint-budget-update \ install-dev install-proxy-dev install-test-deps install-hooks \ install-helm-unittest check-circular-imports check-import-safety @@ -22,18 +22,14 @@ help: @echo " make install-hooks - Install git hooks (Conventional Commits + Branches)" @echo " make format - Apply Black code formatting" @echo " make format-check - Check Black code formatting (matches CI)" - @echo " make lint - Run all linting (Ruff, MyPy, Black check, circular imports, import safety)" + @echo " make lint - Run all linting (Ruff, basedpyright, Black check, circular imports, import safety)" @echo " make lint-ruff - Run Ruff linting only" - @echo " make lint-mypy - Run MyPy (disallow_untyped_defs), gated by per-rule error counts" - @echo " make lint-mypy-budget-update - Re-capture the MyPy per-rule budget (ratchet)" @echo " make lint-basedpyright - Run basedpyright strict, gated by per-rule error counts" @echo " make lint-basedpyright-budget-update - Re-capture the basedpyright per-rule budget (ratchet)" @echo " make lint-black - Check Black formatting (matches CI)" @echo " make lint-ruff-budget - Gate the codebase total of each strict ruff rule against its ceiling" - @echo " make lint-any - Gate changed files under litellm/ against their per-file Any budget" @echo " make lint-ruff-budget-update - Re-capture per-rule baselines in ruff-strict-budget.json (ratchet)" - @echo " make lint-budget-update - Re-capture all four ratchet budgets (ruff + mypy + basedpyright + any)" - @echo " make lint-any-budget-update - Re-capture the per-file Any budget across the whole tree (ratchet)" + @echo " make lint-budget-update - Re-capture all ratchet budgets (ruff + basedpyright)" @echo " make check-circular-imports - Check for circular imports" @echo " make check-import-safety - Check import safety" @echo " make test - Run all tests" @@ -127,17 +123,11 @@ lint-ruff-FULL-dev: install-dev if [ -n "$$files" ]; then echo "$$files" | xargs $(UV_RUN) ruff check; \ else echo "No changed .py files to check."; fi -lint-mypy: install-dev - cd litellm && ($(UV_RUN) mypy . || true) | $(UV_RUN) python ../scripts/type_check_gate.py --tool mypy - -lint-mypy-budget-update: install-dev - cd litellm && ($(UV_RUN) mypy . || true) | $(UV_RUN) python ../scripts/type_check_gate.py --tool mypy --update - lint-basedpyright: install-dev - ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py --tool basedpyright + ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py lint-basedpyright-budget-update: install-dev - ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py --tool basedpyright --update + ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py --update lint-black: format-check @@ -147,14 +137,8 @@ lint-ruff-budget: install-dev lint-ruff-budget-update: install-dev $(UV_RUN) python scripts/ruff_strict_gate.py --update -# Ratchet all four budgets in one shot (ruff strict + mypy + basedpyright + any) -lint-budget-update: lint-ruff-budget-update lint-mypy-budget-update lint-basedpyright-budget-update lint-any-budget-update - -lint-any: install-dev - $(UV_RUN) python scripts/check_any_discipline.py --changed - -lint-any-budget-update: install-dev - $(UV_RUN) python scripts/check_any_discipline.py --update +# Ratchet all budgets in one shot (ruff strict + basedpyright) +lint-budget-update: lint-ruff-budget-update lint-basedpyright-budget-update check-circular-imports: install-dev cd litellm && $(UV_RUN) python ../tests/documentation_tests/test_circular_imports.py && cd .. @@ -163,10 +147,10 @@ check-import-safety: install-dev @$(UV_RUN) python -c "from litellm import *; print('[from litellm import *] OK! no issues!');" || (echo '🚨 import failed, this means you introduced unprotected imports! 🚨'; exit 1) # Combined linting (matches test-linting.yml workflow) -lint: format-check lint-ruff lint-mypy lint-basedpyright check-circular-imports check-import-safety lint-ruff-budget lint-any +lint: format-check lint-ruff lint-basedpyright check-circular-imports check-import-safety lint-ruff-budget # Faster linting for local development (only checks changed code) -lint-dev: lint-format-changed lint-mypy lint-any check-circular-imports check-import-safety +lint-dev: lint-format-changed check-circular-imports check-import-safety # Testing targets test: install-test-deps diff --git a/any-discipline-budget.json b/any-discipline-budget.json deleted file mode 100644 index d78b15e3653..00000000000 --- a/any-discipline-budget.json +++ /dev/null @@ -1,5974 +0,0 @@ -{ - "litellm/__init__.py": { - "baseline": 801, - "slack": 401 - }, - "litellm/_lazy_imports.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/_logging.py": { - "baseline": 165, - "slack": 83 - }, - "litellm/_redis.py": { - "baseline": 416, - "slack": 208 - }, - "litellm/_redis_credential_provider.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/_service_logger.py": { - "baseline": 96, - "slack": 48 - }, - "litellm/_uuid.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/a2a_protocol/card_resolver.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/a2a_protocol/client.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/a2a_protocol/cost_calculator.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/a2a_protocol/exception_mapping_utils.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/a2a_protocol/litellm_completion_bridge/handler.py": { - "baseline": 104, - "slack": 52 - }, - "litellm/a2a_protocol/litellm_completion_bridge/transformation.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/a2a_protocol/main.py": { - "baseline": 209, - "slack": 105 - }, - "litellm/a2a_protocol/providers/bedrock_agentcore/config.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/a2a_protocol/providers/bedrock_agentcore/handler.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/a2a_protocol/providers/bedrock_agentcore/transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/a2a_protocol/providers/langflow/config.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/a2a_protocol/providers/pydantic_ai_agents/config.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/a2a_protocol/providers/pydantic_ai_agents/handler.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/a2a_protocol/providers/pydantic_ai_agents/transformation.py": { - "baseline": 142, - "slack": 71 - }, - "litellm/a2a_protocol/providers/watsonx_orchestrate/config.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/a2a_protocol/providers/watsonx_orchestrate/handler.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/a2a_protocol/providers/watsonx_orchestrate/transformation.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/a2a_protocol/streaming_iterator.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/a2a_protocol/utils.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/anthropic_beta_headers_manager.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/anthropic_interface/exceptions/exception_mapping_utils.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/anthropic_interface/exceptions/exceptions.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/anthropic_interface/messages/__init__.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/assistants/main.py": { - "baseline": 398, - "slack": 199 - }, - "litellm/assistants/utils.py": { - "baseline": 94, - "slack": 47 - }, - "litellm/batch_completion/main.py": { - "baseline": 178, - "slack": 89 - }, - "litellm/batches/batch_utils.py": { - "baseline": 129, - "slack": 65 - }, - "litellm/batches/main.py": { - "baseline": 240, - "slack": 120 - }, - "litellm/budget_manager.py": { - "baseline": 117, - "slack": 59 - }, - "litellm/caching/_internal_lru_cache.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/caching/azure_blob_cache.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/caching/base_cache.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/caching/caching.py": { - "baseline": 378, - "slack": 189 - }, - "litellm/caching/caching_handler.py": { - "baseline": 337, - "slack": 169 - }, - "litellm/caching/disk_cache.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/caching/dual_cache.py": { - "baseline": 192, - "slack": 96 - }, - "litellm/caching/gcs_cache.py": { - "baseline": 92, - "slack": 46 - }, - "litellm/caching/in_memory_cache.py": { - "baseline": 173, - "slack": 87 - }, - "litellm/caching/llm_caching_handler.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/caching/qdrant_semantic_cache.py": { - "baseline": 359, - "slack": 180 - }, - "litellm/caching/redis_cache.py": { - "baseline": 588, - "slack": 294 - }, - "litellm/caching/redis_cluster_cache.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/caching/redis_semantic_cache.py": { - "baseline": 194, - "slack": 97 - }, - "litellm/caching/s3_cache.py": { - "baseline": 138, - "slack": 69 - }, - "litellm/completion_extras/litellm_responses_transformation/handler.py": { - "baseline": 187, - "slack": 94 - }, - "litellm/completion_extras/litellm_responses_transformation/transformation.py": { - "baseline": 562, - "slack": 281 - }, - "litellm/compression/compress.py": { - "baseline": 120, - "slack": 60 - }, - "litellm/compression/content_detection.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/compression/message_stubbing.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/compression/retrieval_tool.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/compression/scoring/bm25.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/compression/scoring/embedding_scorer.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/constants.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/containers/endpoint_factory.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/containers/main.py": { - "baseline": 278, - "slack": 139 - }, - "litellm/containers/utils.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/cost_calculator.py": { - "baseline": 428, - "slack": 214 - }, - "litellm/endpoints/speech/speech_to_completion_bridge/handler.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/endpoints/speech/speech_to_completion_bridge/transformation.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/evals/main.py": { - "baseline": 522, - "slack": 261 - }, - "litellm/exceptions.py": { - "baseline": 481, - "slack": 241 - }, - "litellm/experimental_mcp_client/client.py": { - "baseline": 174, - "slack": 87 - }, - "litellm/experimental_mcp_client/tools.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/files/main.py": { - "baseline": 257, - "slack": 129 - }, - "litellm/files/streaming.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/files/types.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/fine_tuning/main.py": { - "baseline": 167, - "slack": 84 - }, - "litellm/google_genai/adapters/handler.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/google_genai/adapters/transformation.py": { - "baseline": 325, - "slack": 163 - }, - "litellm/google_genai/main.py": { - "baseline": 179, - "slack": 90 - }, - "litellm/google_genai/streaming_iterator.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/images/main.py": { - "baseline": 326, - "slack": 163 - }, - "litellm/images/utils.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/integrations/SlackAlerting/batching_handler.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/integrations/SlackAlerting/hanging_request_check.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/integrations/SlackAlerting/slack_alerting.py": { - "baseline": 644, - "slack": 322 - }, - "litellm/integrations/SlackAlerting/utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/integrations/additional_logging_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/agentops/agentops.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/integrations/anthropic_cache_control_hook.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/integrations/argilla.py": { - "baseline": 204, - "slack": 102 - }, - "litellm/integrations/arize/__init__.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/integrations/arize/_utils.py": { - "baseline": 632, - "slack": 316 - }, - "litellm/integrations/arize/arize.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/integrations/arize/arize_phoenix.py": { - "baseline": 159, - "slack": 80 - }, - "litellm/integrations/arize/arize_phoenix_client.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/integrations/arize/arize_phoenix_prompt_manager.py": { - "baseline": 117, - "slack": 59 - }, - "litellm/integrations/athina.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/integrations/azure_sentinel/azure_sentinel.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/integrations/azure_storage/azure_storage.py": { - "baseline": 148, - "slack": 74 - }, - "litellm/integrations/bitbucket/__init__.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/integrations/bitbucket/bitbucket_client.py": { - "baseline": 87, - "slack": 44 - }, - "litellm/integrations/bitbucket/bitbucket_prompt_manager.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/integrations/braintrust_logging.py": { - "baseline": 318, - "slack": 159 - }, - "litellm/integrations/braintrust_mock_client.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/integrations/cloudzero/cloudzero.py": { - "baseline": 200, - "slack": 100 - }, - "litellm/integrations/cloudzero/cz_resource_names.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/integrations/cloudzero/cz_stream_api.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/integrations/cloudzero/database.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/integrations/cloudzero/transform.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/integrations/compression_interception/handler.py": { - "baseline": 184, - "slack": 92 - }, - "litellm/integrations/custom_batch_logger.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/integrations/custom_guardrail.py": { - "baseline": 304, - "slack": 152 - }, - "litellm/integrations/custom_logger.py": { - "baseline": 197, - "slack": 99 - }, - "litellm/integrations/custom_prompt_management.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/custom_sso_handler.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/datadog/datadog.py": { - "baseline": 266, - "slack": 133 - }, - "litellm/integrations/datadog/datadog_cost_management.py": { - "baseline": 79, - "slack": 40 - }, - "litellm/integrations/datadog/datadog_handler.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/datadog/datadog_llm_obs.py": { - "baseline": 314, - "slack": 157 - }, - "litellm/integrations/datadog/datadog_metrics.py": { - "baseline": 78, - "slack": 39 - }, - "litellm/integrations/datadog/datadog_mock_client.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/datadog/datadog_team_handler.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/integrations/deepeval/api.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/integrations/deepeval/deepeval.py": { - "baseline": 131, - "slack": 66 - }, - "litellm/integrations/deepeval/types.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/integrations/dotprompt/__init__.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/integrations/dotprompt/dotprompt_manager.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/integrations/dotprompt/prompt_manager.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/integrations/dynamodb.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/integrations/email_alerting.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/integrations/focus/database.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/integrations/focus/destinations/base.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/integrations/focus/destinations/factory.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/integrations/focus/destinations/gcs_destination.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/integrations/focus/destinations/mavvrik_destination.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/integrations/focus/destinations/s3_destination.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/integrations/focus/destinations/vantage_destination.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/integrations/focus/export_engine.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/integrations/focus/focus_logger.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/integrations/focus/schema.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/integrations/focus/serializers/csv.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/integrations/focus/serializers/parquet.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/focus/transformer.py": { - "baseline": 109, - "slack": 55 - }, - "litellm/integrations/galileo.py": { - "baseline": 381, - "slack": 191 - }, - "litellm/integrations/gcs_bucket/gcs_bucket.py": { - "baseline": 104, - "slack": 52 - }, - "litellm/integrations/gcs_bucket/gcs_bucket_base.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/integrations/gcs_bucket/gcs_bucket_mock_client.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/integrations/gcs_pubsub/pub_sub.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/integrations/generic_api/generic_api_callback.py": { - "baseline": 198, - "slack": 99 - }, - "litellm/integrations/generic_prompt_management/generic_prompt_manager.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/integrations/gitlab/__init__.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/integrations/gitlab/gitlab_client.py": { - "baseline": 97, - "slack": 49 - }, - "litellm/integrations/gitlab/gitlab_prompt_manager.py": { - "baseline": 137, - "slack": 69 - }, - "litellm/integrations/greenscale.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/integrations/helicone.py": { - "baseline": 191, - "slack": 96 - }, - "litellm/integrations/helicone_mock_client.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/humanloop.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/integrations/lago.py": { - "baseline": 123, - "slack": 62 - }, - "litellm/integrations/langfuse/langfuse.py": { - "baseline": 610, - "slack": 305 - }, - "litellm/integrations/langfuse/langfuse_handler.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/integrations/langfuse/langfuse_mock_client.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/integrations/langfuse/langfuse_otel.py": { - "baseline": 135, - "slack": 68 - }, - "litellm/integrations/langfuse/langfuse_otel_attributes.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/integrations/langfuse/langfuse_prompt_management.py": { - "baseline": 120, - "slack": 60 - }, - "litellm/integrations/langsmith.py": { - "baseline": 245, - "slack": 123 - }, - "litellm/integrations/langsmith_mock_client.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/langtrace.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/integrations/levo/levo.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/integrations/litellm_agent/litellm_agent_model_resolver.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/integrations/literal_ai.py": { - "baseline": 281, - "slack": 141 - }, - "litellm/integrations/logfire_logger.py": { - "baseline": 88, - "slack": 44 - }, - "litellm/integrations/lunary.py": { - "baseline": 126, - "slack": 63 - }, - "litellm/integrations/mavvrik_focus/mavvrik_focus_logger.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/integrations/mlflow.py": { - "baseline": 239, - "slack": 120 - }, - "litellm/integrations/mock_client_factory.py": { - "baseline": 88, - "slack": 44 - }, - "litellm/integrations/newrelic/newrelic.py": { - "baseline": 274, - "slack": 137 - }, - "litellm/integrations/openmeter.py": { - "baseline": 87, - "slack": 44 - }, - "litellm/integrations/opentelemetry.py": { - "baseline": 1474, - "slack": 737 - }, - "litellm/integrations/opentelemetry_utils/base_otel_llm_obs_attributes.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/opentelemetry_utils/gen_ai_semconv.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/integrations/opik/opik.py": { - "baseline": 82, - "slack": 41 - }, - "litellm/integrations/opik/opik_payload_builder/api.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/integrations/opik/opik_payload_builder/extractors.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/integrations/opik/opik_payload_builder/payload_builders.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/integrations/opik/opik_payload_builder/types.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/integrations/opik/utils.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/integrations/otel/logger.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/integrations/otel/mappers/genai.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/mappers/langfuse.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/otel/mappers/langtrace.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/mappers/openinference.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/integrations/otel/mappers/utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/integrations/otel/model/baggage.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/model/config.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/otel/model/metadata.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/integrations/otel/model/payloads.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/integrations/otel/model/spans.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/integrations/otel/model/utils.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/otel/mount.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/integrations/otel/plumbing/metrics.py": { - "baseline": 115, - "slack": 58 - }, - "litellm/integrations/otel/plumbing/providers.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/otel/plumbing/routing.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/otel/presets/agentops.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/integrations/otel/presets/arize.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/integrations/otel/presets/langfuse.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/otel/presets/langtrace.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/presets/levo.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/presets/phoenix.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/otel/presets/weave.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/runtime.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/posthog.py": { - "baseline": 349, - "slack": 175 - }, - "litellm/integrations/posthog_mock_client.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/prometheus.py": { - "baseline": 1095, - "slack": 548 - }, - "litellm/integrations/prometheus_helpers/__init__.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/integrations/prometheus_helpers/bounded_prometheus_series_tracker.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/prometheus_helpers/prometheus_api.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/integrations/prometheus_services.py": { - "baseline": 112, - "slack": 56 - }, - "litellm/integrations/prompt_layer.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/integrations/prompt_management_base.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/integrations/rubrik.py": { - "baseline": 205, - "slack": 103 - }, - "litellm/integrations/s3.py": { - "baseline": 120, - "slack": 60 - }, - "litellm/integrations/s3_v2.py": { - "baseline": 242, - "slack": 121 - }, - "litellm/integrations/sqs.py": { - "baseline": 120, - "slack": 60 - }, - "litellm/integrations/supabase.py": { - "baseline": 79, - "slack": 40 - }, - "litellm/integrations/traceloop.py": { - "baseline": 130, - "slack": 65 - }, - "litellm/integrations/vantage/vantage_logger.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/integrations/vector_store_integrations/vector_store_pre_call_hook.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/integrations/weave/weave_otel.py": { - "baseline": 108, - "slack": 54 - }, - "litellm/integrations/websearch_interception/handler.py": { - "baseline": 447, - "slack": 224 - }, - "litellm/integrations/websearch_interception/tools.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/integrations/websearch_interception/transformation.py": { - "baseline": 185, - "slack": 93 - }, - "litellm/integrations/weights_biases.py": { - "baseline": 107, - "slack": 54 - }, - "litellm/interactions/agents/http_handler.py": { - "baseline": 170, - "slack": 85 - }, - "litellm/interactions/agents/main.py": { - "baseline": 194, - "slack": 97 - }, - "litellm/interactions/http_handler.py": { - "baseline": 158, - "slack": 79 - }, - "litellm/interactions/litellm_responses_transformation/handler.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/interactions/litellm_responses_transformation/streaming_iterator.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/interactions/litellm_responses_transformation/transformation.py": { - "baseline": 131, - "slack": 66 - }, - "litellm/interactions/main.py": { - "baseline": 153, - "slack": 77 - }, - "litellm/interactions/streaming_iterator.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/interactions/utils.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/litellm_core_utils/app_crypto.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/litellm_core_utils/asyncify.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/litellm_core_utils/audio_utils/utils.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/litellm_core_utils/cli_token_utils.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/litellm_core_utils/cloud_storage_security.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/litellm_core_utils/completion_timeout.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/litellm_core_utils/core_helpers.py": { - "baseline": 192, - "slack": 96 - }, - "litellm/litellm_core_utils/coroutine_checker.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/litellm_core_utils/credential_accessor.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/litellm_core_utils/custom_logger_registry.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/litellm_core_utils/dd_tracing.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/litellm_core_utils/default_encoding.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/litellm_core_utils/dot_notation_indexing.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/litellm_core_utils/duration_parser.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/litellm_core_utils/exception_mapping_utils.py": { - "baseline": 2076, - "slack": 1038 - }, - "litellm/litellm_core_utils/fallback_utils.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/litellm_core_utils/get_blog_posts.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/litellm_core_utils/get_litellm_params.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/litellm_core_utils/get_llm_provider_logic.py": { - "baseline": 143, - "slack": 72 - }, - "litellm/litellm_core_utils/get_model_cost_map.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/litellm_core_utils/get_provider_specific_headers.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/litellm_core_utils/get_supported_openai_params.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/litellm_core_utils/health_check_helpers.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/litellm_core_utils/health_check_utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/litellm_core_utils/initialize_dynamic_callback_params.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/litellm_core_utils/json_validation_rule.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/litellm_core_utils/litellm_logging.py": { - "baseline": 2348, - "slack": 1174 - }, - "litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py": { - "baseline": 107, - "slack": 54 - }, - "litellm/litellm_core_utils/llm_cost_calc/usage_object_transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/litellm_core_utils/llm_cost_calc/utils.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/litellm_core_utils/llm_request_utils.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py": { - "baseline": 336, - "slack": 168 - }, - "litellm/litellm_core_utils/llm_response_utils/get_api_base.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/litellm_core_utils/llm_response_utils/get_formatted_prompt.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/litellm_core_utils/llm_response_utils/get_headers.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/litellm_core_utils/llm_response_utils/response_metadata.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/litellm_core_utils/logging_callback_manager.py": { - "baseline": 90, - "slack": 45 - }, - "litellm/litellm_core_utils/logging_utils.py": { - "baseline": 181, - "slack": 91 - }, - "litellm/litellm_core_utils/logging_worker.py": { - "baseline": 103, - "slack": 52 - }, - "litellm/litellm_core_utils/model_param_helper.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/litellm_core_utils/model_response_utils.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/litellm_core_utils/prompt_templates/common_utils.py": { - "baseline": 362, - "slack": 181 - }, - "litellm/litellm_core_utils/prompt_templates/factory.py": { - "baseline": 1452, - "slack": 726 - }, - "litellm/litellm_core_utils/prompt_templates/huggingface_template_handler.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/litellm_core_utils/prompt_templates/image_handling.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/litellm_core_utils/realtime_streaming.py": { - "baseline": 631, - "slack": 316 - }, - "litellm/litellm_core_utils/redact_messages.py": { - "baseline": 195, - "slack": 98 - }, - "litellm/litellm_core_utils/rules.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/litellm_core_utils/safe_json_dumps.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/litellm_core_utils/safe_json_loads.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/litellm_core_utils/sensitive_data_masker.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/litellm_core_utils/streaming_chunk_builder_utils.py": { - "baseline": 313, - "slack": 157 - }, - "litellm/litellm_core_utils/streaming_handler.py": { - "baseline": 1020, - "slack": 510 - }, - "litellm/litellm_core_utils/token_counter.py": { - "baseline": 249, - "slack": 125 - }, - "litellm/litellm_core_utils/url_utils.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/__init__.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/a2a/chat/guardrail_translation/handler.py": { - "baseline": 158, - "slack": 79 - }, - "litellm/llms/a2a/chat/streaming_iterator.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/a2a/chat/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/a2a/common_utils.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/ai21/chat/transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/aiml/image_generation/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/aiml/image_generation/transformation.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/llms/aiohttp_openai/chat/transformation.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/llms/amazon_nova/chat/transformation.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/llms/anthropic/batches/handler.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/anthropic/batches/transformation.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/anthropic/chat/guardrail_translation/handler.py": { - "baseline": 181, - "slack": 91 - }, - "litellm/llms/anthropic/chat/handler.py": { - "baseline": 390, - "slack": 195 - }, - "litellm/llms/anthropic/chat/transformation.py": { - "baseline": 770, - "slack": 385 - }, - "litellm/llms/anthropic/common_utils.py": { - "baseline": 278, - "slack": 139 - }, - "litellm/llms/anthropic/completion/transformation.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/llms/anthropic/cost_calculation.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/llms/anthropic/count_tokens/handler.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/anthropic/count_tokens/token_counter.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/anthropic/count_tokens/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/anthropic/experimental_pass_through/adapters/handler.py": { - "baseline": 228, - "slack": 114 - }, - "litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py": { - "baseline": 434, - "slack": 217 - }, - "litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py": { - "baseline": 328, - "slack": 164 - }, - "litellm/llms/anthropic/experimental_pass_through/context_management/dispatcher.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/llms/anthropic/experimental_pass_through/context_management/editors/clear_tool_uses.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/llms/anthropic/experimental_pass_through/context_management/editors/compact.py": { - "baseline": 420, - "slack": 210 - }, - "litellm/llms/anthropic/experimental_pass_through/context_management/placeholders.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/anthropic/experimental_pass_through/context_management/result.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py": { - "baseline": 193, - "slack": 97 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/fake_stream_iterator.py": { - "baseline": 78, - "slack": 39 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/handler.py": { - "baseline": 148, - "slack": 74 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py": { - "baseline": 150, - "slack": 75 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/streaming_iterator.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/transformation.py": { - "baseline": 162, - "slack": 81 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/utils.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/anthropic/experimental_pass_through/responses_adapters/handler.py": { - "baseline": 96, - "slack": 48 - }, - "litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py": { - "baseline": 187, - "slack": 94 - }, - "litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py": { - "baseline": 250, - "slack": 125 - }, - "litellm/llms/anthropic/files/handler.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/anthropic/files/transformation.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/llms/anthropic/skills/transformation.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/apiserpent/search/defaults.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/apiserpent/search/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/aws_polly/text_to_speech/transformation.py": { - "baseline": 74, - "slack": 37 - }, - "litellm/llms/azure/assistants.py": { - "baseline": 114, - "slack": 57 - }, - "litellm/llms/azure/audio_transcription/transformation.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/llms/azure/audio_transcriptions.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/llms/azure/azure.py": { - "baseline": 459, - "slack": 230 - }, - "litellm/llms/azure/batches/handler.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/azure/chat/gpt_5_transformation.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/llms/azure/chat/gpt_transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/azure/chat/o_series_handler.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/azure/chat/o_series_transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/azure/common_utils.py": { - "baseline": 221, - "slack": 111 - }, - "litellm/llms/azure/completion/handler.py": { - "baseline": 129, - "slack": 65 - }, - "litellm/llms/azure/completion/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/azure/containers/transformation.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/azure/exception_mapping.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/azure/files/handler.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/llms/azure/fine_tuning/handler.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/azure/image_edit/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/azure/image_generation/http_utils.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/llms/azure/passthrough/transformation.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/azure/realtime/handler.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/azure/realtime/http_transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/azure/responses/o_series_transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/azure/responses/transformation.py": { - "baseline": 72, - "slack": 36 - }, - "litellm/llms/azure/text_to_speech/transformation.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/llms/azure/vector_stores/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/azure/videos/transformation.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/azure_ai/agents/handler.py": { - "baseline": 293, - "slack": 147 - }, - "litellm/llms/azure_ai/agents/transformation.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/azure_ai/anthropic/count_tokens/handler.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/azure_ai/anthropic/count_tokens/token_counter.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/azure_ai/anthropic/count_tokens/transformation.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/llms/azure_ai/anthropic/handler.py": { - "baseline": 101, - "slack": 51 - }, - "litellm/llms/azure_ai/anthropic/messages_transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/azure_ai/anthropic/transformation.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/azure_ai/azure_model_router/transformation.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/azure_ai/chat/transformation.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/llms/azure_ai/embed/cohere_transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/azure_ai/embed/handler.py": { - "baseline": 79, - "slack": 40 - }, - "litellm/llms/azure_ai/image_edit/flux2_transformation.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/llms/azure_ai/image_edit/mai_transformation.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/llms/azure_ai/image_edit/transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/azure_ai/image_generation/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/azure_ai/image_generation/mai_transformation.py": { - "baseline": 88, - "slack": 44 - }, - "litellm/llms/azure_ai/ocr/document_intelligence/transformation.py": { - "baseline": 126, - "slack": 63 - }, - "litellm/llms/azure_ai/ocr/transformation.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/azure_ai/rerank/transformation.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/azure_ai/vector_stores/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/base.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/base_llm/agents/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/anthropic_messages/transformation.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/llms/base_llm/audio_transcription/transformation.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/base_llm/base_model_iterator.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/llms/base_llm/base_utils.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/llms/base_llm/batches/transformation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/base_llm/chat/transformation.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/llms/base_llm/completion/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/containers/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/base_llm/embedding/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/evals/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/files/azure_blob_storage_backend.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/base_llm/files/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/google_genai/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/base_llm/guardrail_translation/base_translation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/base_llm/guardrail_translation/utils.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/base_llm/image_edit/transformation.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/base_llm/image_generation/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/image_variations/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/interactions/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/base_llm/managed_resources/base_managed_resource.py": { - "baseline": 111, - "slack": 56 - }, - "litellm/llms/base_llm/managed_resources/isolation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/base_llm/managed_resources/utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/base_llm/ocr/transformation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/base_llm/passthrough/transformation.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/base_llm/realtime/http_transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/realtime/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/rerank/transformation.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/llms/base_llm/responses/transformation.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/llms/base_llm/search/transformation.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/llms/base_llm/skills/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/text_to_speech/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/base_llm/vector_store/transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/base_llm/vector_store_files/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/videos/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/baseten/chat.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/bedrock/base_aws_llm.py": { - "baseline": 341, - "slack": 171 - }, - "litellm/llms/bedrock/batches/handler.py": { - "baseline": 78, - "slack": 39 - }, - "litellm/llms/bedrock/batches/transformation.py": { - "baseline": 144, - "slack": 72 - }, - "litellm/llms/bedrock/chat/agentcore/transformation.py": { - "baseline": 195, - "slack": 98 - }, - "litellm/llms/bedrock/chat/converse_handler.py": { - "baseline": 152, - "slack": 76 - }, - "litellm/llms/bedrock/chat/converse_transformation.py": { - "baseline": 527, - "slack": 264 - }, - "litellm/llms/bedrock/chat/invoke_agent/transformation.py": { - "baseline": 65, - "slack": 33 - }, - "litellm/llms/bedrock/chat/invoke_handler.py": { - "baseline": 634, - "slack": 317 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_ai21_transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_cohere_transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_deepseek_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_llama_transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_mistral_transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_moonshot_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_nova_transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_openai_transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_qwen2_transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_qwen3_transformation.py": { - "baseline": 72, - "slack": 36 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_titan_transformation.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py": { - "baseline": 102, - "slack": 51 - }, - "litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude2_transformation.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py": { - "baseline": 139, - "slack": 70 - }, - "litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py": { - "baseline": 192, - "slack": 96 - }, - "litellm/llms/bedrock/chat/mantle/transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/bedrock/claude_platform/common_utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/bedrock/claude_platform/messages_transformation.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/bedrock/claude_platform/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/bedrock/common_utils.py": { - "baseline": 279, - "slack": 140 - }, - "litellm/llms/bedrock/count_tokens/bedrock_token_counter.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/bedrock/count_tokens/handler.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/bedrock/count_tokens/transformation.py": { - "baseline": 106, - "slack": 53 - }, - "litellm/llms/bedrock/embed/amazon_nova_transformation.py": { - "baseline": 96, - "slack": 48 - }, - "litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/bedrock/embed/amazon_titan_multimodal_transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/bedrock/embed/amazon_titan_v2_transformation.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/bedrock/embed/cohere_transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/bedrock/embed/embedding.py": { - "baseline": 225, - "slack": 113 - }, - "litellm/llms/bedrock/embed/twelvelabs_marengo_transformation.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/llms/bedrock/files/handler.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/llms/bedrock/files/transformation.py": { - "baseline": 218, - "slack": 109 - }, - "litellm/llms/bedrock/image_edit/amazon_nova_canvas_image_edit_transformation.py": { - "baseline": 156, - "slack": 78 - }, - "litellm/llms/bedrock/image_edit/handler.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/llms/bedrock/image_edit/stability_transformation.py": { - "baseline": 87, - "slack": 44 - }, - "litellm/llms/bedrock/image_generation/amazon_nova_canvas_transformation.py": { - "baseline": 79, - "slack": 40 - }, - "litellm/llms/bedrock/image_generation/amazon_stability1_transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/bedrock/image_generation/amazon_stability3_transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/bedrock/image_generation/amazon_titan_transformation.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/llms/bedrock/image_generation/cost_calculator.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/bedrock/image_generation/image_handler.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py": { - "baseline": 237, - "slack": 119 - }, - "litellm/llms/bedrock/messages/mantle_transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/bedrock/passthrough/guardrail_translation/handler.py": { - "baseline": 337, - "slack": 169 - }, - "litellm/llms/bedrock/passthrough/transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/bedrock/realtime/handler.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/bedrock/realtime/transformation.py": { - "baseline": 293, - "slack": 147 - }, - "litellm/llms/bedrock/rerank/handler.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/bedrock/rerank/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/bedrock/vector_stores/transformation.py": { - "baseline": 123, - "slack": 62 - }, - "litellm/llms/bedrock_mantle/chat/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/bedrock_mantle/responses/transformation.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/llms/black_forest_labs/image_edit/handler.py": { - "baseline": 126, - "slack": 63 - }, - "litellm/llms/black_forest_labs/image_edit/transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/black_forest_labs/image_generation/handler.py": { - "baseline": 130, - "slack": 65 - }, - "litellm/llms/black_forest_labs/image_generation/transformation.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/brave/search/transformation.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/bytez/chat/transformation.py": { - "baseline": 128, - "slack": 64 - }, - "litellm/llms/cerebras/chat.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/chatgpt/authenticator.py": { - "baseline": 107, - "slack": 54 - }, - "litellm/llms/chatgpt/chat/streaming_utils.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/chatgpt/chat/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/chatgpt/common_utils.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/llms/chatgpt/responses/transformation.py": { - "baseline": 105, - "slack": 53 - }, - "litellm/llms/clarifai/chat/transformation.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/cloudflare/chat/transformation.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/llms/codestral/completion/handler.py": { - "baseline": 128, - "slack": 64 - }, - "litellm/llms/codestral/completion/transformation.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/cohere/chat/transformation.py": { - "baseline": 113, - "slack": 57 - }, - "litellm/llms/cohere/chat/v2_transformation.py": { - "baseline": 97, - "slack": 49 - }, - "litellm/llms/cohere/common_utils.py": { - "baseline": 165, - "slack": 83 - }, - "litellm/llms/cohere/embed/handler.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/cohere/embed/transformation.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/llms/cohere/embed/v1_transformation.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/cohere/rerank/guardrail_translation/handler.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/cohere/rerank/transformation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/cohere/rerank_v2/transformation.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/cometapi/chat/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/cometapi/embed/transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/cometapi/image_generation/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/cometapi/image_generation/transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/compactifai/chat/transformation.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/custom_httpx/aiohttp_handler.py": { - "baseline": 187, - "slack": 94 - }, - "litellm/llms/custom_httpx/aiohttp_transport.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/custom_httpx/async_client_cleanup.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/custom_httpx/container_handler.py": { - "baseline": 169, - "slack": 85 - }, - "litellm/llms/custom_httpx/http_handler.py": { - "baseline": 339, - "slack": 170 - }, - "litellm/llms/custom_httpx/httpx_handler.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/custom_httpx/llm_http_handler.py": { - "baseline": 3900, - "slack": 1950 - }, - "litellm/llms/custom_httpx/mock_transport.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/custom_llm.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/dashscope/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/dashscope/common_utils.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/dashscope/cost_calculator.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/dashscope/embed/transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/dashscope/image_generation/transformation.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/llms/dashscope/rerank/transformation.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/llms/databricks/chat/transformation.py": { - "baseline": 168, - "slack": 84 - }, - "litellm/llms/databricks/common_utils.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/databricks/cost_calculator.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/databricks/embed/handler.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/databricks/embed/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/databricks/responses/transformation.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/databricks/streaming_utils.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/llms/dataforseo/search/transformation.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/deepgram/audio_transcription/transformation.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/llms/deepinfra/chat/transformation.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/deepinfra/rerank/transformation.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/llms/deepseek/chat/transformation.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/llms/deepseek/messages/transformation.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/deprecated_providers/aleph_alpha.py": { - "baseline": 102, - "slack": 51 - }, - "litellm/llms/deprecated_providers/palm.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/llms/docker_model_runner/chat/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/duckduckgo/search/transformation.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/llms/elevenlabs/audio_transcription/transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/elevenlabs/text_to_speech/transformation.py": { - "baseline": 94, - "slack": 47 - }, - "litellm/llms/exa_ai/search/transformation.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/fal_ai/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/fal_ai/image_generation/bria_transformation.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/llms/fal_ai/image_generation/bytedance_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/fal_ai/image_generation/flux_pro_v11_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/fal_ai/image_generation/flux_pro_v11_ultra_transformation.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/fal_ai/image_generation/flux_schnell_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/fal_ai/image_generation/ideogram_v3_transformation.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/llms/fal_ai/image_generation/imagen4_transformation.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/llms/fal_ai/image_generation/nano_banana_transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/fal_ai/image_generation/recraft_v3_transformation.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/llms/fal_ai/image_generation/stable_diffusion_transformation.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/llms/fal_ai/image_generation/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/fastcrw/search/transformation.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/llms/featherless_ai/chat/transformation.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/llms/firecrawl/search/transformation.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/fireworks_ai/chat/transformation.py": { - "baseline": 124, - "slack": 62 - }, - "litellm/llms/fireworks_ai/common_utils.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/fireworks_ai/completion/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/fireworks_ai/cost_calculator.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/fireworks_ai/embed/fireworks_ai_transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/fireworks_ai/rerank/transformation.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/gemini/agents/transformation.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/gemini/chat/transformation.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/llms/gemini/common_utils.py": { - "baseline": 158, - "slack": 79 - }, - "litellm/llms/gemini/count_tokens/handler.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/llms/gemini/files/transformation.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/gemini/google_genai/transformation.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/gemini/image_edit/cost_calculator.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/gemini/image_edit/transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/gemini/image_generation/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/gemini/image_generation/transformation.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/gemini/image_usage_transformation.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/gemini/interactions/transformation.py": { - "baseline": 92, - "slack": 46 - }, - "litellm/llms/gemini/realtime/transformation.py": { - "baseline": 324, - "slack": 162 - }, - "litellm/llms/gemini/vector_stores/transformation.py": { - "baseline": 97, - "slack": 49 - }, - "litellm/llms/gemini/videos/transformation.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/llms/gigachat/authenticator.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/gigachat/chat/streaming.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/llms/gigachat/chat/transformation.py": { - "baseline": 157, - "slack": 79 - }, - "litellm/llms/gigachat/embedding/transformation.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/llms/gigachat/file_handler.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/llms/github_copilot/authenticator.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/github_copilot/chat/transformation.py": { - "baseline": 87, - "slack": 44 - }, - "litellm/llms/github_copilot/common_utils.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/llms/github_copilot/embedding/transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/github_copilot/responses/transformation.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/llms/google_pse/search/transformation.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/llms/gradient_ai/chat/transformation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/groq/chat/handler.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/groq/chat/transformation.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/groq/stt/transformation.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/heroku/chat/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/hosted_vllm/chat/transformation.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/llms/hosted_vllm/embedding/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/hosted_vllm/rerank/transformation.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/hosted_vllm/responses/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/hosted_vllm/transcriptions/transformation.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/huggingface/chat/transformation.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/huggingface/common_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/huggingface/embedding/handler.py": { - "baseline": 157, - "slack": 79 - }, - "litellm/llms/huggingface/embedding/transformation.py": { - "baseline": 224, - "slack": 112 - }, - "litellm/llms/huggingface/rerank/transformation.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/llms/hyperbolic/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/inception/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/inception/completion/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/infinity/common_utils.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/infinity/embedding/transformation.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/llms/infinity/rerank/transformation.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/jina_ai/common_utils.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/jina_ai/embedding/transformation.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/jina_ai/rerank/transformation.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/langflow/a2a.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/langflow/chat/transformation.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/llms/langgraph/chat/sse_iterator.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/langgraph/chat/transformation.py": { - "baseline": 103, - "slack": 52 - }, - "litellm/llms/lemonade/chat/transformation.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/llms/linkup/search/transformation.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/llms/litellm_proxy/chat/transformation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/litellm_proxy/image_edit/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/litellm_proxy/image_generation/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/litellm_proxy/skills/code_execution.py": { - "baseline": 111, - "slack": 56 - }, - "litellm/llms/litellm_proxy/skills/handler.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/llms/litellm_proxy/skills/prompt_injection.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/litellm_proxy/skills/sandbox_executor.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/llms/litellm_proxy/skills/transformation.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/llms/lm_studio/chat/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/lm_studio/embed/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/manus/files/transformation.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/llms/manus/responses/transformation.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/maritalk.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/meta_llama/chat/transformation.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/milvus/vector_stores/transformation.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/llms/minimax/chat/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/minimax/text_to_speech/transformation.py": { - "baseline": 112, - "slack": 56 - }, - "litellm/llms/mistral/audio_transcription/transformation.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/llms/mistral/chat/transformation.py": { - "baseline": 183, - "slack": 92 - }, - "litellm/llms/mistral/ocr/guardrail_translation/handler.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/mistral/ocr/transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/modelscope/chat/transformation.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/modelscope/image_generation/transformation.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/llms/moonshot/chat/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/morph/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/nebius/chat/transformation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/nlp_cloud/chat/handler.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/nlp_cloud/chat/transformation.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/llms/nlp_cloud/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/novita/chat/transformation.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/nscale/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/nvidia_nim/chat/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/nvidia_nim/embed.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/nvidia_nim/rerank/ranking_transformation.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/nvidia_nim/rerank/transformation.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/nvidia_riva/audio_transcription/audio_utils.py": { - "baseline": 89, - "slack": 45 - }, - "litellm/llms/nvidia_riva/audio_transcription/handler.py": { - "baseline": 142, - "slack": 71 - }, - "litellm/llms/nvidia_riva/audio_transcription/transformation.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/llms/nvidia_riva/common_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/oci/chat/cohere.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/llms/oci/chat/generic.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/oci/chat/transformation.py": { - "baseline": 159, - "slack": 80 - }, - "litellm/llms/oci/common_utils.py": { - "baseline": 221, - "slack": 111 - }, - "litellm/llms/oci/embed/transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/ollama/chat/transformation.py": { - "baseline": 173, - "slack": 87 - }, - "litellm/llms/ollama/common_utils.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/llms/ollama/completion/handler.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/llms/ollama/completion/transformation.py": { - "baseline": 143, - "slack": 72 - }, - "litellm/llms/oobabooga/chat/oobabooga.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/llms/oobabooga/chat/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/oobabooga/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/openai/chat/gpt_5_transformation.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/llms/openai/chat/gpt_audio_transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/openai/chat/gpt_transformation.py": { - "baseline": 132, - "slack": 66 - }, - "litellm/llms/openai/chat/guardrail_translation/handler.py": { - "baseline": 196, - "slack": 98 - }, - "litellm/llms/openai/chat/o_series_transformation.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/openai/common_utils.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/openai/completion/guardrail_translation/handler.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/llms/openai/completion/handler.py": { - "baseline": 140, - "slack": 70 - }, - "litellm/llms/openai/completion/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/openai/completion/utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/openai/containers/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/openai/cost_calculation.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/openai/embeddings/guardrail_translation/handler.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/openai/evals/transformation.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/llms/openai/fine_tuning/handler.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/openai/image_edit/dalle2_transformation.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/openai/image_edit/transformation.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/llms/openai/image_generation/cost_calculator.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/openai/image_generation/dall_e_2_transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/openai/image_generation/dall_e_3_transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/openai/image_generation/gpt_transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/openai/image_generation/guardrail_translation/handler.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/openai/image_variations/handler.py": { - "baseline": 69, - "slack": 35 - }, - "litellm/llms/openai/image_variations/transformation.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/openai/openai.py": { - "baseline": 664, - "slack": 332 - }, - "litellm/llms/openai/realtime/handler.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/openai/realtime/http_transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/openai/responses/count_tokens/handler.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/openai/responses/count_tokens/token_counter.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/openai/responses/count_tokens/transformation.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/llms/openai/responses/guardrail_translation/handler.py": { - "baseline": 256, - "slack": 128 - }, - "litellm/llms/openai/responses/transformation.py": { - "baseline": 126, - "slack": 63 - }, - "litellm/llms/openai/speech/guardrail_translation/handler.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/openai/transcriptions/gpt_transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/openai/transcriptions/guardrail_translation/handler.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/openai/transcriptions/handler.py": { - "baseline": 74, - "slack": 37 - }, - "litellm/llms/openai/transcriptions/whisper_transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/openai/vector_store_files/transformation.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/openai/vector_stores/transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/openai/videos/transformation.py": { - "baseline": 154, - "slack": 77 - }, - "litellm/llms/openai_like/chat/handler.py": { - "baseline": 113, - "slack": 57 - }, - "litellm/llms/openai_like/chat/transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/openai_like/common_utils.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/openai_like/dynamic_config.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/llms/openai_like/embedding/handler.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/llms/openai_like/json_loader.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/openai_like/responses/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/openrouter/chat/transformation.py": { - "baseline": 88, - "slack": 44 - }, - "litellm/llms/openrouter/embedding/transformation.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/llms/openrouter/image_edit/transformation.py": { - "baseline": 93, - "slack": 47 - }, - "litellm/llms/openrouter/image_generation/transformation.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/llms/openrouter/responses/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/ovhcloud/audio_transcription/transformation.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/ovhcloud/chat/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/ovhcloud/embedding/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/parallel_ai/search/transformation.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/pass_through/guardrail_translation/handler.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/perplexity/chat/transformation.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/llms/perplexity/cost_calculator.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/perplexity/embedding/transformation.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/llms/perplexity/responses/transformation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/perplexity/search/transformation.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/llms/petals/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/petals/completion/handler.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/petals/completion/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/pg_vector/vector_stores/transformation.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/predibase/chat/handler.py": { - "baseline": 98, - "slack": 49 - }, - "litellm/llms/predibase/chat/transformation.py": { - "baseline": 116, - "slack": 58 - }, - "litellm/llms/predibase/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/ragflow/chat/transformation.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/llms/ragflow/vector_stores/transformation.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/llms/recraft/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/recraft/image_edit/transformation.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/recraft/image_generation/transformation.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/reducto/common.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/reducto/ocr/transformation.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/llms/replicate/chat/handler.py": { - "baseline": 139, - "slack": 70 - }, - "litellm/llms/replicate/chat/transformation.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/llms/replicate/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/runwayml/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/runwayml/image_generation/transformation.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/llms/runwayml/text_to_speech/transformation.py": { - "baseline": 116, - "slack": 58 - }, - "litellm/llms/runwayml/videos/transformation.py": { - "baseline": 125, - "slack": 63 - }, - "litellm/llms/s3_vectors/vector_stores/transformation.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/sagemaker/chat/handler.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/llms/sagemaker/chat/transformation.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/llms/sagemaker/common_utils.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/llms/sagemaker/completion/handler.py": { - "baseline": 301, - "slack": 151 - }, - "litellm/llms/sagemaker/completion/transformation.py": { - "baseline": 95, - "slack": 48 - }, - "litellm/llms/sagemaker/embedding/cohere_transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/sagemaker/embedding/transformation.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/sagemaker/nova/transformation.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/sambanova/chat.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/sambanova/common_utils.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/sambanova/embedding/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/sap/chat/handler.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/llms/sap/chat/models.py": { - "baseline": 95, - "slack": 48 - }, - "litellm/llms/sap/chat/transformation.py": { - "baseline": 182, - "slack": 91 - }, - "litellm/llms/sap/credentials.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/llms/sap/embed/transformation.py": { - "baseline": 82, - "slack": 41 - }, - "litellm/llms/scaleway/audio_transcription/transformation.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/searchapi/search/transformation.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/searxng/search/transformation.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/llms/serper/search/transformation.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/llms/snowflake/chat/transformation.py": { - "baseline": 244, - "slack": 122 - }, - "litellm/llms/snowflake/common_utils.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/snowflake/embedding/transformation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/snowflake/utils.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/llms/soniox/audio_transcription/handler.py": { - "baseline": 196, - "slack": 98 - }, - "litellm/llms/soniox/audio_transcription/transformation.py": { - "baseline": 107, - "slack": 54 - }, - "litellm/llms/soniox/common_utils.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/llms/stability/image_edit/transformations.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/llms/stability/image_generation/transformation.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/llms/tavily/search/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/together_ai/chat.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/together_ai/completion/transformation.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/llms/together_ai/cost_calculator.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/together_ai/rerank/handler.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/together_ai/rerank/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/topaz/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/topaz/image_variations/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/triton/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/triton/completion/transformation.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/triton/embedding/transformation.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/v0/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/vercel_ai_gateway/chat/transformation.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/llms/vercel_ai_gateway/embedding/transformation.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/vertex_ai/agent_engine/sse_iterator.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/vertex_ai/agent_engine/transformation.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/vertex_ai/aws_credentials_supplier.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/vertex_ai/batches/handler.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/llms/vertex_ai/batches/transformation.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/vertex_ai/common_utils.py": { - "baseline": 493, - "slack": 247 - }, - "litellm/llms/vertex_ai/context_caching/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/vertex_ai/context_caching/vertex_ai_context_caching.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/llms/vertex_ai/cost_calculator.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/vertex_ai/count_tokens/handler.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/vertex_ai/files/handler.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/llms/vertex_ai/files/transformation.py": { - "baseline": 177, - "slack": 89 - }, - "litellm/llms/vertex_ai/fine_tuning/handler.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/llms/vertex_ai/gemini/transformation.py": { - "baseline": 311, - "slack": 156 - }, - "litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py": { - "baseline": 912, - "slack": 456 - }, - "litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_handler.py": { - "baseline": 82, - "slack": 41 - }, - "litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/llms/vertex_ai/google_genai/transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/vertex_ai/image_edit/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/vertex_ai/image_edit/vertex_gemini_transformation.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/llms/vertex_ai/image_edit/vertex_imagen_transformation.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/llms/vertex_ai/image_generation/image_generation_handler.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/vertex_ai/image_generation/vertex_gemini_transformation.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/llms/vertex_ai/image_generation/vertex_imagen_transformation.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/llms/vertex_ai/multimodal_embeddings/embedding_handler.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/llms/vertex_ai/multimodal_embeddings/transformation.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/vertex_ai/ocr/deepseek_transformation.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/llms/vertex_ai/ocr/transformation.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/vertex_ai/rag_engine/ingestion.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/vertex_ai/rag_engine/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/vertex_ai/realtime/transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/vertex_ai/rerank/transformation.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/llms/vertex_ai/text_to_speech/transformation.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/vertex_ai/vector_stores/rag_api/transformation.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/llms/vertex_ai/vector_stores/search_api/transformation.py": { - "baseline": 88, - "slack": 44 - }, - "litellm/llms/vertex_ai/vertex_ai_aws_wif.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/vertex_ai/vertex_ai_non_gemini.py": { - "baseline": 319, - "slack": 160 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/ai21/transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/output_params_utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/count_tokens/handler.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/gpt_oss/transformation.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/llama3/transformation.py": { - "baseline": 65, - "slack": 33 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/main.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/llms/vertex_ai/vertex_embeddings/bge.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/vertex_ai/vertex_embeddings/embedding_handler.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/vertex_ai/vertex_embeddings/transformation.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/vertex_ai/vertex_embeddings/types.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/vertex_ai/vertex_gemma_models/main.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/vertex_ai/vertex_gemma_models/transformation.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/llms/vertex_ai/vertex_llm_base.py": { - "baseline": 305, - "slack": 153 - }, - "litellm/llms/vertex_ai/vertex_model_garden/main.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/vertex_ai/videos/transformation.py": { - "baseline": 164, - "slack": 82 - }, - "litellm/llms/vllm/common_utils.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/vllm/completion/handler.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/llms/vllm/passthrough/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/volcengine/chat/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/volcengine/common_utils.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/volcengine/embedding/transformation.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/volcengine/responses/transformation.py": { - "baseline": 200, - "slack": 100 - }, - "litellm/llms/voyage/embedding/transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/voyage/embedding/transformation_contextual.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/voyage/embedding/transformation_multimodal.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/voyage/rerank/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/wandb/chat/transformation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/watsonx/audio_transcription/transformation.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/watsonx/chat/handler.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/llms/watsonx/chat/transformation.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/llms/watsonx/common_utils.py": { - "baseline": 101, - "slack": 51 - }, - "litellm/llms/watsonx/completion/transformation.py": { - "baseline": 117, - "slack": 59 - }, - "litellm/llms/watsonx/embed/transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/watsonx/passthrough/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/watsonx/rerank/transformation.py": { - "baseline": 89, - "slack": 45 - }, - "litellm/llms/xai/chat/transformation.py": { - "baseline": 105, - "slack": 53 - }, - "litellm/llms/xai/common_utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/xai/cost_calculator.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/xai/oauth.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/xai/realtime/handler.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/xai/responses/transformation.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/llms/xinference/image_generation/transformation.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/llms/you_com/search/transformation.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/main.py": { - "baseline": 3138, - "slack": 1569 - }, - "litellm/models/access_group.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/models/base.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/models/budget.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/models/config.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/models/credentials.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/models/end_user.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/models/managed_files.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/models/mcp_server.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/models/model.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/models/object_permission.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/models/organization.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/models/organization_membership.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/models/project.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/models/skills.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/models/spend_logs.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/models/tag.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/models/team.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/models/team_membership.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/models/user.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/models/verification_token.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/ocr/main.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/passthrough/main.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/passthrough/timeout_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/passthrough/utils.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/proxy/_experimental/mcp_server/auth/token_exchange.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py": { - "baseline": 175, - "slack": 88 - }, - "litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/proxy/_experimental/mcp_server/cost_calculator.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/_experimental/mcp_server/db.py": { - "baseline": 428, - "slack": 214 - }, - "litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py": { - "baseline": 193, - "slack": 97 - }, - "litellm/proxy/_experimental/mcp_server/elicitation_handler.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/proxy/_experimental/mcp_server/guardrail_translation/handler.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/proxy/_experimental/mcp_server/mcp_debug.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/_experimental/mcp_server/mcp_server_manager.py": { - "baseline": 877, - "slack": 439 - }, - "litellm/proxy/_experimental/mcp_server/oauth2_token_cache.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/proxy/_experimental/mcp_server/oauth_utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py": { - "baseline": 213, - "slack": 107 - }, - "litellm/proxy/_experimental/mcp_server/rest_endpoints.py": { - "baseline": 288, - "slack": 144 - }, - "litellm/proxy/_experimental/mcp_server/sampling_handler.py": { - "baseline": 541, - "slack": 271 - }, - "litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py": { - "baseline": 98, - "slack": 49 - }, - "litellm/proxy/_experimental/mcp_server/server.py": { - "baseline": 971, - "slack": 486 - }, - "litellm/proxy/_experimental/mcp_server/sse_transport.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/proxy/_experimental/mcp_server/tool_registry.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/_experimental/mcp_server/toolset_db.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/proxy/_experimental/mcp_server/ui_session_utils.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/proxy/_experimental/mcp_server/utils.py": { - "baseline": 99, - "slack": 50 - }, - "litellm/proxy/_lazy_features.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/proxy/_lazy_openapi_snapshot.py": { - "baseline": 72, - "slack": 36 - }, - "litellm/proxy/_logging.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/_types.py": { - "baseline": 848, - "slack": 424 - }, - "litellm/proxy/a2a/agent_card.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/proxy/a2a/discovery.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/a2a/endpoints.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/proxy/agent_endpoints/a2a_endpoints.py": { - "baseline": 333, - "slack": 167 - }, - "litellm/proxy/agent_endpoints/a2a_routing.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/proxy/agent_endpoints/agent_registry.py": { - "baseline": 143, - "slack": 72 - }, - "litellm/proxy/agent_endpoints/auth/agent_permission_handler.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/proxy/agent_endpoints/databricks_oauth.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/proxy/agent_endpoints/endpoints.py": { - "baseline": 222, - "slack": 111 - }, - "litellm/proxy/agent_endpoints/model_list_helpers.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/analytics_endpoints/analytics_endpoints.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py": { - "baseline": 225, - "slack": 113 - }, - "litellm/proxy/anthropic_endpoints/endpoints.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/proxy/anthropic_endpoints/skills_endpoints.py": { - "baseline": 106, - "slack": 53 - }, - "litellm/proxy/auth/auth_checks.py": { - "baseline": 654, - "slack": 327 - }, - "litellm/proxy/auth/auth_checks_organization.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/auth/auth_exception_handler.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/auth/auth_utils.py": { - "baseline": 276, - "slack": 138 - }, - "litellm/proxy/auth/handle_jwt.py": { - "baseline": 378, - "slack": 189 - }, - "litellm/proxy/auth/ip_address_utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/proxy/auth/litellm_license.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/proxy/auth/login_utils.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/proxy/auth/model_checks.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/proxy/auth/oauth2_check.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/auth/oauth2_proxy_hook.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/auth/rds_iam_token.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/proxy/auth/route_checks.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/proxy/auth/trusted_proxy_utils.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/proxy/auth/user_api_key_auth.py": { - "baseline": 590, - "slack": 295 - }, - "litellm/proxy/batches_endpoints/endpoints.py": { - "baseline": 344, - "slack": 172 - }, - "litellm/proxy/caching_routes.py": { - "baseline": 105, - "slack": 53 - }, - "litellm/proxy/client/chat.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/proxy/client/cli/commands/agents.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/proxy/client/cli/commands/auth.py": { - "baseline": 236, - "slack": 118 - }, - "litellm/proxy/client/cli/commands/chat.py": { - "baseline": 101, - "slack": 51 - }, - "litellm/proxy/client/cli/commands/credentials.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/proxy/client/cli/commands/http.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/client/cli/commands/keys.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/proxy/client/cli/commands/models.py": { - "baseline": 151, - "slack": 76 - }, - "litellm/proxy/client/cli/commands/teams.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/proxy/client/cli/commands/users.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/proxy/client/cli/interface.py": { - "baseline": 95, - "slack": 48 - }, - "litellm/proxy/client/cli/main.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/client/credentials.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/client/health.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/client/http_client.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/client/keys.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/proxy/client/model_groups.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/client/models.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/proxy/client/teams.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/client/users.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/common_request_processing.py": { - "baseline": 753, - "slack": 377 - }, - "litellm/proxy/common_utils/admin_ui_utils.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/common_utils/banner.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/common_utils/cache_coordinator.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/proxy/common_utils/cache_pydantic_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/common_utils/callback_utils.py": { - "baseline": 255, - "slack": 128 - }, - "litellm/proxy/common_utils/custom_openapi_spec.py": { - "baseline": 119, - "slack": 60 - }, - "litellm/proxy/common_utils/debug_utils.py": { - "baseline": 366, - "slack": 183 - }, - "litellm/proxy/common_utils/encrypt_decrypt_utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/proxy/common_utils/expired_ui_session_key_cleanup_manager.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/proxy/common_utils/get_routes.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/proxy/common_utils/http_parsing_utils.py": { - "baseline": 177, - "slack": 89 - }, - "litellm/proxy/common_utils/key_rotation_manager.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/proxy/common_utils/load_config_utils.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/proxy/common_utils/openai_endpoint_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/common_utils/openapi_schema_compat.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/proxy/common_utils/performance_utils.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/proxy/common_utils/proxy_rate_limit_error.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/proxy/common_utils/proxy_state.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/common_utils/rbac_utils.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/proxy/common_utils/reset_budget_job.py": { - "baseline": 539, - "slack": 270 - }, - "litellm/proxy/common_utils/swagger_utils.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/common_utils/timezone_utils.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/common_utils/user_api_key_cache.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/proxy/compliance_checks.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/config_management_endpoints/pass_through_endpoints.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/container_endpoints/endpoints.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/proxy/container_endpoints/handler_factory.py": { - "baseline": 120, - "slack": 60 - }, - "litellm/proxy/container_endpoints/ownership.py": { - "baseline": 167, - "slack": 84 - }, - "litellm/proxy/credential_endpoints/endpoints.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/proxy/custom_hooks/custom_ui_sso_hook.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/custom_prompt_management.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/custom_sso.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/db/check_migration.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/db/create_views.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/proxy/db/db_spend_update_writer.py": { - "baseline": 347, - "slack": 174 - }, - "litellm/proxy/db/db_transaction_queue/base_update_queue.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/proxy/db/db_transaction_queue/daily_spend_update_queue.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/proxy/db/db_transaction_queue/pod_lock_manager.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/proxy/db/db_transaction_queue/redis_update_buffer.py": { - "baseline": 140, - "slack": 70 - }, - "litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/proxy/db/db_transaction_queue/spend_update_queue.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/proxy/db/db_url_settings.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/db/dynamo_db.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/db/exception_handler.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/proxy/db/log_db_metrics.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/proxy/db/prisma_client.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/proxy/db/routing_prisma_wrapper.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/proxy/db/spend_counter_reseed.py": { - "baseline": 65, - "slack": 33 - }, - "litellm/proxy/db/spend_log_tool_index.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/proxy/db/tool_registry_writer.py": { - "baseline": 145, - "slack": 73 - }, - "litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/proxy/example_config_yaml/custom_auth.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/proxy/example_config_yaml/custom_callbacks.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/example_config_yaml/custom_callbacks1.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/example_config_yaml/custom_guardrail.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/proxy/example_config_yaml/custom_handler.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/example_config_yaml/pipeline_test_guardrails.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/fine_tuning_endpoints/endpoints.py": { - "baseline": 222, - "slack": 111 - }, - "litellm/proxy/google_endpoints/agents_endpoints.py": { - "baseline": 158, - "slack": 79 - }, - "litellm/proxy/google_endpoints/endpoints.py": { - "baseline": 131, - "slack": 66 - }, - "litellm/proxy/guardrails/_content_utils.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/proxy/guardrails/guardrail_endpoints.py": { - "baseline": 610, - "slack": 305 - }, - "litellm/proxy/guardrails/guardrail_helpers.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/guardrails/guardrail_hooks/aim/aim.py": { - "baseline": 139, - "slack": 70 - }, - "litellm/proxy/guardrails/guardrail_hooks/akto/akto.py": { - "baseline": 127, - "slack": 64 - }, - "litellm/proxy/guardrails/guardrail_hooks/aporia_ai/aporia_ai.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/proxy/guardrails/guardrail_hooks/azure/base.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/guardrails/guardrail_hooks/azure/text_moderation.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py": { - "baseline": 271, - "slack": 136 - }, - "litellm/proxy/guardrails/guardrail_hooks/block_code_execution/block_code_execution.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/guardrails/guardrail_hooks/cato_networks/cato_networks.py": { - "baseline": 402, - "slack": 201 - }, - "litellm/proxy/guardrails/guardrail_hooks/cisco_ai_defense/cisco_ai_defense.py": { - "baseline": 756, - "slack": 378 - }, - "litellm/proxy/guardrails/guardrail_hooks/cisco_ai_defense/cisco_ai_defense_mcp.py": { - "baseline": 324, - "slack": 162 - }, - "litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py": { - "baseline": 104, - "slack": 52 - }, - "litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/proxy/guardrails/guardrail_hooks/custom_code/primitives.py": { - "baseline": 102, - "slack": 51 - }, - "litellm/proxy/guardrails/guardrail_hooks/custom_code/sandbox.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/guardrails/guardrail_hooks/custom_guardrail.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py": { - "baseline": 79, - "slack": 40 - }, - "litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py": { - "baseline": 107, - "slack": 54 - }, - "litellm/proxy/guardrails/guardrail_hooks/grayswan/__init__.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/proxy/guardrails/guardrail_hooks/grayswan/grayswan.py": { - "baseline": 154, - "slack": 77 - }, - "litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py": { - "baseline": 194, - "slack": 97 - }, - "litellm/proxy/guardrails/guardrail_hooks/ibm_guardrails/ibm_detector.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/proxy/guardrails/guardrail_hooks/javelin/javelin.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/proxy/guardrails/guardrail_hooks/lakera_ai.py": { - "baseline": 92, - "slack": 46 - }, - "litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/proxy/guardrails/guardrail_hooks/lasso/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py": { - "baseline": 373, - "slack": 187 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/competitor_intent/airline.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/competitor_intent/base.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py": { - "baseline": 275, - "slack": 138 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/guardrail_benchmarks/test_eval.py": { - "baseline": 194, - "slack": 97 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/patterns.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/proxy/guardrails/guardrail_hooks/mcp_end_user_permission/mcp_end_user_permission.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py": { - "baseline": 202, - "slack": 101 - }, - "litellm/proxy/guardrails/guardrail_hooks/mcp_security/mcp_security_guardrail.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/base.py": { - "baseline": 160, - "slack": 80 - }, - "litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/purview_dlp.py": { - "baseline": 131, - "slack": 66 - }, - "litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py": { - "baseline": 196, - "slack": 98 - }, - "litellm/proxy/guardrails/guardrail_hooks/noma/noma.py": { - "baseline": 202, - "slack": 101 - }, - "litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py": { - "baseline": 69, - "slack": 35 - }, - "litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/__init__.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py": { - "baseline": 656, - "slack": 328 - }, - "litellm/proxy/guardrails/guardrail_hooks/pillar/pillar.py": { - "baseline": 181, - "slack": 91 - }, - "litellm/proxy/guardrails/guardrail_hooks/presidio.py": { - "baseline": 462, - "slack": 231 - }, - "litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py": { - "baseline": 259, - "slack": 130 - }, - "litellm/proxy/guardrails/guardrail_hooks/promptguard/promptguard.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/proxy/guardrails/guardrail_hooks/qohash/qohash.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py": { - "baseline": 127, - "slack": 64 - }, - "litellm/proxy/guardrails/guardrail_hooks/semantic_guard/route_loader.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/proxy/guardrails/guardrail_hooks/semantic_guard/semantic_guard.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/proxy/guardrails/guardrail_hooks/tool_permission.py": { - "baseline": 210, - "slack": 105 - }, - "litellm/proxy/guardrails/guardrail_hooks/tool_policy/tool_policy_guardrail.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py": { - "baseline": 144, - "slack": 72 - }, - "litellm/proxy/guardrails/guardrail_hooks/vigil_guard/vigil_guard.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py": { - "baseline": 221, - "slack": 111 - }, - "litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py": { - "baseline": 194, - "slack": 97 - }, - "litellm/proxy/guardrails/guardrail_initializers.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/proxy/guardrails/guardrail_registry.py": { - "baseline": 228, - "slack": 114 - }, - "litellm/proxy/guardrails/init_guardrails.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/guardrails/tool_name_extraction.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/proxy/guardrails/usage_endpoints.py": { - "baseline": 454, - "slack": 227 - }, - "litellm/proxy/guardrails/usage_tracking.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/proxy/health_check.py": { - "baseline": 302, - "slack": 151 - }, - "litellm/proxy/health_check_utils/shared_health_check_manager.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/proxy/health_endpoints/_health_endpoints.py": { - "baseline": 686, - "slack": 343 - }, - "litellm/proxy/hooks/azure_content_safety.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/proxy/hooks/batch_rate_limiter.py": { - "baseline": 111, - "slack": 56 - }, - "litellm/proxy/hooks/batch_redis_get.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/proxy/hooks/cache_control_check.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/hooks/dynamic_rate_limiter.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/proxy/hooks/dynamic_rate_limiter_v3.py": { - "baseline": 112, - "slack": 56 - }, - "litellm/proxy/hooks/key_management_event_hooks.py": { - "baseline": 114, - "slack": 57 - }, - "litellm/proxy/hooks/litellm_skills/main.py": { - "baseline": 389, - "slack": 195 - }, - "litellm/proxy/hooks/max_budget_limiter.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/proxy/hooks/max_budget_per_session_limiter.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/proxy/hooks/max_iterations_limiter.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/proxy/hooks/mcp_semantic_filter/hook.py": { - "baseline": 105, - "slack": 53 - }, - "litellm/proxy/hooks/model_max_budget_limiter.py": { - "baseline": 115, - "slack": 58 - }, - "litellm/proxy/hooks/parallel_request_limiter.py": { - "baseline": 417, - "slack": 209 - }, - "litellm/proxy/hooks/parallel_request_limiter_v3.py": { - "baseline": 630, - "slack": 315 - }, - "litellm/proxy/hooks/prompt_injection_detection.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/proxy/hooks/proxy_track_cost_callback.py": { - "baseline": 204, - "slack": 102 - }, - "litellm/proxy/hooks/rate_limiter_utils.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/proxy/hooks/responses_id_security.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/proxy/hooks/sensitive_data_routing.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/proxy/hooks/user_management_event_hooks.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/image_endpoints/endpoints.py": { - "baseline": 125, - "slack": 63 - }, - "litellm/proxy/lambda.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/litellm_pre_call_utils.py": { - "baseline": 912, - "slack": 456 - }, - "litellm/proxy/management_endpoints/access_group_endpoints.py": { - "baseline": 277, - "slack": 139 - }, - "litellm/proxy/management_endpoints/budget_management_endpoints.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/proxy/management_endpoints/cache_settings_endpoints.py": { - "baseline": 154, - "slack": 77 - }, - "litellm/proxy/management_endpoints/callback_management_endpoints.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/proxy/management_endpoints/common_daily_activity.py": { - "baseline": 446, - "slack": 223 - }, - "litellm/proxy/management_endpoints/common_utils.py": { - "baseline": 147, - "slack": 74 - }, - "litellm/proxy/management_endpoints/compliance_endpoints.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/proxy/management_endpoints/config_override_endpoints.py": { - "baseline": 165, - "slack": 83 - }, - "litellm/proxy/management_endpoints/cost_tracking_settings.py": { - "baseline": 104, - "slack": 52 - }, - "litellm/proxy/management_endpoints/customer_endpoints.py": { - "baseline": 198, - "slack": 99 - }, - "litellm/proxy/management_endpoints/fallback_management_endpoints.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/proxy/management_endpoints/internal_user_endpoints.py": { - "baseline": 720, - "slack": 360 - }, - "litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/proxy/management_endpoints/key_management_endpoints.py": { - "baseline": 1565, - "slack": 783 - }, - "litellm/proxy/management_endpoints/mcp_management_endpoints.py": { - "baseline": 625, - "slack": 313 - }, - "litellm/proxy/management_endpoints/model_access_group_management_endpoints.py": { - "baseline": 163, - "slack": 82 - }, - "litellm/proxy/management_endpoints/model_management_endpoints.py": { - "baseline": 389, - "slack": 195 - }, - "litellm/proxy/management_endpoints/organization_endpoints.py": { - "baseline": 322, - "slack": 161 - }, - "litellm/proxy/management_endpoints/policy_endpoints/ai_policy_suggester.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/management_endpoints/policy_endpoints/endpoints.py": { - "baseline": 286, - "slack": 143 - }, - "litellm/proxy/management_endpoints/router_settings_endpoints.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/proxy/management_endpoints/scim/scim_transformations.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/proxy/management_endpoints/scim/scim_v2.py": { - "baseline": 640, - "slack": 320 - }, - "litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/proxy/management_endpoints/sso_helper_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/management_endpoints/tag_management_endpoints.py": { - "baseline": 207, - "slack": 104 - }, - "litellm/proxy/management_endpoints/team_callback_endpoints.py": { - "baseline": 127, - "slack": 64 - }, - "litellm/proxy/management_endpoints/team_endpoints.py": { - "baseline": 1236, - "slack": 618 - }, - "litellm/proxy/management_endpoints/tool_management_endpoints.py": { - "baseline": 172, - "slack": 86 - }, - "litellm/proxy/management_endpoints/types.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/management_endpoints/ui_sso.py": { - "baseline": 1009, - "slack": 505 - }, - "litellm/proxy/management_endpoints/usage_endpoints/ai_usage_chat.py": { - "baseline": 163, - "slack": 82 - }, - "litellm/proxy/management_endpoints/usage_endpoints/endpoints.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py": { - "baseline": 176, - "slack": 88 - }, - "litellm/proxy/management_endpoints/workflow_management_endpoints.py": { - "baseline": 147, - "slack": 74 - }, - "litellm/proxy/management_helpers/audit_logs.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/proxy/management_helpers/object_permission_utils.py": { - "baseline": 145, - "slack": 73 - }, - "litellm/proxy/management_helpers/team_member_permission_checks.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/management_helpers/user_invitation.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/proxy/management_helpers/utils.py": { - "baseline": 277, - "slack": 139 - }, - "litellm/proxy/mcp_tools.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/memory/memory_endpoints.py": { - "baseline": 178, - "slack": 89 - }, - "litellm/proxy/middleware/in_flight_requests_middleware.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/middleware/prometheus_auth_middleware.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/proxy/middleware/request_size_limit_middleware.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/proxy/ocr_endpoints/endpoints.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/proxy/openai_evals_endpoints/endpoints.py": { - "baseline": 265, - "slack": 133 - }, - "litellm/proxy/openai_files_endpoints/common_utils.py": { - "baseline": 206, - "slack": 103 - }, - "litellm/proxy/openai_files_endpoints/file_content_streaming_handler.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/proxy/openai_files_endpoints/files_endpoints.py": { - "baseline": 427, - "slack": 214 - }, - "litellm/proxy/openai_files_endpoints/storage_backend_service.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/proxy/pass_through_endpoints/jsonpath_extractor.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py": { - "baseline": 375, - "slack": 188 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py": { - "baseline": 164, - "slack": 82 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/assembly_passthrough_logging_handler.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/base_passthrough_logging_handler.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/cohere_passthrough_logging_handler.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/gemini_passthrough_logging_handler.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py": { - "baseline": 114, - "slack": 57 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_ai_live_passthrough_logging_handler.py": { - "baseline": 141, - "slack": 71 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py": { - "baseline": 163, - "slack": 82 - }, - "litellm/proxy/pass_through_endpoints/managed_id_codec.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/pass_through_endpoints/managed_id_rewriter.py": { - "baseline": 312, - "slack": 156 - }, - "litellm/proxy/pass_through_endpoints/pass_through_endpoints.py": { - "baseline": 937, - "slack": 469 - }, - "litellm/proxy/pass_through_endpoints/passthrough_endpoint_router.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/proxy/pass_through_endpoints/passthrough_guardrails.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/pass_through_endpoints/streaming_handler.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/proxy/pass_through_endpoints/success_handler.py": { - "baseline": 113, - "slack": 57 - }, - "litellm/proxy/policy_engine/attachment_registry.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/proxy/policy_engine/init_policies.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/proxy/policy_engine/pipeline_executor.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/proxy/policy_engine/policy_endpoints.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/proxy/policy_engine/policy_registry.py": { - "baseline": 257, - "slack": 129 - }, - "litellm/proxy/policy_engine/policy_resolve_endpoints.py": { - "baseline": 187, - "slack": 94 - }, - "litellm/proxy/policy_engine/policy_validator.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/post_call_rules.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/prisma_migration.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/prometheus_cleanup.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/prompts/init_prompts.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/prompts/prompt_endpoints.py": { - "baseline": 181, - "slack": 91 - }, - "litellm/proxy/prompts/prompt_registry.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/proxy/proxy_cli.py": { - "baseline": 308, - "slack": 154 - }, - "litellm/proxy/proxy_server.py": { - "baseline": 5145, - "slack": 2573 - }, - "litellm/proxy/public_endpoints/public_endpoints.py": { - "baseline": 165, - "slack": 83 - }, - "litellm/proxy/rag_endpoints/endpoints.py": { - "baseline": 249, - "slack": 125 - }, - "litellm/proxy/realtime_endpoints/endpoints.py": { - "baseline": 243, - "slack": 122 - }, - "litellm/proxy/rerank_endpoints/endpoints.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/proxy/response_api_endpoints/endpoints.py": { - "baseline": 322, - "slack": 161 - }, - "litellm/proxy/response_polling/background_streaming.py": { - "baseline": 162, - "slack": 81 - }, - "litellm/proxy/response_polling/polling_handler.py": { - "baseline": 82, - "slack": 41 - }, - "litellm/proxy/route_llm_request.py": { - "baseline": 138, - "slack": 69 - }, - "litellm/proxy/search_endpoints/endpoints.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/proxy/search_endpoints/search_tool_management.py": { - "baseline": 95, - "slack": 48 - }, - "litellm/proxy/search_endpoints/search_tool_registry.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/proxy/shutdown/graceful_shutdown_manager.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/spend_tracking/budget_reservation.py": { - "baseline": 245, - "slack": 123 - }, - "litellm/proxy/spend_tracking/cloudzero_endpoints.py": { - "baseline": 121, - "slack": 61 - }, - "litellm/proxy/spend_tracking/cold_storage_handler.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/spend_tracking/spend_log_error_logger.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/spend_tracking/spend_management_endpoints.py": { - "baseline": 980, - "slack": 490 - }, - "litellm/proxy/spend_tracking/spend_tracking_utils.py": { - "baseline": 274, - "slack": 137 - }, - "litellm/proxy/spend_tracking/vantage_endpoints.py": { - "baseline": 177, - "slack": 89 - }, - "litellm/proxy/types_utils/utils.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py": { - "baseline": 481, - "slack": 241 - }, - "litellm/proxy/utils.py": { - "baseline": 1731, - "slack": 866 - }, - "litellm/proxy/vector_store_endpoints/endpoints.py": { - "baseline": 163, - "slack": 82 - }, - "litellm/proxy/vector_store_endpoints/management_endpoints.py": { - "baseline": 248, - "slack": 124 - }, - "litellm/proxy/vector_store_endpoints/utils.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/proxy/vector_store_files_endpoints/endpoints.py": { - "baseline": 292, - "slack": 146 - }, - "litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/proxy/video_endpoints/endpoints.py": { - "baseline": 238, - "slack": 119 - }, - "litellm/proxy/video_endpoints/utils.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy_auth/credentials.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/rag/__init__.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/rag/ingestion/base_ingestion.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/rag/ingestion/bedrock_ingestion.py": { - "baseline": 273, - "slack": 137 - }, - "litellm/rag/ingestion/file_parsers/pdf_parser.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/rag/ingestion/gemini_ingestion.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/rag/ingestion/openai_ingestion.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/rag/ingestion/s3_vectors_ingestion.py": { - "baseline": 252, - "slack": 126 - }, - "litellm/rag/ingestion/vertex_ai_ingestion.py": { - "baseline": 134, - "slack": 67 - }, - "litellm/rag/main.py": { - "baseline": 108, - "slack": 54 - }, - "litellm/rag/rag_query.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/realtime_api/main.py": { - "baseline": 165, - "slack": 83 - }, - "litellm/repositories/base_repository.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/repositories/budget_repository.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/repositories/config_repository.py": { - "baseline": 94, - "slack": 47 - }, - "litellm/repositories/credentials_repository.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/repositories/model_repository.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/repositories/object_permission_repository.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/repositories/organization_repository.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/repositories/project_repository.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/repositories/table_repositories.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/repositories/team_repository.py": { - "baseline": 163, - "slack": 82 - }, - "litellm/repositories/user_repository.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/repositories/verification_token_repository.py": { - "baseline": 116, - "slack": 58 - }, - "litellm/rerank_api/main.py": { - "baseline": 129, - "slack": 65 - }, - "litellm/rerank_api/rerank_utils.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/responses/file_search/emulated_handler.py": { - "baseline": 280, - "slack": 140 - }, - "litellm/responses/litellm_completion_transformation/handler.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/responses/litellm_completion_transformation/session_handler.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/responses/litellm_completion_transformation/streaming_iterator.py": { - "baseline": 152, - "slack": 76 - }, - "litellm/responses/litellm_completion_transformation/transformation.py": { - "baseline": 555, - "slack": 278 - }, - "litellm/responses/main.py": { - "baseline": 567, - "slack": 284 - }, - "litellm/responses/mcp/chat_completions_handler.py": { - "baseline": 367, - "slack": 184 - }, - "litellm/responses/mcp/litellm_proxy_mcp_handler.py": { - "baseline": 454, - "slack": 227 - }, - "litellm/responses/mcp/mcp_streaming_iterator.py": { - "baseline": 202, - "slack": 101 - }, - "litellm/responses/sse_output_recovery.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/responses/streaming_iterator.py": { - "baseline": 990, - "slack": 495 - }, - "litellm/responses/utils.py": { - "baseline": 295, - "slack": 148 - }, - "litellm/router.py": { - "baseline": 4343, - "slack": 2172 - }, - "litellm/router_strategy/adaptive_router/adaptive_router.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/router_strategy/adaptive_router/bandit.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/router_strategy/adaptive_router/hooks.py": { - "baseline": 143, - "slack": 72 - }, - "litellm/router_strategy/adaptive_router/signals.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/router_strategy/adaptive_router/update_queue.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/router_strategy/auto_router/auto_router.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/router_strategy/auto_router/litellm_encoder.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/router_strategy/base_routing_strategy.py": { - "baseline": 114, - "slack": 57 - }, - "litellm/router_strategy/budget_limiter.py": { - "baseline": 347, - "slack": 174 - }, - "litellm/router_strategy/complexity_router/complexity_router.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/router_strategy/complexity_router/evals/eval_complexity_router.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/router_strategy/least_busy.py": { - "baseline": 155, - "slack": 78 - }, - "litellm/router_strategy/lowest_cost.py": { - "baseline": 211, - "slack": 106 - }, - "litellm/router_strategy/lowest_latency.py": { - "baseline": 404, - "slack": 202 - }, - "litellm/router_strategy/lowest_tpm_rpm.py": { - "baseline": 168, - "slack": 84 - }, - "litellm/router_strategy/lowest_tpm_rpm_v2.py": { - "baseline": 351, - "slack": 176 - }, - "litellm/router_strategy/quality_router/config.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/router_strategy/quality_router/quality_router.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/router_strategy/simple_shuffle.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/router_strategy/tag_based_routing.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/router_utils/add_retry_fallback_headers.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/router_utils/batch_utils.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/router_utils/client_initalization_utils.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/router_utils/clientside_credential_handler.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/router_utils/common_utils.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/router_utils/cooldown_cache.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/router_utils/cooldown_callbacks.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/router_utils/cooldown_handlers.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/router_utils/fallback_event_handlers.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/router_utils/get_retry_from_policy.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/router_utils/handle_error.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/router_utils/health_state_cache.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/router_utils/pattern_match_deployments.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/router_utils/pre_call_checks/deployment_affinity_check.py": { - "baseline": 112, - "slack": 56 - }, - "litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/router_utils/pre_call_checks/model_rate_limit_check.py": { - "baseline": 134, - "slack": 67 - }, - "litellm/router_utils/pre_call_checks/prompt_caching_deployment_check.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/router_utils/pre_call_checks/responses_api_deployment_check.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/router_utils/prompt_caching_cache.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/router_utils/router_callbacks/track_deployment_metrics.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/router_utils/search_api_router.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/scheduler.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/search/cost_calculator.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/search/main.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/secret_managers/aws_secret_manager.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/secret_managers/aws_secret_manager_v2.py": { - "baseline": 132, - "slack": 66 - }, - "litellm/secret_managers/base_secret_manager.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/secret_managers/custom_secret_manager_loader.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/secret_managers/cyberark_secret_manager.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/secret_managers/get_azure_ad_token_provider.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/secret_managers/google_kms.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/secret_managers/google_secret_manager.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/secret_managers/hashicorp_secret_manager.py": { - "baseline": 220, - "slack": 110 - }, - "litellm/secret_managers/main.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/secret_managers/secret_manager_handler.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/setup_wizard.py": { - "baseline": 109, - "slack": 55 - }, - "litellm/skills/main.py": { - "baseline": 215, - "slack": 108 - }, - "litellm/timeout.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/types/access_group.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/types/adapter.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/agents.py": { - "baseline": 117, - "slack": 59 - }, - "litellm/types/caching.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/types/completion.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/types/compression.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/types/containers/main.py": { - "baseline": 96, - "slack": 48 - }, - "litellm/types/embedding.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/files.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/types/google_genai/main.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/types/guardrails.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/types/images/main.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/types/integrations/anthropic_cache_control_hook.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/types/integrations/argilla.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/integrations/arize.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/types/integrations/arize_phoenix.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/integrations/base_health_check.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/integrations/compression_interception.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/integrations/custom_logger.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/integrations/datadog.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/types/integrations/datadog_cost_management.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/types/integrations/datadog_llm_obs.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/types/integrations/datadog_metrics.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/types/integrations/gcs_bucket.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/types/integrations/langfuse.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/types/integrations/langfuse_otel.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/integrations/langsmith.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/types/integrations/pagerduty.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/types/integrations/posthog.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/integrations/prometheus.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/types/integrations/rag/bedrock_knowledgebase.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/types/integrations/s3_v2.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/integrations/slack_alerting.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/types/integrations/websearch_interception.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/interactions/generated.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/types/litellm_core_utils/streaming_chunk_builder_utils.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/types/llms/aiml.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/types/llms/anthropic.py": { - "baseline": 258, - "slack": 129 - }, - "litellm/types/llms/anthropic_messages/anthropic_response.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/types/llms/anthropic_skills.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/types/llms/azure_ai.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/types/llms/base.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/types/llms/bedrock.py": { - "baseline": 402, - "slack": 201 - }, - "litellm/types/llms/bedrock_agentcore.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/types/llms/bedrock_invoke_agents.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/types/llms/cohere.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/types/llms/custom_http.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/llms/custom_llm.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/llms/databricks.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/types/llms/gemini.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/types/llms/langgraph.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/types/llms/mistral.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/types/llms/oci.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/types/llms/ollama.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/types/llms/openai.py": { - "baseline": 750, - "slack": 375 - }, - "litellm/types/llms/openai_evals.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/types/llms/openrouter.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/llms/recraft.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/types/llms/rerank.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/llms/stability.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/types/llms/vertex_ai.py": { - "baseline": 347, - "slack": 174 - }, - "litellm/types/llms/vertex_ai_text_to_speech.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/types/llms/watsonx.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/types/llms/xai.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/types/management_endpoints/cache_settings_endpoints.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/management_endpoints/router_settings_endpoints.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/types/mcp.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/types/mcp_server/mcp_server_manager.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/mcp_server/mcp_toolset.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/types/mcp_server/tool_registry.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/types/memory_management.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/types/passthrough_endpoints/pass_through_endpoints.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/prompts/init_prompts.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/types/proxy/claude_code_endpoints.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/types/proxy/cloudzero_endpoints.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/types/proxy/compliance_endpoints.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/types/proxy/control_plane_endpoints.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/azure/azure_prompt_shield.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/azure/azure_text_moderation.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/base.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/block_code_execution.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/cisco_ai_defense.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/dynamoai.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/enkryptai.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/ibm/ibm_detector.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/javelin.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/litellm_content_filter.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/presidio.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/tool_permission.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/xecguard.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/proxy/litellm_pre_call_utils.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/proxy/management_endpoints/common_daily_activity.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/types/proxy/management_endpoints/config_overrides.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/proxy/management_endpoints/internal_user_endpoints.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/types/proxy/management_endpoints/key_management_endpoints.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/types/proxy/management_endpoints/model_management_endpoints.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/types/proxy/management_endpoints/scim_v2.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/types/proxy/management_endpoints/team_endpoints.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/types/proxy/management_endpoints/ui_sso.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/types/proxy/policy_engine/pipeline_types.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/types/proxy/policy_engine/policy_types.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/proxy/policy_engine/resolver_types.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/types/proxy/policy_engine/validation_types.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/proxy/prompt_endpoints.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/proxy/public_endpoints/public_endpoints.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/types/proxy/ui_sso.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/types/proxy/vantage_endpoints.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/types/rag.py": { - "baseline": 78, - "slack": 39 - }, - "litellm/types/realtime.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/types/rerank.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/types/responses/main.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/types/router.py": { - "baseline": 194, - "slack": 97 - }, - "litellm/types/search.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/types/services.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/types/tag_management.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/tool_management.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/types/utils.py": { - "baseline": 1085, - "slack": 543 - }, - "litellm/types/vector_store_files.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/types/vector_stores.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/types/videos/main.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/types/videos/utils.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/utils.py": { - "baseline": 3367, - "slack": 1684 - }, - "litellm/vector_store_files/main.py": { - "baseline": 244, - "slack": 122 - }, - "litellm/vector_store_files/utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/vector_stores/main.py": { - "baseline": 268, - "slack": 134 - }, - "litellm/vector_stores/utils.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/vector_stores/vector_store_registry.py": { - "baseline": 94, - "slack": 47 - }, - "litellm/videos/main.py": { - "baseline": 513, - "slack": 257 - }, - "litellm/videos/utils.py": { - "baseline": 54, - "slack": 27 - } -} diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 7ece944fd0e..d241c501797 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -5455,21 +5455,19 @@ class StandardLoggingPayloadSetup: error_information = StandardLoggingPayloadSetup.get_error_information( original_exception=original_exception, ) - if not metadata.get("client_disconnected"): # any-ok: untyped metadata + if not metadata.get("client_disconnected"): return error_information, error_str - client_disconnect_error = metadata.get( # any-ok: untyped metadata - "error_information" - ) - if isinstance(client_disconnect_error, dict): # any-ok: untyped metadata + client_disconnect_error = metadata.get("error_information") + if isinstance(client_disconnect_error, dict): error_information = cast( StandardLoggingPayloadErrorInformation, - client_disconnect_error, # any-ok: untyped metadata + client_disconnect_error, ) else: error_information = cast( StandardLoggingPayloadErrorInformation, - { # any-ok: untyped metadata + { "error_code": "499", "error_message": "Client disconnected the request", "error_class": "ClientDisconnected", @@ -5808,7 +5806,7 @@ def get_standard_logging_object_payload( error_information, error_str = ( StandardLoggingPayloadSetup.get_error_information_for_logging_payload( - metadata=metadata, # any-ok: untyped metadata + metadata=metadata, original_exception=original_exception, error_str=error_str, ) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 2e18d15a5ce..c24c990f356 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -2215,7 +2215,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): inference_geo = _usage["inference_geo"] service_tier = cast( str | None, - _usage.get("service_tier"), # any-ok: untyped usage dict + _usage.get("service_tier"), ) iterations: Optional[List[Any]] = _usage.get("iterations") diff --git a/litellm/llms/hosted_vllm/chat/transformation.py b/litellm/llms/hosted_vllm/chat/transformation.py index 40906e83a9d..7c42e6a9a00 100644 --- a/litellm/llms/hosted_vllm/chat/transformation.py +++ b/litellm/llms/hosted_vllm/chat/transformation.py @@ -205,53 +205,40 @@ class HostedVLLMChatConfig(OpenAIGPTConfig): tool_calls: list[ChatCompletionAssistantToolCall] = [] content_blocks: list[object] = [] has_structured_content = False - for c in existing_content: # any-ok: untyped content - if ( - isinstance(c, dict) # any-ok: untyped content - and c.get("type") == "text" # any-ok: untyped content - ): - text_parts.append( # any-ok: untyped content - c.get("text", "") # any-ok: untyped content - ) - content_blocks.append(c) # any-ok: untyped content - elif ( - isinstance(c, dict) # any-ok: untyped content - and c.get("type") == "tool_use" # any-ok: untyped content - ): - tool_input = c.get("input", {}) # any-ok: untyped content + for c in existing_content: + if isinstance(c, dict) and c.get("type") == "text": + text_parts.append(c.get("text", "")) + content_blocks.append(c) + elif isinstance(c, dict) and c.get("type") == "tool_use": + tool_input = c.get("input", {}) tool_calls.append( ChatCompletionAssistantToolCall( - id=c.get("id"), # any-ok: untyped content + id=c.get("id"), type="function", function=ChatCompletionToolCallFunctionChunk( - name=c.get("name"), # any-ok: untyped content + name=c.get("name"), arguments=( tool_input if isinstance( - tool_input, # any-ok: untyped content - str, # any-ok: untyped content - ) - else json.dumps( - tool_input # any-ok: untyped content + tool_input, + str, ) + else json.dumps(tool_input) ), ), ) ) else: - content_blocks.append(c) # any-ok: untyped content + content_blocks.append(c) has_structured_content = True if tool_calls: existing_tool_calls = message.get("tool_calls") if isinstance(existing_tool_calls, list): existing_tool_call_ids = { - tool_call.get("id") # any-ok: untyped content + tool_call.get("id") for tool_call in existing_tool_calls - if isinstance( - tool_call, dict - ) # any-ok: untyped content - and tool_call.get("id") - is not None # any-ok: untyped content + if isinstance(tool_call, dict) + and tool_call.get("id") is not None } new_tool_calls = [ tool_call @@ -264,7 +251,7 @@ class HostedVLLMChatConfig(OpenAIGPTConfig): ) else: message["tool_calls"] = tool_calls - content_str = "\n".join(text_parts) # any-ok: untyped content + content_str = "\n".join(text_parts) new_content = ( content_blocks if has_structured_content else content_str ) diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index a2634ffaa40..c171538b9c0 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -2844,7 +2844,7 @@ async def make_call( sync_stream=False, logging_obj=logging_obj, response_headers=response.headers, - response=response, # any-ok: untyped stream + response=response, ) # LOGGING logging_obj.post_call( @@ -2888,7 +2888,7 @@ def make_sync_call( sync_stream=True, logging_obj=logging_obj, response_headers=response.headers, - response=response, # any-ok: untyped stream + response=response, ) # LOGGING @@ -3661,16 +3661,14 @@ class ModelResponseIterator: raise RuntimeError(f"Error parsing chunk: {e},\nReceived chunk: {chunk}") async def aclose(self) -> None: - iterator = getattr( # any-ok: untyped stream + iterator = getattr( self, "async_response_iterator", - self.streaming_response, # any-ok: untyped stream + self.streaming_response, ) - if iterator is not None and hasattr( # any-ok: untyped stream - iterator, "aclose" # any-ok: untyped stream - ): + if iterator is not None and hasattr(iterator, "aclose"): try: - await iterator.aclose() # any-ok: untyped stream + await iterator.aclose() except Exception as e: # noqa: BLE001 verbose_logger.debug( "ModelResponseIterator.aclose: error closing iterator: %s", e @@ -3684,14 +3682,10 @@ class ModelResponseIterator: ) def close(self) -> None: - iterator = getattr( # any-ok: untyped stream - self, "response_iterator", self.streaming_response # any-ok: untyped stream - ) - if iterator is not None and hasattr( # any-ok: untyped stream - iterator, "close" # any-ok: untyped stream - ): + iterator = getattr(self, "response_iterator", self.streaming_response) + if iterator is not None and hasattr(iterator, "close"): try: - iterator.close() # any-ok: untyped stream + iterator.close() except Exception as e: # noqa: BLE001 verbose_logger.debug( "ModelResponseIterator.close: error closing iterator: %s", e diff --git a/litellm/mypy.ini b/litellm/mypy.ini deleted file mode 100644 index b65e11bab42..00000000000 --- a/litellm/mypy.ini +++ /dev/null @@ -1,22 +0,0 @@ -[mypy] -warn_return_any = True -ignore_missing_imports = True -disallow_untyped_defs = True -mypy_path = litellm/stubs -namespace_packages = True -disable_error_code = - annotation-unchecked, - import-untyped - -[mypy-litellm.*] -ignore_missing_imports = False - -[mypy-google.*] -ignore_missing_imports = True - -[mypy-cryptography.hazmat.bindings._rust.x509] -ignore_errors = True - -[mypy-fastuuid.*] -ignore_missing_imports = True -ignore_errors = True \ No newline at end of file diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 7cccae6e761..2a0e8402f17 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -110,46 +110,32 @@ async def _record_streaming_client_disconnect_if_needed( if not disconnected: return False - logging_obj = request_data.get("litellm_logging_obj") # any-ok: untyped request - if logging_obj is not None: # any-ok: untyped request - litellm_params = ( - logging_obj.model_call_details.setdefault( # any-ok: untyped request - "litellm_params", {} - ) - ) + logging_obj = request_data.get("litellm_logging_obj") + if logging_obj is not None: + litellm_params = logging_obj.model_call_details.setdefault("litellm_params", {}) + _apply_client_disconnect_metadata(litellm_params.setdefault("metadata", {})) _apply_client_disconnect_metadata( - litellm_params.setdefault("metadata", {}) # any-ok: untyped request - ) - _apply_client_disconnect_metadata( - logging_obj.model_call_details.setdefault( # any-ok: untyped request - "metadata", {} - ) + logging_obj.model_call_details.setdefault("metadata", {}) ) - _apply_client_disconnect_metadata( - request_data.setdefault("metadata", {}) # any-ok: untyped request - ) - litellm_params = request_data.setdefault( # any-ok: untyped request - "litellm_params", {} # any-ok: untyped request - ) - _apply_client_disconnect_metadata( - litellm_params.setdefault("metadata", {}) # any-ok: untyped request - ) + _apply_client_disconnect_metadata(request_data.setdefault("metadata", {})) + litellm_params = request_data.setdefault("litellm_params", {}) + _apply_client_disconnect_metadata(litellm_params.setdefault("metadata", {})) verbose_proxy_logger.debug( "Recorded streaming client disconnect with error_code=499 for litellm_call_id=%s", - request_data.get("litellm_call_id"), # any-ok: untyped request + request_data.get("litellm_call_id"), ) return True async def _cancel_pending_gather_tasks(tasks: list["asyncio.Task[Any]"]) -> None: - pending_tasks = [task for task in tasks if not task.done()] # any-ok: untyped task - for task in pending_tasks: # any-ok: untyped task - task.cancel() # any-ok: untyped task - for task in pending_tasks: # any-ok: untyped task + pending_tasks = [task for task in tasks if not task.done()] + for task in pending_tasks: + task.cancel() + for task in pending_tasks: try: - await task # any-ok: untyped request + await task except (asyncio.CancelledError, Exception): # noqa: BLE001 pass @@ -1401,24 +1387,22 @@ class ProxyBaseLLMRequestProcessing: user_model=user_model, user_api_key_dict=user_api_key_dict, ) - llm_call_task = asyncio.create_task(llm_call) # any-ok: untyped task - tasks.append(llm_call_task) # any-ok: untyped task + llm_call_task = asyncio.create_task(llm_call) + tasks.append(llm_call_task) llm_responses = asyncio.gather( *tasks ) # run the moderation check in parallel to the actual llm api call try: - if general_settings.get( # any-ok: untyped request - "cancel_on_disconnect", False - ): - responses = await _await_llm_call_cancelling_on_disconnect( # any-ok: untyped request - request, llm_responses # any-ok: untyped task + if general_settings.get("cancel_on_disconnect", False): + responses = await _await_llm_call_cancelling_on_disconnect( + request, llm_responses ) else: - responses = await llm_responses # any-ok: untyped request + responses = await llm_responses finally: - await _cancel_pending_gather_tasks(tasks) # any-ok: untyped task + await _cancel_pending_gather_tasks(tasks) response = responses[1] @@ -2477,18 +2461,16 @@ class ProxyBaseLLMRequestProcessing: recorded_client_disconnect = ( await _record_streaming_client_disconnect_if_needed( request, - request_data, # any-ok: untyped request - client_disconnected, # any-ok: untyped request + request_data, + client_disconnected, ) ) if recorded_client_disconnect: - ProxyLogging._fire_deferred_stream_logging( - request_data # any-ok: untyped request - ) + ProxyLogging._fire_deferred_stream_logging(request_data) - if hasattr(response, "aclose"): # any-ok: untyped request + if hasattr(response, "aclose"): try: - await response.aclose() # any-ok: untyped request + await response.aclose() except BaseException as e: # noqa: BLE001 verbose_proxy_logger.debug( "async_streaming_data_generator: error closing response stream: %s", @@ -2624,8 +2606,8 @@ class ProxyBaseLLMRequestProcessing: finally: await ProxyBaseLLMRequestProcessing._finalize_streaming_generator_cleanup( request=request, - request_data=request_data, # any-ok: untyped request - response=response, # any-ok: untyped request + request_data=request_data, + response=response, stream_completed=stream_completed, client_disconnected=client_disconnected, ) diff --git a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py index 726e71e307c..0e7e67aa37f 100644 --- a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py +++ b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py @@ -25,15 +25,9 @@ async def get_ui_config(): or general_settings.get("auto_redirect_ui_login_to_sso", False) is True ) admin_ui_disabled = os.getenv("DISABLE_ADMIN_UI", "false").lower() == "true" - hide_default_credentials_hint = bool( # any-ok: untyped settings - os.getenv( # any-ok: untyped settings - "LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", "false" - ).lower() - == "true" - or general_settings.get( # any-ok: untyped settings - "hide_default_credentials_hint", False - ) - is True + hide_default_credentials_hint = bool( + os.getenv("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", "false").lower() == "true" + or general_settings.get("hide_default_credentials_hint", False) is True ) sso_configured = _has_user_setup_sso() @@ -48,7 +42,7 @@ async def get_ui_config(): auto_redirect_to_sso=sso_configured and auto_redirect_ui_login_to_sso, admin_ui_disabled=admin_ui_disabled, sso_configured=sso_configured, - hide_default_credentials_hint=hide_default_credentials_hint, # any-ok: untyped settings + hide_default_credentials_hint=hide_default_credentials_hint, is_control_plane=is_control_plane, workers=proxy_config.worker_registry if is_control_plane else [], ) diff --git a/litellm/proxy/google_endpoints/endpoints.py b/litellm/proxy/google_endpoints/endpoints.py index 4234c433f22..6427835c250 100644 --- a/litellm/proxy/google_endpoints/endpoints.py +++ b/litellm/proxy/google_endpoints/endpoints.py @@ -107,7 +107,7 @@ async def google_stream_generate_content( data["stream"] = True # google-genai SDK (?alt=sse) must not receive OpenAI's data: [DONE] terminator. data["_litellm_skip_openai_stream_done"] = True - data["_litellm_raw_sse_stream"] = True # any-ok: untyped request + data["_litellm_raw_sse_stream"] = True processor = ProxyBaseLLMRequestProcessing(data=data) try: diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 5efb5966262..a8afe4efe2a 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -747,12 +747,12 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): # to the model would carry anonymization tokens and the response would echo them. if ( self.should_run_guardrail( - data=data, # any-ok: untyped request - event_type=GuardrailEventHooks.pre_call, # any-ok: untyped request + data=data, + event_type=GuardrailEventHooks.pre_call, ) is not True ): - return data # any-ok: untyped request + return data try: content_safety = data.get("content_safety", None) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 6e567a428e4..d6ecc59f263 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -3239,10 +3239,8 @@ async def _get_model_max_budget_current_spend( f"{VIRTUAL_KEY_SPEND_CACHE_KEY_PREFIX}:" f"{api_key_hash}:{model}:{budget_config.budget_duration}" ) - current_spend: float | None = ( - await user_api_key_cache.async_get_cache( # any-ok: untyped dump - key=virtual_key_model_spend_cache_key, - ) + current_spend: float | None = await user_api_key_cache.async_get_cache( + key=virtual_key_model_spend_cache_key, ) if current_spend is None: model_without_prefix = model.split("/")[-1] if "/" in model else model @@ -3250,13 +3248,11 @@ async def _get_model_max_budget_current_spend( f"{VIRTUAL_KEY_SPEND_CACHE_KEY_PREFIX}:" f"{api_key_hash}:{model_without_prefix}:{budget_config.budget_duration}" ) - current_spend = ( - await user_api_key_cache.async_get_cache( # any-ok: untyped dump - key=virtual_key_model_spend_cache_key, - ) + current_spend = await user_api_key_cache.async_get_cache( + key=virtual_key_model_spend_cache_key, ) try: - return float(current_spend or 0.0) # any-ok: untyped dump + return float(current_spend or 0.0) except (TypeError, ValueError): return 0.0 @@ -3365,27 +3361,17 @@ async def info_key_fn_v2( k_dict = k.model_dump() except Exception: k_dict = k.dict() - k_token_hash = k_dict.pop("token", None) # any-ok: untyped dump + k_token_hash = k_dict.pop("token", None) - model_max_budget = ( - k_dict.get("model_max_budget") or {} # any-ok: untyped dump - ) - budget_table = ( - k_dict.get("litellm_budget_table") or {} # any-ok: untyped dump - ) - if not model_max_budget and isinstance( # any-ok: untyped dump - budget_table, dict # any-ok: untyped dump - ): - model_max_budget = ( - budget_table.get("model_max_budget") or {} # any-ok: untyped dump - ) - if model_max_budget and k_token_hash: # any-ok: untyped dump - k_dict["model_max_budget_usage"] = ( # any-ok: untyped dump - await _build_model_max_budget_usage( # any-ok: untyped dump - api_key_hash=k_token_hash, # any-ok: untyped dump - model_max_budget=model_max_budget, # any-ok: untyped dump - user_api_key_cache=user_api_key_cache, - ) + model_max_budget = k_dict.get("model_max_budget") or {} + budget_table = k_dict.get("litellm_budget_table") or {} + if not model_max_budget and isinstance(budget_table, dict): + model_max_budget = budget_table.get("model_max_budget") or {} + if model_max_budget and k_token_hash: + k_dict["model_max_budget_usage"] = await _build_model_max_budget_usage( + api_key_hash=k_token_hash, + model_max_budget=model_max_budget, + user_api_key_cache=user_api_key_cache, ) filtered_key_info.append(k_dict) @@ -3470,27 +3456,17 @@ async def info_key_fn( except Exception: # if using pydantic v1 key_info = key_info.dict() - key_token_hash = key_info.pop("token") # any-ok: untyped dump + key_token_hash = key_info.pop("token") - model_max_budget = ( - key_info.get("model_max_budget") or {} # any-ok: untyped dump - ) - budget_table = ( - key_info.get("litellm_budget_table") or {} # any-ok: untyped dump - ) - if not model_max_budget and isinstance( # any-ok: untyped dump - budget_table, dict # any-ok: untyped dump - ): - model_max_budget = ( - budget_table.get("model_max_budget") or {} # any-ok: untyped dump - ) - if model_max_budget and key_token_hash: # any-ok: untyped dump - key_info["model_max_budget_usage"] = ( # any-ok: untyped dump - await _build_model_max_budget_usage( # any-ok: untyped dump - api_key_hash=key_token_hash, # any-ok: untyped dump - model_max_budget=model_max_budget, # any-ok: untyped dump - user_api_key_cache=user_api_key_cache, - ) + model_max_budget = key_info.get("model_max_budget") or {} + budget_table = key_info.get("litellm_budget_table") or {} + if not model_max_budget and isinstance(budget_table, dict): + model_max_budget = budget_table.get("model_max_budget") or {} + if model_max_budget and key_token_hash: + key_info["model_max_budget_usage"] = await _build_model_max_budget_usage( + api_key_hash=key_token_hash, + model_max_budget=model_max_budget, + user_api_key_cache=user_api_key_cache, ) # Attach object_permission if object_permission_id is set diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 91a5c109acf..427c87e0f44 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -194,11 +194,7 @@ def _is_valid_cli_sso_user_code(user_code: str | None) -> bool: def _cli_sso_verification_uri_complete_enabled() -> bool: from litellm.proxy.proxy_server import general_settings - return bool( - general_settings.get( # any-ok: operator opt-in read from the untyped general_settings dict - "allow_cli_sso_verification_uri_complete", False - ) - ) + return bool(general_settings.get("allow_cli_sso_verification_uri_complete", False)) def _cli_sso_start_response_body( diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 8de369efbde..7e9d2688894 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -896,7 +896,7 @@ async def proxy_startup_event(app: FastAPI): if transaction_buffer_redis_cache is None: transaction_buffer_redis_cache = ( ProxyStartupEvent._get_transaction_buffer_redis_cache( - general_settings=general_settings # any-ok: untyped stream + general_settings=general_settings ) ) @@ -7082,9 +7082,7 @@ async def async_data_generator( # happened to ship a streaming-iterator override (the default). needs_iterator_wrap = proxy_logging_obj.needs_iterator_wrap() needs_per_chunk_hook = proxy_logging_obj.needs_per_chunk_streaming_hook() - is_raw_sse_stream = bool( - request_data.get("_litellm_raw_sse_stream") # any-ok: untyped stream - ) + is_raw_sse_stream = bool(request_data.get("_litellm_raw_sse_stream")) raw_sse_buffer = "" if needs_iterator_wrap: @@ -7123,26 +7121,26 @@ async def async_data_generator( frame, raw_sse_buffer = _pop_complete_sse_frame(raw_sse_buffer) if frame is None: break - yield frame # any-ok: untyped stream + yield frame if len(raw_sse_buffer) > _MAX_RAW_SSE_BUFFER_CHARS: raise ValueError( "Raw SSE stream exceeded maximum buffered size without a frame delimiter" ) continue if chunk.startswith(("data:", "event:", ":")): - yield ( # any-ok: untyped stream + yield ( chunk if chunk.endswith(_SSE_FRAME_DELIMITERS) else chunk + "\n\n" ) continue - elif isinstance(chunk, str) and is_raw_sse_stream: # any-ok: untyped stream + elif isinstance(chunk, str) and is_raw_sse_stream: raw_sse_buffer += chunk while True: frame, raw_sse_buffer = _pop_complete_sse_frame(raw_sse_buffer) if frame is None: break - yield frame # any-ok: untyped stream + yield frame if len(raw_sse_buffer) > _MAX_RAW_SSE_BUFFER_CHARS: raise ValueError( "Raw SSE stream exceeded maximum buffered size without a frame delimiter" @@ -7165,7 +7163,7 @@ async def async_data_generator( ProxyLogging._fire_deferred_stream_logging(request_data) if raw_sse_buffer: - yield ( # any-ok: untyped stream + yield ( raw_sse_buffer if raw_sse_buffer.endswith(_SSE_FRAME_DELIMITERS) else raw_sse_buffer + "\n\n" @@ -7231,8 +7229,8 @@ async def async_data_generator( await ProxyBaseLLMRequestProcessing._finalize_streaming_generator_cleanup( request=request, - request_data=request_data, # any-ok: untyped stream - response=response, # any-ok: untyped stream + request_data=request_data, + response=response, stream_completed=stream_completed, client_disconnected=client_disconnected, ) @@ -7353,10 +7351,8 @@ class ProxyStartupEvent: from litellm._redis import _redis_kwargs_from_environment from litellm.secret_managers.main import str_to_bool - _use_redis_transaction_buffer: bool | str | None = ( - general_settings.get( # any-ok: untyped stream - "use_redis_transaction_buffer", False - ) + _use_redis_transaction_buffer: bool | str | None = general_settings.get( + "use_redis_transaction_buffer", False ) if isinstance(_use_redis_transaction_buffer, str): _use_redis_transaction_buffer = str_to_bool(_use_redis_transaction_buffer) @@ -7364,14 +7360,11 @@ class ProxyStartupEvent: if not _use_redis_transaction_buffer: return None - redis_env_kwargs = _redis_kwargs_from_environment() # any-ok: untyped stream - if ( - "host" not in redis_env_kwargs # any-ok: untyped stream - and "url" not in redis_env_kwargs # any-ok: untyped stream - ): + redis_env_kwargs = _redis_kwargs_from_environment() + if "host" not in redis_env_kwargs and "url" not in redis_env_kwargs: return None - return RedisCache(**redis_env_kwargs) # any-ok: untyped stream + return RedisCache(**redis_env_kwargs) @classmethod async def _initialize_semantic_tool_filter( diff --git a/litellm/router_utils/fallback_event_handlers.py b/litellm/router_utils/fallback_event_handlers.py index bc01a894e1d..eb756e3cf8b 100644 --- a/litellm/router_utils/fallback_event_handlers.py +++ b/litellm/router_utils/fallback_event_handlers.py @@ -244,16 +244,12 @@ def _check_non_standard_fallback_format(fallbacks: Optional[List[Any]]) -> bool: if all(isinstance(item, str) for item in fallbacks): return True elif all(isinstance(item, dict) for item in fallbacks): - for item in fallbacks: # any-ok: untyped config - for ( - key - ) in ( - LiteLLMParamsTypedDict.__annotations__.keys() # any-ok: untyped config - ): - if key in item: # any-ok: untyped config + for item in fallbacks: + for key in LiteLLMParamsTypedDict.__annotations__.keys(): + if key in item: # If the value is a list, it's likely a standard fallback model group mapping # (e.g. {"model": ["backup"]}) rather than a parameter override. - if not isinstance(item[key], list): # any-ok: untyped config + if not isinstance(item[key], list): return True return False diff --git a/litellm/secret_managers/aws_secret_manager_v2.py b/litellm/secret_managers/aws_secret_manager_v2.py index 299217f14b2..ef3c821caf1 100644 --- a/litellm/secret_managers/aws_secret_manager_v2.py +++ b/litellm/secret_managers/aws_secret_manager_v2.py @@ -321,13 +321,13 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): ) try: - response = await async_client.post( # any-ok: untyped httpx + response = await async_client.post( url=endpoint_url, - headers=headers, # any-ok: untyped httpx - data=body.decode("utf-8"), # any-ok: untyped httpx + headers=headers, + data=body.decode("utf-8"), ) - response.raise_for_status() # any-ok: untyped httpx - create_response = response.json() # any-ok: untyped httpx + response.raise_for_status() + create_response = response.json() except httpx.HTTPStatusError as err: raise ValueError(f"HTTP error occurred: {err.response.text}") except httpx.TimeoutException: @@ -338,7 +338,7 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): await self.async_replicate_secret( secret_name=secret_name, replica_regions=self.replica_regions, - optional_params=optional_params, # any-ok: untyped httpx + optional_params=optional_params, timeout=timeout, ) verbose_logger.debug( @@ -354,7 +354,7 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): str(replication_err), ) - return create_response # any-ok: untyped httpx + return create_response async def async_replicate_secret( self, @@ -392,7 +392,7 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): "AddReplicaRegions": [{"Region": r} for r in replica_regions], } - endpoint_url, headers, body = self._prepare_request( # any-ok: untyped httpx + endpoint_url, headers, body = self._prepare_request( action="ReplicateSecretToRegions", secret_name=secret_name, optional_params=optional_params, @@ -401,7 +401,7 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): async_client = get_async_httpx_client( llm_provider=httpxSpecialProvider.SecretManager, - params={"timeout": timeout}, # any-ok: untyped httpx + params={"timeout": timeout}, ) try: diff --git a/litellm/utils.py b/litellm/utils.py index 30b5691a140..916260cab5a 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -6043,13 +6043,13 @@ def _get_model_info_helper( cache_read_input_token_cost_above_200k_tokens=_model_info.get( "cache_read_input_token_cost_above_200k_tokens", None ), - cache_read_input_token_cost_above_200k_tokens_priority=_model_info.get( # any-ok: untyped cost map + cache_read_input_token_cost_above_200k_tokens_priority=_model_info.get( "cache_read_input_token_cost_above_200k_tokens_priority", None ), cache_read_input_token_cost_above_272k_tokens=_model_info.get( "cache_read_input_token_cost_above_272k_tokens", None ), - cache_read_input_token_cost_above_272k_tokens_priority=_model_info.get( # any-ok: untyped cost map + cache_read_input_token_cost_above_272k_tokens_priority=_model_info.get( "cache_read_input_token_cost_above_272k_tokens_priority", None ), cache_read_input_token_cost_above_512k_tokens=_model_info.get( @@ -6073,13 +6073,13 @@ def _get_model_info_helper( input_cost_per_token_above_200k_tokens=_model_info.get( "input_cost_per_token_above_200k_tokens", None ), - input_cost_per_token_above_200k_tokens_priority=_model_info.get( # any-ok: untyped cost map + input_cost_per_token_above_200k_tokens_priority=_model_info.get( "input_cost_per_token_above_200k_tokens_priority", None ), input_cost_per_token_above_272k_tokens=_model_info.get( "input_cost_per_token_above_272k_tokens", None ), - input_cost_per_token_above_272k_tokens_priority=_model_info.get( # any-ok: untyped cost map + input_cost_per_token_above_272k_tokens_priority=_model_info.get( "input_cost_per_token_above_272k_tokens_priority", None ), input_cost_per_token_above_512k_tokens=_model_info.get( @@ -6137,13 +6137,13 @@ def _get_model_info_helper( output_cost_per_token_above_200k_tokens=_model_info.get( "output_cost_per_token_above_200k_tokens", None ), - output_cost_per_token_above_200k_tokens_priority=_model_info.get( # any-ok: untyped cost map + output_cost_per_token_above_200k_tokens_priority=_model_info.get( "output_cost_per_token_above_200k_tokens_priority", None ), output_cost_per_token_above_272k_tokens=_model_info.get( "output_cost_per_token_above_272k_tokens", None ), - output_cost_per_token_above_272k_tokens_priority=_model_info.get( # any-ok: untyped cost map + output_cost_per_token_above_272k_tokens_priority=_model_info.get( "output_cost_per_token_above_272k_tokens_priority", None ), output_cost_per_token_above_512k_tokens=_model_info.get( diff --git a/mypy-code-budget.json b/mypy-code-budget.json deleted file mode 100644 index 2cae0d661e9..00000000000 --- a/mypy-code-budget.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "import-not-found": { - "baseline": 8, - "slack": 3 - }, - "no-any-return": { - "baseline": 902, - "slack": 10 - }, - "no-untyped-def": { - "baseline": 4888, - "slack": 10 - }, - "valid-type": { - "baseline": 1, - "slack": 3 - } -} diff --git a/pyproject.toml b/pyproject.toml index 8b1386aaf87..8ee2840b573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,7 +148,6 @@ dev = [ "diff-cover==9.7.2", "flake8==7.3.0", "black==26.3.1", - "mypy==1.19.0", "basedpyright==1.39.7", "pytest==9.0.3", "pytest-mock==3.15.1", @@ -261,8 +260,6 @@ source-exclude = [ "litellm/proxy/enterprise", "**/__pycache__", "**/__pycache__/**", - "**/.mypy_cache", - "**/.mypy_cache/**", "**/.pytest_cache", "**/.pytest_cache/**", "**/.ruff_cache", @@ -278,9 +275,6 @@ version_files = [ "pyproject.toml:^version", ] -[tool.mypy] -plugins = "pydantic.mypy" - [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" diff --git a/scripts/budget_ratchet_check.py b/scripts/budget_ratchet_check.py index 6406b0d888e..861d65489e8 100644 --- a/scripts/budget_ratchet_check.py +++ b/scripts/budget_ratchet_check.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Non-gating ratchet guard: budget ceilings may only fall, never rise. -Every `*-budget.json` file (ruff-strict, type-discipline, mypy-code, basedpyright-code, -any-discipline) is a one-way ratchet: each rule's ceiling is `baseline + slack`, and the whole point is +Every `*-budget.json` file (ruff-strict, type-discipline, basedpyright-code) is a +one-way ratchet: each rule's ceiling is `baseline + slack`, and the whole point is to drive that number DOWN over time. This check compares every budget file against its own content at the merge-base with the target branch and fails (exits 1, red) if: @@ -12,12 +12,6 @@ its own content at the merge-base with the target branch and fails (exits 1, red New rules and lowered/equal ceilings are fine. -The any-discipline budget is keyed by file rather than rule: its gate treats an -absent file as ceiling 0 (the file must be Any-free), so an entry vanishing means -that file was cleaned to zero -- a tightening, and exactly the cleanup this -ratchet exists to encourage. Such a budget is therefore exempt from the -dropped-entry rule (a raised ceiling is still caught). - This is deliberately NOT a gating check. It should turn the run red so that a loosening is impossible to miss in review, but it must stay OUT of the branch-protection required-checks list: a justified bump (e.g. banning a new API, @@ -44,17 +38,9 @@ DEFAULT_BASE = "origin/litellm_internal_staging" DEFAULT_BUDGETS: tuple[str, ...] = ( "ruff-strict-budget.json", "type-discipline-budget.json", - "mypy-code-budget.json", "basedpyright-code-budget.json", - "any-discipline-budget.json", ) -# File-keyed budgets whose gate treats an absent entry as ceiling 0 (the file -# must stay clean). Dropping an entry there is a tightening, not the "untracked, -# now unbounded" loosening a vanished rule is for the rule-keyed budgets, so a -# dropped entry must not read as a regression. -ZERO_FLOOR_BUDGETS: frozenset[str] = frozenset({"any-discipline-budget.json"}) - class Regression(NamedTuple): budget: str @@ -112,15 +98,17 @@ def regressions_for(rel: str, base: dict | None, head: dict | None) -> list[Regr base_caps = _caps(base) head_caps = _caps(head) - drop_floors_to_zero = rel in ZERO_FLOOR_BUDGETS - out: list[Regression] = [] - for rule, base_cap in sorted(base_caps.items()): - if rule not in head_caps: - if not drop_floors_to_zero: - out.append(Regression(rel, rule, f"rule dropped (ceiling {base_cap} -> removed)")) - elif head_caps[rule] > base_cap: - out.append(Regression(rel, rule, f"ceiling raised {base_cap} -> {head_caps[rule]}")) - return out + return [ + Regression( + rel, + rule, + f"rule dropped (ceiling {base_cap} -> removed)" + if rule not in head_caps + else f"ceiling raised {base_cap} -> {head_caps[rule]}", + ) + for rule, base_cap in sorted(base_caps.items()) + if rule not in head_caps or head_caps[rule] > base_cap + ] def main() -> int: diff --git a/scripts/check_any_discipline.py b/scripts/check_any_discipline.py deleted file mode 100644 index 5b8c83e63e0..00000000000 --- a/scripts/check_any_discipline.py +++ /dev/null @@ -1,778 +0,0 @@ -#!/usr/bin/env python3 -"""Any-discipline gate: fail when a changed file exceeds its `Any` budget. - -Where ruff, `mypy --strict`, and even basedpyright's `reportAny` stop short, this -catches the case that actually bites: a *union* hiding an `Any`. For example -`re.Match.group()` -> `str | Any`, `json.loads()` -> `Any`, and bare `list`/`dict` --> `list[Any]`/`dict[..., Any]`. Any value whose inferred type *contains* `Any` -(recursively, through unions / generics / tuples) is reported. - -Scope: changed files, per-file budget -------------------------------------- -litellm carries a large amount of pre-existing `Any` (a single legacy file can -have >100 findings). Rather than force every touched line clean (the original -changed-lines rule, which tripped on merely *editing* a legacy `X | Any` line), -this gate grandfathers each file: `any-discipline-budget.json` records every -file's current count of Any-typed values, and a file fails only when its count -exceeds `baseline + slack`, where `slack` is 50% headroom (rounded up). New or -unbudgeted files have baseline 0, so they stay airtight. - -Only *changed* files (vs the merge-base with `--base`) are re-type-checked -- an -unchanged file's count can't move from edits this branch didn't make -- so the -per-PR cost equals re-checking just those files, exactly like the original -changed-lines gate. The whole-tree scan needed to (re)capture the budget -(~2 min, ~3 GB) runs only under `--update`. - -The budget is a one-way ratchet (the same `{baseline, slack}` shape as the -ruff / mypy / basedpyright budgets) guarded by `scripts/budget_ratchet_check.py`: -a file's ceiling may fall but never rise. Drive a file's count down and rerun -`--update` (`make lint-any-budget-update`) to lock in the lower ceiling. - -How it works ------------- -It loads `litellm/mypy.ini` (the same config `make lint-mypy` uses, so findings -match what developers already see), builds the changed files with mypy asking for -its exported expression->type map, and walks each file's AST applying a recursive -"contains Any" predicate -- the test `mypy --disallow-any-expr` uses internally -but applies inconsistently (python/mypy#12856). - -mypy only re-exports types for modules it re-type-checks, so for each target we -invalidate just its cached hash (deps stay warm) to force a fast re-check against -a persisted incremental cache (.mypy_cache_any). - -Rules ------ -Codes share the `LIT***` namespace with `scripts/check_type_discipline.py` (PR -#30500), which owns LIT001/002/003/004/006/007/008. This gate claims the rest: -LIT009 A value expression's inferred type is, or contains, `Any`. Budgeted - per file (a file fails when its count exceeds `baseline + slack`). - Suppress an individual line with `# any-ok: `. -LIT005 An `# any-ok` suppression without a reason (the shared - suppression-needs-a-reason code, same as `# cast-ok` / `# guard-ok`). -LIT000 Setup failure: mypy could not build, or a target file could not be read. - -`Any`s produced purely by an already-reported error, and the special-form / -implementation-artifact internal `Any`s, are ignored. A bound method *reference* -whose signature mentions `Any` is not flagged -- only the value its call produces. - -Usage ------ - # gate mode (CI / pre-push): per-file Any budget on changed files - uv run --no-sync python scripts/check_any_discipline.py --changed --base origin/litellm_internal_staging - - # re-capture the per-file budget across the whole tree (ratchet) - uv run --no-sync python scripts/check_any_discipline.py --update - - # whole-file spot-check (no budget, no line filter), paths relative to repo root - uv run --no-sync python scripts/check_any_discipline.py litellm/budget_manager.py - -Exit code 1 if a file is over budget (or a hard rule trips), 2 on a setup error. -""" - -from __future__ import annotations - -import argparse -import json -import os -import re -import subprocess -import sys -import tokenize -from collections.abc import Callable, Iterable, Sequence -from pathlib import Path -from typing import NamedTuple - -try: - from mypy import build - from mypy.config_parser import parse_config_file - from mypy.find_sources import create_source_list - from mypy.fscache import FileSystemCache - from mypy.modulefinder import BuildSource - from mypy.nodes import AssignmentStmt, Expression, NameExpr, Node, TempNode - from mypy.options import Options - from mypy.types import ( - AnyType, - CallableType, - Instance, - Overloaded, - TupleType, - Type, - TypeOfAny, - UnionType, - get_proper_type, - ) -except ImportError: # pragma: no cover - environment guard - sys.stderr.write( - "check_any_discipline: mypy is not importable in this interpreter.\n" - "Run it through the project environment, e.g.\n" - " uv run --no-sync python scripts/check_any_discipline.py --changed\n" - ) - raise SystemExit(2) - - -REPO_ROOT = Path(__file__).resolve().parent.parent -LITELLM_DIR = REPO_ROOT / "litellm" -MYPY_INI = LITELLM_DIR / "mypy.ini" -CACHE_DIR = REPO_ROOT / ".mypy_cache_any" -PY_TAG = f"{sys.version_info.major}.{sys.version_info.minor}" -DEFAULT_BASE = "origin/litellm_internal_staging" -BUDGET_PATH = REPO_ROOT / "any-discipline-budget.json" - -MIN_REASON_LEN = 3 -ANY_OK_RE = re.compile(r"#\s*any-ok(?::\s*(?P.*))?") -_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") - -# Files allowed to surface `Any` (the typed/untyped boundary). A finding is -# skipped if any fragment below is a substring of the file's posix path. Keep -# this tight -- prefer a line-level `# any-ok: ` over a blanket exemption. -BOUNDARY_PATHS: frozenset[str] = frozenset() - -# `Any` kinds that are not actionable: produced by an already-reported error, or -# an internal placeholder that never corresponds to a concrete runtime value. -# NOTE: `special_form` is deliberately NOT here. In mypy 1.19 the `Any` in -# typeshed unions like `re.Match.group() -> str | Any` is tagged `special_form`, -# and that union is the headline case this gate exists to catch. -_HARMLESS_ANY = frozenset( - kind - for kind in ( - TypeOfAny.from_error, - getattr(TypeOfAny, "implementation_artifact", None), - ) - if kind is not None -) - -# AST attributes that point OUTSIDE the syntactic subtree (a RefExpr's resolved -# definition, a node's TypeInfo). Skipping exactly these two makes a generic -# child-walk equivalent to mypy's TraverserVisitor -- validated to the node -# against ExtendedTraverserVisitor across the full grammar (see commit notes). -_NON_SYNTACTIC_ATTRS = frozenset({"node", "info"}) - -# Awaitable / coroutine / generator instances carry synthetic `Any` in their -# send (and, for coroutines, yield) protocol slots: `async def f() -> float` -# produces `Coroutine[Any, Any, float]`, so the bare call expression `f()` would -# be flagged even though the awaited value is a clean `float`. Only the args that -# hold a value the caller observes (the awaited result, the yielded item) are -# meaningful; a real `Any` there -- e.g. a coroutine that returns `Any` -- is -# still caught because that index is still checked. -_SYNTHETIC_SEND_YIELD_VALUE_ARGS: dict[str, tuple[int, ...]] = { - "typing.Coroutine": (2,), - "typing.Generator": (0, 2), - "typing.AsyncGenerator": (0,), -} - - -class Violation(NamedTuple): - path: Path - line: int - col: int - code: str - message: str - - def render(self) -> str: - return f"{self.path}:{self.line}:{self.col}: {self.code} {self.message}" - - -# --------------------------------------------------------------------------- # -# The "contains Any" predicate -# --------------------------------------------------------------------------- # - - -# Recursive type aliases (e.g. a JSON-like `T = Union[..., list[T], dict[str, T]]`) -# make `get_proper_type` yield a fresh object at every unfold, so an id()-based -# cycle guard never trips and a naive recursion overflows the stack. We walk -# iteratively and cap the depth: a real `Any` lives at shallow depth in the -# alias's definition, so a deep alias that has not produced one by `_MAX_DEPTH` -# never will. (The changed-lines gate never hit this; a whole-tree scan does.) -_MAX_DEPTH = 100 - - -def contains_any(t: Type) -> bool: - """True if a *value* of type ``t`` carries `Any` anywhere meaningful.""" - seen: set[int] = set() - stack: list[tuple[Type, int]] = [(t, 0)] - while stack: - cur, depth = stack.pop() - if depth > _MAX_DEPTH: - continue - p = get_proper_type(cur) - if id(p) in seen: - continue - seen.add(id(p)) - - # A function/method *reference* whose signature mentions Any is not itself - # an unsafe value -- only its eventual call result is. Don't recurse in. - if isinstance(p, (CallableType, Overloaded)): - continue - if isinstance(p, AnyType): - if p.type_of_any not in _HARMLESS_ANY: - return True - continue - if isinstance(p, UnionType): - stack.extend((item, depth + 1) for item in p.items) - elif isinstance(p, Instance): - value_arg_indices = _SYNTHETIC_SEND_YIELD_VALUE_ARGS.get(p.type.fullname) - if value_arg_indices is None: - stack.extend((arg, depth + 1) for arg in p.args) - else: - stack.extend( - (p.args[index], depth + 1) - for index in value_arg_indices - if index < len(p.args) - ) - elif isinstance(p, TupleType): - stack.extend((item, depth + 1) for item in p.items) - return False - - -# --------------------------------------------------------------------------- # -# Generic, leak-free AST walk (works under a mypyc-compiled mypy, which forbids -# subclassing TraverserVisitor) -# --------------------------------------------------------------------------- # - - -def _walk_file(tree: Node) -> tuple[list[Expression], set[int]]: - """Return (every Expression in `tree`, ids of simple assignment-target names). - - The walk follows only syntactic children (every attribute except the two - non-syntactic back-references), so it never escapes the module. Simple - ``x = `` name targets are collected separately so we don't double-report - the assigned name as an echo of an Any rvalue. - """ - exprs: list[Expression] = [] - skip_lvalues: set[int] = set() - stack: list[object] = [tree] - seen: set[int] = set() - while stack: - n = stack.pop() - if isinstance(n, Node): - if id(n) in seen: - continue - seen.add(id(n)) - if isinstance(n, Expression): - exprs.append(n) - if isinstance(n, AssignmentStmt): - for lvalue in n.lvalues: - if isinstance(lvalue, NameExpr): - skip_lvalues.add(id(lvalue)) - for name in dir(n): - if name.startswith("__") or name in _NON_SYNTACTIC_ATTRS: - continue - try: - val = getattr(n, name) - except Exception: - continue - if callable(val): - continue - if isinstance(val, (Node, list, tuple)): - stack.append(val) - elif isinstance(n, (list, tuple)): - stack.extend(n) - return exprs, skip_lvalues - - -def find_any_in_tree(tree: Node, idmap: dict[int, Type]) -> list[tuple[int, int, str]]: - exprs, skip_lvalues = _walk_file(tree) - findings: list[tuple[int, int, str]] = [] - for expr in exprs: - # A TempNode is mypy's synthetic placeholder for a position with no real - # expression -- e.g. the rvalue of an annotation-only `field: T` in a - # TypedDict / class body, whose `special_form` `Any` is not a value the - # author wrote. It never corresponds to a runtime value, so skip it. - if id(expr) in skip_lvalues or isinstance(expr, TempNode): - continue - t = idmap.get(id(expr)) - if t is not None and contains_any(t): - findings.append((expr.line, expr.column, str(get_proper_type(t)))) - - out: list[tuple[int, int, str]] = [] - seen_pos: set[tuple[int, int]] = set() - for line, col, typ in sorted(findings): - if line < 1 or (line, col) in seen_pos: - continue - seen_pos.add((line, col)) - out.append((line, col, typ)) - return out - - -# --------------------------------------------------------------------------- # -# Comment scanning (LIT005 + any-ok suppression) -# --------------------------------------------------------------------------- # - - -def _reason_ok(reason: str | None) -> bool: - return reason is not None and len(reason.strip()) >= MIN_REASON_LEN - - -def scan_any_ok( - path: Path, source: str -) -> tuple[frozenset[int], tuple[Violation, ...]]: - """Return (lines with a valid any-ok suppression, LIT005 violations).""" - try: - tokens = tokenize.generate_tokens( - iter(source.splitlines(keepends=True)).__next__ - ) - comments = tuple( - (t.start[0], t.string) for t in tokens if t.type == tokenize.COMMENT - ) - except tokenize.TokenError: - return frozenset(), () - - ok_lines: set[int] = set() - violations: list[Violation] = [] - for line, text in comments: - m = ANY_OK_RE.search(text) - if m is None: - continue - if _reason_ok(m.group("reason")): - ok_lines.add(line) - else: - violations.append( - Violation( - path, - line, - 0, - "LIT005", - "any-ok requires a reason: `# any-ok: `", - ) - ) - return frozenset(ok_lines), tuple(violations) - - -# --------------------------------------------------------------------------- # -# mypy build (parity with `make lint-mypy`) + forced target re-check -# --------------------------------------------------------------------------- # - - -def _build_options() -> Options: - opts = Options() - if MYPY_INI.exists(): - parse_config_file(opts, lambda: None, str(MYPY_INI), sys.stdout, sys.stderr) - opts.export_types = True - opts.preserve_asts = True - opts.incremental = True - opts.cache_dir = str(CACHE_DIR) - opts.show_traceback = False - return opts - - -def _meta_path(module: str) -> Path: - return CACHE_DIR / PY_TAG / (module.replace(".", os.sep) + ".meta.json") - - -def _force_recheck(sources: Sequence[BuildSource]) -> None: - """Invalidate each target's cached entry so mypy re-type-checks (and thus - re-exports types + preserves the AST for) exactly these modules, while their - dependencies stay warm. A missing entry is a cold build for that module. - - mypy trusts a cache entry whenever the source mtime matches the cached one - (it never re-hashes on that fast path), so we must break BOTH: zero the - cached mtime to force a re-hash, and corrupt the cached hash so the re-hash - mismatches and the module is treated as changed.""" - for src in sources: - if not src.module: - continue - meta = _meta_path(src.module) - if not meta.exists(): - continue - try: - data = json.loads(meta.read_text()) - data["hash"] = "0" * 40 - data["mtime"] = 0 - meta.write_text(json.dumps(data)) - except (OSError, ValueError): - continue - - -def check_files(rel_paths: Sequence[str]) -> tuple[Violation, ...]: - """`rel_paths` are relative to the litellm package dir (the build cwd).""" - prev_cwd = Path.cwd() - os.chdir(LITELLM_DIR) - try: - opts = _build_options() - fscache = FileSystemCache() - sources = create_source_list(list(rel_paths), opts, fscache) - _force_recheck(sources) - try: - res = build.build(sources, options=opts, fscache=fscache) - except build.CompileError as exc: - joined = "; ".join(exc.messages[:3]) or "blocking error" - return ( - Violation( - Path(rel_paths[0]), - 0, - 0, - "LIT000", - f"mypy could not build: {joined}", - ), - ) - idmap = {id(expr): t for expr, t in res.types.items()} - # Resolve trees to absolute source paths while cwd is the build dir, since - # mypy stores the paths it was given (relative to this cwd). - trees: dict[str, Node] = {} - for state in res.graph.values(): - if state.path and state.tree is not None: - trees[os.path.realpath(state.path)] = state.tree - finally: - os.chdir(prev_cwd) - - out: list[Violation] = [] - for rel in rel_paths: - abs_path = (LITELLM_DIR / rel).resolve() - report_path = abs_path.relative_to(REPO_ROOT) - if _is_boundary(report_path): - continue - try: - source = abs_path.read_text(encoding="utf-8") - except (OSError, UnicodeDecodeError) as exc: - out.append( - Violation(report_path, 0, 0, "LIT000", f"could not read file: {exc}") - ) - continue - - ok_lines, ok_violations = scan_any_ok(report_path, source) - out.extend(ok_violations) - tree = trees.get(os.path.realpath(abs_path)) - if tree is None: - continue - for line, col, typ in find_any_in_tree(tree, idmap): - if line in ok_lines: - continue - out.append( - Violation( - report_path, - line, - col, - "LIT009", - f"value type contains Any -> {typ}", - ) - ) - return tuple(out) - - -# --------------------------------------------------------------------------- # -# File selection (changed-only, changed-lines) + driver -# --------------------------------------------------------------------------- # - - -class _AllLines: - """Sentinel: a wholly new / untracked file -- every line is in scope. - - A distinct object, not None, so that `line_map.get(path)` returning None for - a path absent from the map is never mistaken for "whole file in scope".""" - - -# A changed file's in-scope lines: a specific set, or every line. -LineScope = set[int] | _AllLines -ALL_LINES = _AllLines() - - -def _is_boundary(path: Path) -> bool: - posix = path.as_posix() - return any(frag in posix for frag in BOUNDARY_PATHS) - - -def _git(*args: str) -> list[str]: - result = subprocess.run( - ["git", "-C", str(REPO_ROOT), *args], - capture_output=True, - text=True, - check=True, - ) - return result.stdout.splitlines() - - -def _parse_added_lines(diff_text: str) -> dict[str, set[int]]: - """Map repo-relative path -> set of new-file line numbers the diff adds/edits.""" - changed: dict[str, set[int]] = {} - path: str | None = None - for line in diff_text.splitlines(): - if line.startswith("+++ b/"): - path = line[6:] - elif path and (m := _HUNK_RE.match(line)): - start = int(m.group(1)) - count = int(m.group(2)) if m.group(2) is not None else 1 - if count: - changed.setdefault(path, set()).update(range(start, start + count)) - return changed - - -def changed_line_map(base: str) -> dict[str, LineScope] | None: - """Repo-relative `.py` path under litellm/ -> changed line numbers (or - ALL_LINES for untracked files). Compares the working tree to the merge-base - with `base`, so it covers committed-on-branch + unstaged edits. None if git - is unavailable / not a repo.""" - try: - merge_base = _git("merge-base", base, "HEAD") - point = merge_base[0].strip() if merge_base else base - diff = "\n".join( - _git( - "diff", - "--unified=0", - "--no-color", - "--diff-filter=d", - point, - "--", - "litellm", - ) - ) - untracked = _git("ls-files", "--others", "--exclude-standard", "--", "litellm") - except (subprocess.CalledProcessError, FileNotFoundError): - return None - - out: dict[str, LineScope] = {} - for name, lines in _parse_added_lines(diff).items(): - if name.endswith(".py") and (REPO_ROOT / name).exists(): - out[name] = lines - for name in untracked: - if name.endswith(".py") and (REPO_ROOT / name).exists(): - out[name] = ALL_LINES - return out - - -def _to_litellm_relative(paths: Iterable[Path]) -> list[str]: - rels: list[str] = [] - for p in sorted(paths): - try: - rels.append(p.resolve().relative_to(LITELLM_DIR).as_posix()) - except ValueError: - continue - return rels - - -def _in_scope(v: Violation, line_map: dict[str, LineScope] | None) -> bool: - """A finding survives if line filtering is off (explicit paths), it's a build - error, or its line is one the diff added/edited.""" - if line_map is None or v.code == "LIT000": - return True - lines = line_map.get(v.path.as_posix()) - return lines is ALL_LINES or (isinstance(lines, set) and v.line in lines) - - -# --------------------------------------------------------------------------- # -# Per-file Any budget (one-way ratchet, 50% headroom; ratchet-checked) -# --------------------------------------------------------------------------- # - - -def _slack_for(baseline: int) -> int: - """50% headroom, rounded up so even a 1-Any file gets a little room.""" - return (baseline + 1) // 2 - - -def _ceiling(spec: dict[str, int]) -> int: - """A file's ceiling: ``baseline + slack`` (0 for an absent/empty entry).""" - return int(spec.get("baseline", 0)) + int(spec.get("slack", 0)) - - -def load_budget() -> dict[str, dict[str, int]]: - """Read ``any-discipline-budget.json`` ({path: {baseline, slack}}); {} if absent.""" - if not BUDGET_PATH.exists(): - return {} - try: - data = json.loads(BUDGET_PATH.read_text()) - except (OSError, ValueError): - return {} - return data if isinstance(data, dict) else {} - - -def save_budget(counts: dict[str, int]) -> None: - """Write a fresh budget from per-file counts, with 50% headroom each. - - Files with zero Any are omitted: an absent entry means baseline 0, so a - file's first Any always trips the gate until it is deliberately baselined.""" - budget = { - path: {"baseline": n, "slack": _slack_for(n)} - for path, n in counts.items() - if n > 0 - } - BUDGET_PATH.write_text(json.dumps(budget, indent=2, sort_keys=True) + "\n") - - -def lit009_counts(violations: Iterable[Violation]) -> dict[str, int]: - """Count LIT009 (Any-typed value) findings per repo-relative file path.""" - counts: dict[str, int] = {} - for v in violations: - if v.code == "LIT009": - key = v.path.as_posix() - counts[key] = counts.get(key, 0) + 1 - return counts - - -def all_litellm_py_files() -> list[str] | None: - """Every tracked ``.py`` under litellm/, as litellm-package-relative paths; - None if git is unavailable / not a repo (mirrors ``changed_line_map``).""" - try: - tracked = _git("ls-files", "--", "litellm") - except (subprocess.CalledProcessError, FileNotFoundError): - return None - return _to_litellm_relative( - REPO_ROOT / name for name in tracked if name.endswith(".py") - ) - - -def update_budget( - list_files: Callable[[], list[str] | None] = all_litellm_py_files, -) -> int: - """Whole-tree scan: recapture every file's Any count into the budget.""" - rel_paths = list_files() - if rel_paths is None: - print( - "check_any_discipline: not a git repository; cannot capture the budget", - file=sys.stderr, - ) - return 2 - if not rel_paths: - print("check_any_discipline: no litellm/*.py files found", file=sys.stderr) - return 2 - violations = check_files(rel_paths) - build_errors = [v for v in violations if v.code == "LIT000"] - if build_errors: - for v in build_errors: - print(v.render(), file=sys.stderr) - print( - "FAIL: mypy could not build the tree; budget left unchanged.", - file=sys.stderr, - ) - return 2 - counts = lit009_counts(violations) - save_budget(counts) - print( - f"Wrote {BUDGET_PATH.name}: " - f"{sum(1 for n in counts.values() if n > 0)} file(s), " - f"{sum(counts.values())} Any-typed value(s) baselined (50% headroom each)." - ) - return 0 - - -def _report_over_budget( - path: str, - count: int, - spec: dict[str, int] | None, - lit009: list[Violation], - line_map: dict[str, LineScope], -) -> None: - """Print one over-budget file plus the Any findings on its changed lines.""" - ceiling = _ceiling(spec or {}) - if spec: - why = f"baseline {spec['baseline']} + 50% slack {spec['slack']} = ceiling {ceiling}" - else: - why = "no budget entry -> baseline 0 (a new/unbudgeted file must be Any-free)" - print(f"{path}: {count} Any-typed value(s) total, over budget ({why})") - # Surface the findings on changed lines first: the ones this branch most - # likely just added, and the cheapest path back under the ceiling. - scope = line_map.get(path) - for v in sorted(lit009): - if scope is ALL_LINES or (isinstance(scope, set) and v.line in scope): - print(f" changed-line Any {v.line}:{v.col} {v.message}") - - -def run_gate(base: str) -> int: - """Gate changed files under litellm/ against the committed per-file budget.""" - line_map = changed_line_map(base) - if line_map is None: - print( - "check_any_discipline: not a git repository; nothing to check", - file=sys.stderr, - ) - return 0 - rel_paths = _to_litellm_relative((REPO_ROOT / name).resolve() for name in line_map) - if not rel_paths: - print("OK: no changed Python files under litellm/ to check") - return 0 - - violations = check_files(rel_paths) - budget = load_budget() - - # Hard rules, independent of the budget: a build/read failure (always), and a - # reasonless `# any-ok` on a line this branch touched. - hard = sorted( - v - for v in violations - if v.code == "LIT000" or (v.code == "LIT005" and _in_scope(v, line_map)) - ) - - # Per-file Any budget: a changed file fails when its total Any count exceeds - # its ceiling. Unchanged files keep their committed baseline (never re-scanned). - counts = lit009_counts(violations) - lit009_by_file: dict[str, list[Violation]] = {} - for v in violations: - if v.code == "LIT009": - lit009_by_file.setdefault(v.path.as_posix(), []).append(v) - over_budget = [ - (path, count) - for path, count in sorted(counts.items()) - if count > _ceiling(budget.get(path, {})) - ] - - if not hard and not over_budget: - print( - f"OK: {len(rel_paths)} changed file(s) under litellm/ are within their Any budget" - ) - return 0 - - for v in hard: - print(v.render()) - for path, count in over_budget: - _report_over_budget( - path, count, budget.get(path), lit009_by_file.get(path, []), line_map - ) - - print( - f"\nFAIL: {len(hard)} hard violation(s), {len(over_budget)} file(s) over their Any budget.\n" - "Give the new values concrete types (validate untyped input with Pydantic) to get back\n" - "under the file's ceiling, or annotate a genuine boundary line `# any-ok: `.\n" - "Re-baseline with `make lint-any-budget-update` only to lock in a reduction.", - file=sys.stderr, - ) - return 1 - - -def spot_check(rel_paths: Sequence[str]) -> int: - """Explicit-paths mode: report every finding in the files (no budget).""" - violations = sorted(check_files(rel_paths)) - for v in violations: - print(v.render()) - if violations: - print(f"\nFAIL: {len(violations)} Any-discipline finding(s).", file=sys.stderr) - return 1 - print(f"OK: {len(rel_paths)} file(s) have no Any-typed values") - return 0 - - -def main(argv: Sequence[str]) -> int: - parser = argparse.ArgumentParser( - description="Any-discipline gate (changed files, per-file Any budget)." - ) - parser.add_argument( - "paths", - nargs="*", - help="explicit files (repo-root relative); whole-file spot-check, no budget", - ) - parser.add_argument( - "--changed", - action="store_true", - help="gate changed files under litellm/ vs --base against the per-file budget", - ) - parser.add_argument( - "--update", - action="store_true", - help="recapture the whole-tree per-file budget (any-discipline-budget.json)", - ) - parser.add_argument("--base", default=os.environ.get("ANY_GATE_BASE", DEFAULT_BASE)) - args = parser.parse_args(list(argv)) - - if args.update: - return update_budget() - if args.changed: - return run_gate(args.base) - if args.paths: - rel_paths = _to_litellm_relative((REPO_ROOT / p).resolve() for p in args.paths) - if not rel_paths: - print("check_any_discipline: no litellm/*.py paths given", file=sys.stderr) - return 2 - return spot_check(rel_paths) - parser.error("pass --changed, --update, or explicit file paths") - return 2 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/check_type_discipline.py b/scripts/check_type_discipline.py index d83a1a7512f..6e152541863 100644 --- a/scripts/check_type_discipline.py +++ b/scripts/check_type_discipline.py @@ -25,10 +25,8 @@ LIT003 noqa suppression without rule codes or without a reason. Required shape: `# noqa: TID251 # ` LIT004 type/pyright/mypy ignore without bracketed codes or without a reason. Required shape: `# pyright: ignore[reportArgumentType] # ` -LIT005 A `# mutable-ok` / `# cast-ok` / `# guard-ok` / `# kwargs-ok` / `# any-ok` - suppression without a reason. (`any-ok` belongs to check_any_discipline.py; - it is enumerated here so the reason requirement holds even when only this - stdlib checker runs.) +LIT005 A `# mutable-ok` / `# cast-ok` / `# guard-ok` / `# kwargs-ok` + suppression without a reason. LIT006 `cast(...)` call. typing.cast is an unchecked assertion (the moral equivalent of TypeScript's `as`); it lies to the type checker with zero runtime guarantee. Validate into a concrete frozen type at the boundary instead. @@ -41,9 +39,8 @@ LIT008 `**kwargs` parameter. The keyword contract is erased and everything it c syntax. Declare explicit keyword params, or accept one frozen payload. `*args`, by contrast, is fine when typed (it's just a tuple). Suppress: `# kwargs-ok: `. -LIT000 and LIT009 are the sibling Any gate's (check_any_discipline.py, #30379): a mypy -build/read failure and an Any-typed value. They share this LIT namespace but are emitted -by that checker, not this one. +LIT000 Setup failure: a target file could not be read, or contains a syntax error. + Reported as a violation rather than crashing the run. Usage ----- @@ -105,17 +102,13 @@ MUTABLE_OK_RE = re.compile(r"#\s*mutable-ok(?::\s*(?P.*))?") CAST_OK_RE = re.compile(r"#\s*cast-ok(?::\s*(?P.*))?") GUARD_OK_RE = re.compile(r"#\s*guard-ok(?::\s*(?P.*))?") KWARGS_OK_RE = re.compile(r"#\s*kwargs-ok(?::\s*(?P.*))?") -ANY_OK_RE = re.compile(r"#\s*any-ok(?::\s*(?P.*))?") -# Suppression tokens that must each carry a reason (LIT005). `any-ok` is owned by -# check_any_discipline.py but listed here so the reason requirement is enforced even -# when only this stdlib checker runs. +# Suppression tokens that must each carry a reason (LIT005). OK_SUPPRESSIONS: tuple[tuple[str, re.Pattern[str]], ...] = ( ("mutable-ok", MUTABLE_OK_RE), ("cast-ok", CAST_OK_RE), ("guard-ok", GUARD_OK_RE), ("kwargs-ok", KWARGS_OK_RE), - ("any-ok", ANY_OK_RE), ) diff --git a/scripts/type_check_gate.py b/scripts/type_check_gate.py index 5ff485f0b0f..0f9a44703f9 100644 --- a/scripts/type_check_gate.py +++ b/scripts/type_check_gate.py @@ -1,47 +1,38 @@ #!/usr/bin/env python3 -"""Per-rule count gate for mypy and basedpyright. +"""Per-rule count gate for basedpyright. -Each tool's output is reduced to a count of errors per *rule* (mypy error codes -like ``arg-type``, basedpyright rules like ``reportAny``) and checked against a -committed budget of the form ``{rule: {baseline, slack}}``, the same shape as +basedpyright's ``--outputjson`` is reduced to a count of errors per *rule* +(``reportAny``, ``reportArgumentType``, ...) and checked against a committed +budget of the form ``{rule: {baseline, slack}}``, the same shape as ``ruff-strict-budget.json``. A rule fails when its codebase-wide total exceeds ``baseline + slack``. Counts ignore file, line, and column, so a violation moving anywhere in the tree is invisible; only the per-rule total moves the needle. Unlike ``ruff_strict_gate.py`` this does *not* re-run the tool on the merge base -to compute a delta: a second mypy/basedpyright pass is minutes and gigabytes, -whereas ruff is milliseconds. The committed budget is the baseline instead -- -exactly how the previous per-file gate worked -- so keep it fresh with -``--update`` (ratchet), which re-captures every rule's count from the current -tree while preserving each rule's slack. Tool output is read from stdin, so the -caller decides how to invoke the tool (and from which cwd). +to compute a delta: a second basedpyright pass is minutes and gigabytes, whereas +ruff is milliseconds. The committed budget is the baseline instead -- exactly +how the previous per-file gate worked -- so keep it fresh with ``--update`` +(ratchet), which re-captures every rule's count from the current tree while +preserving each rule's slack. Tool output is read from stdin, so the caller +decides how to invoke basedpyright (and from which cwd). -mypy is parsed from its text output (one error per line, the rule code in a -trailing ``[bracket]``). basedpyright is parsed from ``--outputjson``: its text -diagnostics routinely wrap across lines, leaving the ``(reportRule)`` on a -continuation line away from the ``- error:`` marker, so line parsing -mis-attributes ~60% of errors -- the JSON carries an unambiguous ``rule`` field. +``--outputjson`` is used rather than text diagnostics because the latter wrap +across lines, leaving the ``(reportRule)`` on a continuation line away from the +``- error:`` marker, so line parsing mis-attributes ~60% of errors -- the JSON +carries an unambiguous ``rule`` field. """ import argparse import json -import re import sys from collections import Counter from pathlib import Path -from typing import Iterable, Mapping, NamedTuple +from typing import Mapping, NamedTuple REPO_ROOT = Path(__file__).resolve().parent.parent -# mypy: one error per line, e.g. `path:12: error: msg [arg-type]`. ERROR_LINE -# recognizes the line; MYPY_CODE pulls the trailing [code]. Kept separate so an -# error emitted without a code is still counted (under UNCODED), never dropped. -MYPY_ERROR = re.compile(r"^(?P.+?):\d+: error:") -MYPY_CODE = re.compile(r"\[(?P[a-z][a-z0-9-]*)\]\s*$") - -# Bucket for an error whose rule code we couldn't read (a mypy error with no -# code, or a basedpyright diagnostic with no `rule`). Counted so it's gated. +# Bucket for a basedpyright diagnostic with no `rule`. Counted so it's gated. UNCODED = "" # Ceiling for a rule that shows up at HEAD but isn't in the budget at all -- a @@ -72,20 +63,6 @@ def _to_repo_relative(raw: str) -> str | None: return None -def count_mypy(lines: Iterable[str]) -> dict[str, int]: - """Count in-repo mypy errors per rule code from text output. Errors for - files outside the repo (third-party stubs) are ignored, as before.""" - counts: Counter[str] = Counter() - for raw in lines: - line = raw.rstrip("\n") - match = MYPY_ERROR.match(line) - if match is None or _to_repo_relative(match.group("file")) is None: - continue - code = MYPY_CODE.search(line) - counts[code.group("code") if code else UNCODED] += 1 - return dict(counts) - - def count_basedpyright(payload: str) -> dict[str, int]: """Count in-repo basedpyright errors per rule from `--outputjson`. Warnings and information are ignored; only `severity == "error"` is gated.""" @@ -108,12 +85,6 @@ def count_basedpyright(payload: str) -> dict[str, int]: return dict(counts) -def count_errors(stdin_text: str, tool: str) -> dict[str, int]: - if tool == "basedpyright": - return count_basedpyright(stdin_text) - return count_mypy(stdin_text.splitlines()) - - def evaluate( counts: Mapping[str, int], budget: Mapping[str, Mapping[str, int]] ) -> list[Breach]: @@ -136,13 +107,11 @@ def is_vacuous_run( return not counts and any(spec["baseline"] for spec in budget.values()) -def budget_path(tool: str) -> Path: - return REPO_ROOT / f"{tool}-code-budget.json" +BUDGET_PATH = REPO_ROOT / "basedpyright-code-budget.json" -def cmd_update(tool: str, counts: Mapping[str, int]) -> None: - path = budget_path(tool) - existing = json.loads(path.read_text()) if path.exists() else {} +def cmd_update(counts: Mapping[str, int]) -> None: + existing = json.loads(BUDGET_PATH.read_text()) if BUDGET_PATH.exists() else {} budget = { code: { "baseline": count, @@ -152,18 +121,18 @@ def cmd_update(tool: str, counts: Mapping[str, int]) -> None: } for code, count in sorted(counts.items()) } - path.write_text(json.dumps(budget, indent=2, sort_keys=True) + "\n") + BUDGET_PATH.write_text(json.dumps(budget, indent=2, sort_keys=True) + "\n") print( - f"Re-captured {tool} per-rule budget: {len(budget)} rules, {sum(counts.values())} errors total" + f"Re-captured basedpyright per-rule budget: {len(budget)} rules, {sum(counts.values())} errors total" ) -def cmd_check(tool: str, counts: Mapping[str, int]) -> None: - budget = json.loads(budget_path(tool).read_text()) +def cmd_check(counts: Mapping[str, int]) -> None: + budget = json.loads(BUDGET_PATH.read_text()) if is_vacuous_run(counts, budget): expected = sum(spec["baseline"] for spec in budget.values()) print( - f"FAIL: {tool} produced no errors, but {budget_path(tool).name} expects " + f"FAIL: basedpyright produced no errors, but {BUDGET_PATH.name} expects " f"~{expected}. The type checker almost certainly crashed or emitted " f"nothing; refusing to certify a vacuous run." ) @@ -171,25 +140,24 @@ def cmd_check(tool: str, counts: Mapping[str, int]) -> None: breaches = evaluate(counts, budget) if not breaches: print( - f"OK: every rule is within its {tool} ceiling ({sum(counts.values())} errors total)" + f"OK: every rule is within its basedpyright ceiling ({sum(counts.values())} errors total)" ) return - print(f"FAIL: {tool} errors exceed the per-rule ceiling:") + print("FAIL: basedpyright errors exceed the per-rule ceiling:") for breach in breaches: print(f" {breach.code}: {breach.total} errors over cap {breach.cap}") print( - f"Resolve the new errors, or run 'make lint-{tool}-budget-update' if the ceiling should move." + "Resolve the new errors, or run 'make lint-basedpyright-budget-update' if the ceiling should move." ) raise SystemExit(1) def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--tool", choices=("mypy", "basedpyright"), required=True) parser.add_argument("--update", action="store_true") args = parser.parse_args() - counts = count_errors(sys.stdin.read(), args.tool) - cmd_update(args.tool, counts) if args.update else cmd_check(args.tool, counts) + counts = count_basedpyright(sys.stdin.read()) + cmd_update(counts) if args.update else cmd_check(counts) if __name__ == "__main__": diff --git a/tests/test_litellm/test_budget_ratchet_check.py b/tests/test_litellm/test_budget_ratchet_check.py index 8c4150fd36b..9f19944fdba 100644 --- a/tests/test_litellm/test_budget_ratchet_check.py +++ b/tests/test_litellm/test_budget_ratchet_check.py @@ -48,24 +48,7 @@ def test_dropped_rule_is_a_regression(): def test_new_rule_in_head_is_clean(): - assert ratchet.regressions_for("b.json", {}, {"LIT009": _spec_of(5, 0)}) == [] - - -def test_dropped_file_in_the_any_budget_is_not_a_regression(): - # any-discipline is file-keyed: an absent file means ceiling 0, so cleaning a - # file to zero (which drops its entry on --update) is a tightening, never the - # loosening a dropped rule is for the rule-keyed budgets. - base = {"litellm/x.py": _spec_of(10, 5)} - assert ratchet.regressions_for("any-discipline-budget.json", base, {}) == [] - - -def test_raised_ceiling_in_the_any_budget_is_still_a_regression(): - base = {"litellm/x.py": _spec_of(10, 5)} # ceiling 15 - regs = ratchet.regressions_for( - "any-discipline-budget.json", base, {"litellm/x.py": _spec_of(20, 10)} # ceiling 30 - ) - assert [r.rule for r in regs] == ["litellm/x.py"] - assert "15 -> 30" in regs[0].detail + assert ratchet.regressions_for("b.json", {}, {"new-rule": _spec_of(5, 0)}) == [] def test_deleted_budget_file_is_a_regression(): diff --git a/tests/test_litellm/test_check_any_discipline.py b/tests/test_litellm/test_check_any_discipline.py deleted file mode 100644 index 40385664691..00000000000 --- a/tests/test_litellm/test_check_any_discipline.py +++ /dev/null @@ -1,90 +0,0 @@ -import importlib.util -from pathlib import Path - -_MODULE_PATH = ( - Path(__file__).resolve().parents[2] / "scripts" / "check_any_discipline.py" -) -_spec = importlib.util.spec_from_file_location("check_any_discipline", _MODULE_PATH) -mod = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(mod) - -Violation = mod.Violation - - -def _v(path="litellm/x.py", line=10, code="LIT009"): - return Violation(Path(path), line, 0, code, "Any-typed value") - - -def test_violation_on_a_changed_line_is_in_scope(): - assert mod._in_scope(_v(line=10), {"litellm/x.py": {10, 11}}) is True - - -def test_violation_on_an_unchanged_line_of_a_changed_file_is_out_of_scope(): - assert mod._in_scope(_v(line=99), {"litellm/x.py": {10, 11}}) is False - - -def test_whole_new_file_puts_every_line_in_scope(): - assert mod._in_scope(_v(line=99999), {"litellm/x.py": mod.ALL_LINES}) is True - - -def test_file_absent_from_line_map_is_out_of_scope(): - # Regression: ALL_LINES is a distinct sentinel, so a path missing from the map - # (line_map.get -> None) is NOT mistaken for "whole file in scope". - assert mod._in_scope(_v(path="litellm/other.py"), {"litellm/x.py": {1}}) is False - - -def test_no_line_map_means_no_line_filtering(): - assert mod._in_scope(_v(line=12345), None) is True - - -def test_build_error_is_always_in_scope(): - assert mod._in_scope(_v(code="LIT000", line=1), {"litellm/x.py": {2}}) is True - - -# --- per-file Any budget ------------------------------------------------------ - - -def test_slack_is_50_percent_rounded_up(): - assert mod._slack_for(0) == 0 - assert mod._slack_for(1) == 1 # ceil(0.5): even a 1-Any file gets a little room - assert mod._slack_for(3) == 2 # ceil(1.5) - assert mod._slack_for(20) == 10 - assert mod._slack_for(5145) == 2573 - - -def test_ceiling_is_baseline_plus_slack(): - assert mod._ceiling({"baseline": 20, "slack": 10}) == 30 - assert mod._ceiling({}) == 0 # an absent/empty entry means a zero ceiling - - -def test_lit009_counts_groups_by_file_and_ignores_other_codes(): - violations = [ - _v(path="litellm/a.py", line=1, code="LIT009"), - _v(path="litellm/a.py", line=2, code="LIT009"), - _v(path="litellm/a.py", line=3, code="LIT005"), # suppression hygiene, not an Any - _v(path="litellm/b.py", line=1, code="LIT009"), - _v(path="litellm/c.py", line=0, code="LIT000"), # build error, not an Any - ] - assert mod.lit009_counts(violations) == {"litellm/a.py": 2, "litellm/b.py": 1} - - -def test_save_budget_omits_zero_count_files_and_round_trips(monkeypatch, tmp_path): - monkeypatch.setattr(mod, "BUDGET_PATH", tmp_path / "any-discipline-budget.json") - mod.save_budget({"litellm/a.py": 20, "litellm/b.py": 0, "litellm/c.py": 1}) - loaded = mod.load_budget() - assert loaded == { - "litellm/a.py": {"baseline": 20, "slack": 10}, - "litellm/c.py": {"baseline": 1, "slack": 1}, - } - assert "litellm/b.py" not in loaded # zero-Any files are never baselined - - -def test_load_budget_missing_file_is_empty(monkeypatch, tmp_path): - monkeypatch.setattr(mod, "BUDGET_PATH", tmp_path / "nope.json") - assert mod.load_budget() == {} - - -def test_update_budget_reports_setup_error_when_git_is_unavailable(): - # all_litellm_py_files returns None when git can't list files; --update must - # surface a clean setup error (exit 2), not crash with a raw traceback. - assert mod.update_budget(list_files=lambda: None) == 2 diff --git a/tests/test_litellm/test_type_check_gate.py b/tests/test_litellm/test_type_check_gate.py index eb01bd3b93e..18374c5db4b 100644 --- a/tests/test_litellm/test_type_check_gate.py +++ b/tests/test_litellm/test_type_check_gate.py @@ -10,22 +10,6 @@ _spec.loader.exec_module(gate) ROOT = gate.REPO_ROOT -def test_mypy_counts_per_code_ignoring_lines_notes_and_summary(): - text = "\n".join( - [ - f"{ROOT}/litellm/utils.py:10: error: missing annotation [no-untyped-def]", - f"{ROOT}/litellm/utils.py:9999: error: missing annotation [no-untyped-def]", - f"{ROOT}/litellm/main.py:5: error: Returning Any [no-any-return]", - f"{ROOT}/litellm/main.py:5: note: see here", - "Found 3 errors in 2 files (checked 100 source files)", - ] - ) - assert gate.count_errors(text, "mypy") == { - "no-untyped-def": 2, - "no-any-return": 1, - } - - def _bpr(file, severity, rule): diag = {"file": str(file), "severity": severity, "message": "msg"} if rule is not None: @@ -46,7 +30,7 @@ def test_basedpyright_counts_per_rule_from_json_not_warnings(): ] } ) - assert gate.count_errors(payload, "basedpyright") == { + assert gate.count_basedpyright(payload) == { "reportUnknownVariableType": 2, "reportArgumentType": 1, } @@ -56,17 +40,10 @@ def test_basedpyright_error_without_a_rule_is_bucketed(): payload = json.dumps( {"generalDiagnostics": [_bpr(f"{ROOT}/litellm/x.py", "error", None)]} ) - assert gate.count_errors(payload, "basedpyright") == {gate.UNCODED: 1} - - -def test_mypy_error_without_a_code_is_bucketed_so_it_is_still_gated(): - text = f"{ROOT}/litellm/x.py:1: error: something broke with no code" - assert gate.count_errors(text, "mypy") == {gate.UNCODED: 1} + assert gate.count_basedpyright(payload) == {gate.UNCODED: 1} def test_paths_outside_repo_are_skipped(): - text = "/tmp/elsewhere.py:1: error: missing annotation [no-untyped-def]" - assert gate.count_errors(text, "mypy") == {} payload = json.dumps( { "generalDiagnostics": [ @@ -74,7 +51,7 @@ def test_paths_outside_repo_are_skipped(): ] } ) - assert gate.count_errors(payload, "basedpyright") == {} + assert gate.count_basedpyright(payload) == {} def test_at_or_under_ceiling_passes(): @@ -124,10 +101,10 @@ def test_malformed_basedpyright_json_exits_loudly_not_as_zero_errors(): import pytest with pytest.raises(SystemExit): - gate.count_errors("startup warning\n{not json", "basedpyright") + gate.count_basedpyright("startup warning\n{not json") def test_empty_basedpyright_payload_counts_zero(): # Empty (not malformed) output parses to zero; the vacuous-run guard, not the # parser, is what rejects an empty run. - assert gate.count_errors("", "basedpyright") == {} + assert gate.count_basedpyright("") == {} diff --git a/uv.lock b/uv.lock index bc796e6ed07..5339b56df7f 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-06-11T06:56:06.940919973Z" +exclude-newer = "2026-06-14T15:53:04.946308996Z" exclude-newer-span = "P3D" [manifest] @@ -3231,65 +3231,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/7e/e7394eeb49a41cc514b3eb49020223666cbf40d86f5721c2f07871e6d84a/legacy_cgi-2.6.4-py3-none-any.whl", hash = "sha256:7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd", size = 20035, upload-time = "2025-10-27T05:20:04.289Z" }, ] -[[package]] -name = "librt" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, - { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, - { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, - { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, - { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, - { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, - { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, - { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, - { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, - { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, - { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, - { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, - { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, - { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, - { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, - { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, - { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, - { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, - { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, - { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, - { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, - { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, - { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, - { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, -] - [[package]] name = "litellm" version = "1.89.0" @@ -3441,7 +3382,6 @@ dev = [ { name = "fastapi-offline" }, { name = "flake8" }, { name = "langfuse" }, - { name = "mypy" }, { name = "openapi-core" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, @@ -3609,7 +3549,6 @@ dev = [ { name = "fastapi-offline", specifier = "==1.7.6" }, { name = "flake8", specifier = "==7.3.0" }, { name = "langfuse", specifier = "==2.59.7" }, - { name = "mypy", specifier = "==1.19.0" }, { name = "openapi-core", marker = "python_full_version < '3.14'", specifier = "==0.22.0" }, { name = "opentelemetry-api", specifier = "==1.28.0" }, { name = "opentelemetry-exporter-otlp", specifier = "==1.28.0" }, @@ -4224,46 +4163,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] -[[package]] -name = "mypy" -version = "1.19.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "librt" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" }, - { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" }, - { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" }, - { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" }, - { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" }, - { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" }, - { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, - { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, - { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, - { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, - { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, - { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, - { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, - { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, - { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, - { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, - { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, -] - [[package]] name = "mypy-extensions" version = "1.1.0" From 78a7d0b210c0a8cc4c85d128f8fa32e1fc270bb1 Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Wed, 17 Jun 2026 09:44:19 -0700 Subject: [PATCH 3/4] feat(guardrails): surface OpenAI moderation violation_categories on guardrail traces (#30659) The OpenAI moderation guardrail (and the ai-platform-moderation guardrail built on it) stamped the whole moderation model response into the guardrail trace as guardrail_response. That blob carries the full category_scores map plus categories and category_applied_input_types, which on OTEL backends that index span attributes (for example ELK, which caps indexed attribute values at 1024 chars) overflows the limit and gets truncated, so the violated categories cannot be reliably searched. Extract the flagged category names from the moderation response and pass them through tracing_detail to add_standard_logging_guardrail_information_to_request_data, mirroring the Bedrock hook. Both the legacy and v2 OTEL integrations already read violation_categories off the standard logging guardrail information and emit it as a short, queryable guardrail_violation_categories attribute, so dashboards can group and filter by violation category without parsing the large guardrail_response blob. Resolves LIT-3801 --- .../guardrail_hooks/openai/moderations.py | 34 +++++- .../openai/test_moderations.py | 103 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py b/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py index 7e6f3dac008..b3b8fbdb2a5 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py +++ b/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py @@ -25,7 +25,11 @@ from litellm.llms.custom_httpx.http_handler import ( httpxSpecialProvider, ) from litellm.types.guardrails import GuardrailEventHooks -from litellm.types.utils import GenericGuardrailAPIInputs, GuardrailStatus +from litellm.types.utils import ( + GenericGuardrailAPIInputs, + GuardrailStatus, + GuardrailTracingDetail, +) from .base import OpenAIGuardrailBase @@ -287,6 +291,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): start_time=start_time, end_time=end_time, event_type=event_type, + tracing_detail=self._build_tracing_detail(guardrail_response), ) return response @@ -328,9 +333,36 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): start_time=start_time, end_time=end_time, event_type=event_type, + tracing_detail=self._build_tracing_detail(guardrail_response), ) raise e + @staticmethod + def _build_tracing_detail( + guardrail_response: Union[dict, str, Exception], + ) -> Optional[GuardrailTracingDetail]: + """ + Pull the flagged category names out of the moderation response so trace + backends can index a short, queryable ``guardrail_violation_categories`` + attribute instead of the full ``guardrail_response`` blob, whose + ``category_scores`` map (one float per category) blows past indexed-field + length limits on backends like ELK (1024 chars). + """ + if not isinstance(guardrail_response, dict): + return None + + results = guardrail_response.get("results") or [] + violation_categories = [ + category + for result in results + if isinstance(result, dict) + for category, is_flagged in (result.get("categories") or {}).items() + if is_flagged + ] + if not violation_categories: + return None + return GuardrailTracingDetail(violation_categories=violation_categories) + @staticmethod def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: """ diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py index 16b5cbe8589..fe6cb98d1f5 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py @@ -2,6 +2,7 @@ """ Test OpenAI Moderation Guardrail """ + import os import sys @@ -822,6 +823,108 @@ def test_openai_moderation_process_error_metadata_none_edge_case(): assert "_openai_moderation_response" not in request_data["metadata"] +@pytest.mark.asyncio +async def test_openai_moderation_logs_violation_categories_harmful_content(): + """Flagged content surfaces only the violated category names in + StandardLoggingGuardrailInformation.violation_categories, so OTEL can index + a short ``guardrail_violation_categories`` attribute instead of the full + response blob (LIT-3801).""" + from fastapi import HTTPException + + from litellm.types.utils import GenericGuardrailAPIInputs + + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + guardrail = OpenAIModerationGuardrail(guardrail_name="test-openai-moderation") + + mock_response = OpenAIModerationResponse( + id="modr-violations", + model="omni-moderation-latest", + results=[ + OpenAIModerationResult( + flagged=True, + categories={ + "sexual": False, + "hate": False, + "self-harm": True, + "self-harm/intent": True, + "violence": True, + }, + category_scores={ + "sexual": 0.0001, + "hate": 0.0001, + "self-harm": 0.97, + "self-harm/intent": 0.98, + "violence": 0.35, + }, + category_applied_input_types={}, + ) + ], + ) + + with patch.object(guardrail, "async_make_request", return_value=mock_response): + request_data = {"metadata": {}} + with pytest.raises(HTTPException): + await guardrail.apply_guardrail( + inputs=GenericGuardrailAPIInputs( + structured_messages=[{"role": "user", "content": "harmful"}] + ), + request_data=request_data, + input_type="request", + ) + + info = request_data["metadata"]["standard_logging_guardrail_information"][0] + + # Only the flagged categories, never the unflagged ones or the scores + assert info["violation_categories"] == [ + "self-harm", + "self-harm/intent", + "violence", + ] + + +@pytest.mark.asyncio +async def test_openai_moderation_no_violation_categories_safe_content(): + """Safe content carries no violation_categories key, so the short attribute + is absent rather than empty on allowed requests (LIT-3801).""" + from litellm.types.utils import GenericGuardrailAPIInputs + + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + guardrail = OpenAIModerationGuardrail(guardrail_name="test-openai-moderation") + + mock_response = OpenAIModerationResponse( + id="modr-safe", + model="omni-moderation-latest", + results=[ + OpenAIModerationResult( + flagged=False, + categories={"hate": False, "violence": False}, + category_scores={"hate": 0.001, "violence": 0.002}, + category_applied_input_types={}, + ) + ], + ) + + with patch.object(guardrail, "async_make_request", return_value=mock_response): + request_data = {"metadata": {}} + await guardrail.apply_guardrail( + inputs=GenericGuardrailAPIInputs( + structured_messages=[{"role": "user", "content": "hi"}] + ), + request_data=request_data, + input_type="request", + ) + + info = request_data["metadata"]["standard_logging_guardrail_information"][0] + assert "violation_categories" not in info + + +def test_openai_moderation_build_tracing_detail_non_dict_responses(): + """Non-dict guardrail responses (the "allow" sentinel, a raw Exception) yield + no tracing detail so logging never crashes when no moderation call ran.""" + assert OpenAIModerationGuardrail._build_tracing_detail("allow") is None + assert OpenAIModerationGuardrail._build_tracing_detail(ValueError("boom")) is None + + @pytest.mark.asyncio async def test_openai_moderation_guardrail_streaming_defaults(): """Defaults match the unified dispatcher: sampled in-stream, every 5th chunk.""" From 6c8b60d50d9985e5e56e09e1081a63d445598e17 Mon Sep 17 00:00:00 2001 From: Shivam Rawat Date: Wed, 17 Jun 2026 10:32:52 -0700 Subject: [PATCH 4/4] fix(proxy): resolve list files credentials from team BYOK deployments (#30495) * fix(proxy): resolve list files credentials from team BYOK deployments GET /v1/files without target_model_names now prefers the team's own deployment (model_info.team_id) over shared global provider keys, so JWT team auth lists files against the correct upstream account. Co-authored-by: Cursor * fix(proxy): scope list files credential lookup to team allowlist Remove the unrestricted deployment scan that could leak global provider keys to teams without access, normalize all-proxy-models to the team-scoped model list, and fix TID251 violations by using dict instead of Dict/Any. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .../openai_files_endpoints/common_utils.py | 88 +++++ .../openai_files_endpoints/files_endpoints.py | 25 +- .../test_files_endpoint.py | 326 ++++++++++++++++++ 3 files changed, 436 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 2ba1d937c04..bb3033e2a6c 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -14,6 +14,8 @@ from litellm.types.utils import SpecialEnums if TYPE_CHECKING: from fastapi import Request + from litellm.router import Router + def _is_base64_encoded_unified_file_id(b64_uid: str) -> Union[str, Literal[False]]: # Ensure b64_uid is a string and not a mock object @@ -300,6 +302,92 @@ def get_credentials_for_model( return credentials +def get_team_provider_credentials( + llm_router: Optional["Router"], + team_models: List[str], + custom_llm_provider: str, + team_id: Optional[str] = None, +) -> Optional[dict]: + """ + Resolve upstream credentials for a provider-scoped file operation + (e.g. GET /v1/files), which doesn't pin a model. + + Priority: + 1. The team's own (BYOK) deployment for this provider — a deployment whose + ``model_info.team_id`` matches ``team_id``. This keeps team-scoped listings + on the team's own provider account/key instead of a shared global one. + 2. Fallback: any deployment the team is granted access to for this provider, + expanding wildcard routes and the all-proxy-models sentinel. + + Credential lookup is always scoped to the team's allowlist, so a team can + never resolve a provider key for a deployment it isn't authorized to use. + Returns None when the router is unavailable or no authorized deployment + matches, so the caller can fall back to default credential resolution. + """ + if llm_router is None: + return None + + def _provider_credentials(model_id: str) -> Optional[dict]: + credentials = llm_router.get_deployment_credentials_with_provider( + model_id=model_id + ) + if ( + credentials is not None + and credentials.get("custom_llm_provider") == custom_llm_provider + ): + return credentials + return None + + # 1. Prefer the team's own BYOK deployment, matched by model_info.team_id. + if team_id is not None: + for deployment in llm_router.model_list or []: + model_info = deployment.get("model_info") or {} + if model_info.get("team_id") != team_id: + continue + deployment_id = model_info.get("id") + if deployment_id is None: + continue + credentials = _provider_credentials(deployment_id) + if credentials is not None: + return credentials + + # 2. Fall back to deployments the team is allowed to access. The + # all-proxy-models sentinel isn't expanded by get_complete_model_list, so + # normalize it to an empty allowlist, which defers to the team-scoped + # proxy model list. A team with a restricted allowlist (e.g. anthropic + # only) therefore never resolves another provider's key. + from litellm.proxy._types import SpecialModelNames + from litellm.proxy.auth.model_checks import get_complete_model_list + + grants_all_models = SpecialModelNames.all_proxy_models.value in team_models + effective_team_models = [] if grants_all_models else team_models + + proxy_model_list = llm_router.get_model_names(team_id=team_id) + model_access_groups = llm_router.get_model_access_groups() + models_to_try = list( + dict.fromkeys( + get_complete_model_list( + key_models=[], + team_models=effective_team_models, + proxy_model_list=proxy_model_list, + user_model=None, + infer_model_from_keys=False, + return_wildcard_routes=True, + llm_router=llm_router, + model_access_groups=model_access_groups, + include_model_access_groups=True, + team_id=team_id, + ) + ) + ) + for model_name in models_to_try: + credentials = _provider_credentials(model_name) + if credentials is not None: + return credentials + + return None + + def prepare_data_with_credentials( data: dict, credentials: dict, diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index f43e876d111..d7dab350154 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -43,6 +43,7 @@ from litellm.proxy.openai_files_endpoints.common_utils import ( encode_file_id_with_model, extract_file_creation_params, get_credentials_for_model, + get_team_provider_credentials, handle_model_based_routing, prepare_data_with_credentials, validate_managed_files_requirement, @@ -1351,14 +1352,20 @@ async def list_files( status_code=400, detail="target_model_names on list files must be a list of one model name. Example: ['gpt-4o']", ) - ## Use router to list fine-tuning jobs for that model if llm_router is None: raise HTTPException( status_code=500, detail="LLM Router not initialized. Ensure models added to proxy.", ) - data["model"] = target_model_names_list[0] - response = await llm_router.afile_list( + credentials = get_credentials_for_model( + llm_router=llm_router, + model_id=target_model_names_list[0], + operation_context="file list", + ) + prepare_data_with_credentials(data=data, credentials=credentials) + response = await litellm.afile_list( + custom_llm_provider=credentials["custom_llm_provider"], + purpose=purpose, **data, ) else: @@ -1370,6 +1377,18 @@ async def list_files( or "openai" ) + # No model/target_model_names pinned: resolve upstream credentials from + # the team's deployment for this provider so the call is authenticated + # against the team's own account (e.g. the team's openai deployment). + team_credentials = get_team_provider_credentials( + llm_router=llm_router, + team_models=user_api_key_dict.team_models or [], + custom_llm_provider=custom_llm_provider, + team_id=user_api_key_dict.team_id, + ) + if team_credentials is not None: + prepare_data_with_credentials(data=data, credentials=team_credentials) + response = await litellm.afile_list( custom_llm_provider=custom_llm_provider, purpose=purpose, **data # type: ignore ) diff --git a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py index 103e05bd3af..cdb09215aa0 100644 --- a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py +++ b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py @@ -2188,3 +2188,329 @@ def test_require_managed_files_accepts_repeated_target_model_names_bracket_form( assert response.status_code == 200, response.text assert response.json()["id"] == "litellm_managed_file_repeated" assert received_target_model_names == ["azure-gpt-3-5-turbo", "gpt-3.5-turbo"] + + +def test_list_files_resolves_wildcard_deployment_credentials( + mocker: MockerFixture, monkeypatch +): + """ + GET /v1/files?target_model_names= must resolve the upstream api_key + from the matching (wildcard) deployment. Regression for the path routing + through llm_router.afile_list(model=...), which reached OpenAI without an + api_key and failed with "api_key client option must be set". + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + wildcard_router = Router( + model_list=[ + { + "model_name": "*", + "litellm_params": { + "model": "openai/*", + "api_key": "wildcard-openai-key", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, wildcard_router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", wildcard_router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.PROXY_ADMIN, + user_id="test-user", + ) + + try: + response = client.get( + "/v1/files?target_model_names=gpt-4o", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") == "wildcard-openai-key" + assert captured_kwargs.get("custom_llm_provider") == "openai" + proxy_logging_obj.post_call_failure_hook.assert_not_called() + + +def test_list_files_without_target_model_names_uses_team_openai_deployment( + mocker: MockerFixture, monkeypatch +): + """ + Plain GET /v1/files (no target_model_names) must resolve the upstream openai + api_key from the team's openai deployment instead of falling through to a + keyless OpenAI client. Regression for "api_key client option must be set". + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + wildcard_router = Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": { + "model": "openai/*", + "api_key": "team-openai-key", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, wildcard_router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", wildcard_router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="test-user", + team_id="test-team", + team_models=["openai/*"], + ) + + try: + response = client.get( + "/v1/files", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") == "team-openai-key" + assert captured_kwargs.get("custom_llm_provider") == "openai" + proxy_logging_obj.post_call_failure_hook.assert_not_called() + + +def test_list_files_restricted_team_does_not_leak_global_openai_credentials( + mocker: MockerFixture, monkeypatch +): + """ + A team whose allowlist only grants anthropic must NOT resolve a global + openai deployment's api_key for plain GET /v1/files. Regression for the + last-resort scan that ignored team access control. + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + router = Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": { + "model": "openai/*", + "api_key": "global-openai-key", + }, + }, + { + "model_name": "claude-opus-4-6", + "litellm_params": { + "model": "anthropic/claude-opus-4-6", + "api_key": "anthropic-key", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="test-user", + team_id="anthropic-only-team", + team_models=["claude-opus-4-6"], + ) + + try: + response = client.get( + "/v1/files", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") != "global-openai-key" + + +def test_list_files_prefers_team_byok_over_global_openai_deployment( + mocker: MockerFixture, monkeypatch +): + """ + When a team has its own BYOK openai deployment (model_info.team_id set), plain + GET /v1/files must use the team's key, not a shared/global openai deployment. + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + router = Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": { + "model": "openai/*", + "api_key": "global-openai-key", + }, + }, + { + "model_name": "team-gpt-4o", + "litellm_params": { + "model": "openai/gpt-4o", + "api_key": "team-byok-openai-key", + }, + "model_info": { + "id": "team-byok-deployment-id", + "team_id": "test-team", + "team_public_model_name": "team-gpt-4o", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="test-user", + team_id="test-team", + team_models=["team-gpt-4o"], + ) + + try: + response = client.get( + "/v1/files", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") == "team-byok-openai-key" + assert captured_kwargs.get("custom_llm_provider") == "openai" + proxy_logging_obj.post_call_failure_hook.assert_not_called() + + +def test_list_files_with_all_proxy_models_team_uses_openai_deployment( + mocker: MockerFixture, monkeypatch +): + """ + Teams with all-proxy-models (or empty models) must still resolve openai + credentials for plain GET /v1/files. + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles, SpecialModelNames + + wildcard_router = Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": { + "model": "openai/*", + "api_key": "team-openai-key", + }, + }, + { + "model_name": "claude-opus-4-6", + "litellm_params": { + "model": "anthropic/claude-opus-4-6", + "api_key": "anthropic-key", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, wildcard_router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", wildcard_router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="test-user", + team_id="test-team", + team_models=[SpecialModelNames.all_proxy_models.value], + ) + + try: + response = client.get( + "/v1/files", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") == "team-openai-key" + assert captured_kwargs.get("custom_llm_provider") == "openai" + proxy_logging_obj.post_call_failure_hook.assert_not_called()