mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(proxy): key lazy openapi stubs off registered features, not sys.modules
This commit is contained in:
parent
632a007967
commit
fd751a5023
3 changed files with 100 additions and 12 deletions
|
|
@ -8,8 +8,8 @@ omits each feature's routes until the feature is warmed.
|
|||
|
||||
import asyncio
|
||||
import importlib
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Set as AbstractSet
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
|
|
@ -397,11 +397,27 @@ def _make_warmup_router(app: "FastAPI") -> "APIRouter":
|
|||
return router
|
||||
|
||||
|
||||
def inject_lazy_stubs(schema: dict) -> dict:
|
||||
"""Inject openapi entries for unloaded features. Uses the snapshot file
|
||||
when available (full route info), otherwise falls back to a single
|
||||
placeholder per feature. Any failure logs and returns the schema unchanged
|
||||
so /openapi.json never 500s on a cosmetic injection bug."""
|
||||
def loaded_lazy_modules(app: "FastAPI") -> frozenset[str]:
|
||||
"""The set of lazy feature modules whose routers are actually registered
|
||||
on this app (tracked by _force_load), empty before the middleware ever ran.
|
||||
sys.modules is the wrong signal: boot code imports several feature modules
|
||||
(mcp_management, cloudzero, vantage, config_overrides) without mounting
|
||||
their routers, and their stubs must still be injected."""
|
||||
loaded: Final = getattr(app.state, "lazy_loaded", None)
|
||||
if not isinstance(loaded, set):
|
||||
return frozenset()
|
||||
return frozenset(m for m in loaded if isinstance(m, str))
|
||||
|
||||
|
||||
def inject_lazy_stubs(
|
||||
schema: dict,
|
||||
loaded_modules: AbstractSet[str],
|
||||
features: tuple[LazyFeature, ...] = LAZY_FEATURES,
|
||||
) -> dict:
|
||||
"""Inject openapi entries for features not in loaded_modules. Uses the
|
||||
snapshot file when available (full route info), otherwise falls back to a
|
||||
single placeholder per feature. Any failure logs and returns the schema
|
||||
unchanged so /openapi.json never 500s on a cosmetic injection bug."""
|
||||
try:
|
||||
from litellm.proxy._lazy_openapi_snapshot import load_snapshot
|
||||
|
||||
|
|
@ -409,8 +425,8 @@ def inject_lazy_stubs(schema: dict) -> dict:
|
|||
paths: Final = schema.setdefault("paths", {})
|
||||
schemas: Final = schema.setdefault("components", {}).setdefault("schemas", {})
|
||||
|
||||
for feat in LAZY_FEATURES:
|
||||
if feat.module_path in sys.modules and not feat.persistent_swagger_stub:
|
||||
for feat in features:
|
||||
if feat.module_path in loaded_modules and not feat.persistent_swagger_stub:
|
||||
continue
|
||||
|
||||
fragment = (snapshot or {}).get(feat.name)
|
||||
|
|
|
|||
|
|
@ -1502,9 +1502,9 @@ def get_openapi_schema():
|
|||
openapi_schema = CustomOpenAPISpec.add_llm_api_request_schema_body(openapi_schema)
|
||||
|
||||
# Stub unloaded lazy features so they appear as Swagger sections.
|
||||
from litellm.proxy._lazy_features import inject_lazy_stubs
|
||||
from litellm.proxy._lazy_features import inject_lazy_stubs, loaded_lazy_modules
|
||||
|
||||
openapi_schema = inject_lazy_stubs(openapi_schema)
|
||||
openapi_schema = inject_lazy_stubs(openapi_schema, loaded_lazy_modules(app))
|
||||
openapi_schema = ensure_unique_openapi_operation_ids(openapi_schema)
|
||||
|
||||
# Fix Swagger UI execute path error when server_root_path is set
|
||||
|
|
@ -1534,9 +1534,9 @@ def custom_openapi():
|
|||
openapi_schema = CustomOpenAPISpec.add_llm_api_request_schema_body(openapi_schema)
|
||||
|
||||
# Stub unloaded lazy features so they appear as Swagger sections.
|
||||
from litellm.proxy._lazy_features import inject_lazy_stubs
|
||||
from litellm.proxy._lazy_features import inject_lazy_stubs, loaded_lazy_modules
|
||||
|
||||
openapi_schema = inject_lazy_stubs(openapi_schema)
|
||||
openapi_schema = inject_lazy_stubs(openapi_schema, loaded_lazy_modules(app))
|
||||
openapi_schema = ensure_unique_openapi_operation_ids(openapi_schema)
|
||||
|
||||
# Fix Swagger UI execute path error when server_root_path is set
|
||||
|
|
|
|||
|
|
@ -9152,6 +9152,78 @@ class TestLazyFeatureMiddleware:
|
|||
)
|
||||
|
||||
|
||||
class TestInjectLazyStubs:
|
||||
"""Stub injection keys off the app-tracked loaded set, never sys.modules:
|
||||
proxy boot imports several feature modules (mcp_management, cloudzero,
|
||||
vantage, config_overrides) without mounting their routers, and their
|
||||
/openapi.json entries must survive that (LIT-6275)."""
|
||||
|
||||
def test_imported_but_unregistered_module_still_gets_stub(self):
|
||||
import sys
|
||||
|
||||
from litellm.proxy._lazy_features import LazyFeature, inject_lazy_stubs
|
||||
|
||||
feat = LazyFeature(
|
||||
name="dummy_lazy_test",
|
||||
module_path="json",
|
||||
path_prefixes=("/dummy-lazy-test",),
|
||||
)
|
||||
assert feat.module_path in sys.modules
|
||||
|
||||
schema = inject_lazy_stubs({"paths": {}}, loaded_modules=frozenset(), features=(feat,))
|
||||
assert "/dummy-lazy-test" in schema["paths"]
|
||||
|
||||
def test_registered_module_gets_no_stub(self):
|
||||
from litellm.proxy._lazy_features import LazyFeature, inject_lazy_stubs
|
||||
|
||||
feat = LazyFeature(
|
||||
name="dummy_lazy_test",
|
||||
module_path="json",
|
||||
path_prefixes=("/dummy-lazy-test",),
|
||||
)
|
||||
schema = inject_lazy_stubs({"paths": {}}, loaded_modules=frozenset({"json"}), features=(feat,))
|
||||
assert "/dummy-lazy-test" not in schema["paths"]
|
||||
|
||||
def test_snapshot_fragments_injected_for_boot_imported_features(self):
|
||||
from litellm.proxy._lazy_features import LAZY_FEATURES, inject_lazy_stubs
|
||||
from litellm.proxy._lazy_openapi_snapshot import load_snapshot
|
||||
|
||||
snapshot = load_snapshot()
|
||||
assert snapshot
|
||||
boot_imported = tuple(
|
||||
f for f in LAZY_FEATURES if f.name in ("mcp_management", "cloudzero", "vantage", "config_overrides")
|
||||
)
|
||||
assert len(boot_imported) == 4
|
||||
|
||||
schema = inject_lazy_stubs({"paths": {}}, loaded_modules=frozenset(), features=boot_imported)
|
||||
for feat in boot_imported:
|
||||
missing = [p for p in snapshot[feat.name]["paths"] if p not in schema["paths"]]
|
||||
assert not missing, f"{feat.name} snapshot paths missing from /openapi.json: {missing}"
|
||||
|
||||
def test_persistent_stub_survives_load(self):
|
||||
from litellm.proxy._lazy_features import LazyFeature, inject_lazy_stubs
|
||||
|
||||
feat = LazyFeature(
|
||||
name="dummy_lazy_test",
|
||||
module_path="json",
|
||||
path_prefixes=("/dummy-lazy-test",),
|
||||
persistent_swagger_stub=True,
|
||||
)
|
||||
schema = inject_lazy_stubs({"paths": {}}, loaded_modules=frozenset({"json"}), features=(feat,))
|
||||
assert "/dummy-lazy-test" in schema["paths"]
|
||||
|
||||
def test_loaded_lazy_modules_reads_app_state(self):
|
||||
from fastapi import FastAPI
|
||||
|
||||
from litellm.proxy._lazy_features import loaded_lazy_modules
|
||||
|
||||
app = FastAPI()
|
||||
assert loaded_lazy_modules(app) == frozenset()
|
||||
|
||||
app.state.lazy_loaded = {"litellm.proxy.spend_tracking.cloudzero_endpoints"}
|
||||
assert loaded_lazy_modules(app) == frozenset({"litellm.proxy.spend_tracking.cloudzero_endpoints"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_spend_redis_clean_miss_skips_stale_in_memory():
|
||||
"""When Redis is reachable and cleanly returns None (TTL expired,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue