From 43cf7e7493b48e8163ae18970be8e6b1bf4de35f Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:20:54 -0700 Subject: [PATCH] fix(ci): stop deferred pydantic builds leaking caller locals and add the missing Lens FK migration (#44981) * fix(ci): stop deferred pydantic builds leaking caller locals and add missing Lens FK migration Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(types): drop narrating comment from caller-locals regression test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy-extras): guard the Lens review FK migration with DO blocks Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(types): run the caller-locals regression in-process Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy-extras): scope the Lens review FK guards to their table Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: mateo Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../migration.sql | 15 +++++++++++++++ litellm/types/llms/base.py | 19 +++++++++++++++++++ tests/unit/types/llms/test_types_llms_base.py | 19 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20261007000000_lens_review_fk_on_update_cascade/migration.sql diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20261007000000_lens_review_fk_on_update_cascade/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20261007000000_lens_review_fk_on_update_cascade/migration.sql new file mode 100644 index 00000000000..7daf8fa9cf6 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20261007000000_lens_review_fk_on_update_cascade/migration.sql @@ -0,0 +1,15 @@ +-- DropForeignKey +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'LiteLLM_LensReview_lens_id_fkey' AND conrelid = '"LiteLLM_LensReview"'::regclass) THEN + ALTER TABLE "LiteLLM_LensReview" DROP CONSTRAINT "LiteLLM_LensReview_lens_id_fkey"; + END IF; +END $$; + +-- AddForeignKey +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'LiteLLM_LensReview_lens_id_fkey' AND conrelid = '"LiteLLM_LensReview"'::regclass) THEN + ALTER TABLE "LiteLLM_LensReview" ADD CONSTRAINT "LiteLLM_LensReview_lens_id_fkey" FOREIGN KEY ("lens_id") REFERENCES "LiteLLM_Lens"("id") ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; diff --git a/litellm/types/llms/base.py b/litellm/types/llms/base.py index 803e7cb44b0..e63e1b040d0 100644 --- a/litellm/types/llms/base.py +++ b/litellm/types/llms/base.py @@ -1,3 +1,4 @@ +from collections.abc import Mapping from typing import TYPE_CHECKING, Any, Final from openai._models import BaseModel as OpenAIObject @@ -13,6 +14,24 @@ class LiteLLMBaseModel(BaseModel): def __init__(self, /, **data: object) -> None: ... + @classmethod + def model_rebuild( + cls, + *, + force: bool = False, + raise_errors: bool = True, + _parent_namespace_depth: int = 2, + _types_namespace: Mapping[str, object] | None = None, + ) -> bool | None: + # Resolve names from the model's own module, never a caller frame: a deferred first-use build + # reads f_locals 5 frames up, and on Python < 3.13 that rewrites the dict the caller's locals() returned + return super().model_rebuild( + force=force, + raise_errors=raise_errors, + _parent_namespace_depth=0, + _types_namespace=_types_namespace, + ) + def model_post_init(self, context: object, /) -> None: # Instances built by a parent's validator or by model_construct skip this class's own # first-use build, leaving a MockValSer that fails when the instance is serialized as Any diff --git a/tests/unit/types/llms/test_types_llms_base.py b/tests/unit/types/llms/test_types_llms_base.py index 32f22e68397..132fb95cad1 100644 --- a/tests/unit/types/llms/test_types_llms_base.py +++ b/tests/unit/types/llms/test_types_llms_base.py @@ -4,6 +4,9 @@ import sys from typing import Final import pytest +from pydantic import ConfigDict + +from litellm.types.llms.base import LiteLLMBaseModel _PROBE: Final = """ from litellm.types.llms.base import LiteLLMBaseModel @@ -71,3 +74,19 @@ print(Holder(item=validated).model_dump_json(serialize_as_any=True), Holder(item def test_deferred_instances_created_without_their_own_init_still_serialize_as_any() -> None: assert _run_probe(_NESTED_PROBE, None) == '{"item":{"value":1}} {"item":{"value":2}}' + + +def test_deferred_first_use_build_leaves_caller_locals_snapshot_untouched() -> None: + class DeferredProbe(LiteLLMBaseModel): + model_config = ConfigDict(defer_build=True) + value: int + + def build(model: type[DeferredProbe]) -> list[str]: + local_vars: Final = locals() + later: Final = 1 + _ = model(value=later) + return sorted(local_vars) + + assert not DeferredProbe.__pydantic_complete__ + assert build(DeferredProbe) == ["model"] + assert DeferredProbe.__pydantic_complete__