@@ -309,15 +309,81 @@ async def _load(self, feat: LazyFeature) -> None:
309309
310310
311311def attach_lazy_features (app : "FastAPI" ) -> None :
312+ app .include_router (_make_warmup_router (app ))
312313 app .add_middleware (LazyFeatureMiddleware , fastapi_app = app )
313314
314315
316+ def _make_warmup_router (app : "FastAPI" ) -> "APIRouter" :
317+ """POST /lazy/warm/{name}: load a feature and return its partial openapi
318+ so the Swagger plugin can merge in-place without a full /openapi.json refetch."""
319+ from fastapi import APIRouter , HTTPException
320+ from fastapi .openapi .utils import get_openapi
321+
322+ router = APIRouter ()
323+
324+ @router .post ("/lazy/warm/{name}" , include_in_schema = False )
325+ async def warm (name : str ):
326+ feat = next ((f for f in LAZY_FEATURES if f .name == name ), None )
327+ if feat is None :
328+ raise HTTPException (404 , f"unknown lazy feature: { name } " )
329+ if feat .persistent_swagger_stub :
330+ return {"stub_path" : None , "paths" : {}, "components" : {"schemas" : {}}}
331+
332+ already = any (
333+ any (getattr (r , "path" , "" ).startswith (p ) for p in feat .path_prefixes )
334+ for r in app .routes
335+ )
336+ if not already :
337+ loop = asyncio .get_running_loop ()
338+ module = await loop .run_in_executor (
339+ None , importlib .import_module , feat .module_path
340+ )
341+ feat .register_fn (app , module )
342+ app .openapi_schema = None
343+
344+ feat_routes = [
345+ r
346+ for r in app .routes
347+ if any (getattr (r , "path" , "" ).startswith (p ) for p in feat .path_prefixes )
348+ ]
349+ full = get_openapi (title = app .title , version = app .version , routes = feat_routes )
350+ # Force all operations under one tag so they group under a single Swagger
351+ # section — many lazy modules tag routes inconsistently.
352+ for path_ops in full .get ("paths" , {}).values ():
353+ for op in path_ops .values ():
354+ if isinstance (op , dict ):
355+ op ["tags" ] = [feat .name ]
356+ return {
357+ "stub_path" : feat .path_prefixes [0 ],
358+ "paths" : full .get ("paths" , {}),
359+ "components" : {"schemas" : full .get ("components" , {}).get ("schemas" , {})},
360+ }
361+
362+ return router
363+
364+
315365def inject_lazy_stubs (schema : Dict ) -> Dict :
316- """Stub openapi entries for unloaded features so Swagger renders sections."""
366+ """Inject openapi entries for unloaded features. Uses the snapshot file
367+ when available (full route info), otherwise falls back to a single
368+ placeholder per feature."""
369+ from litellm .proxy ._lazy_openapi_snapshot import load_snapshot
370+
371+ snapshot = load_snapshot ()
317372 paths = schema .setdefault ("paths" , {})
373+ schemas = schema .setdefault ("components" , {}).setdefault ("schemas" , {})
374+
318375 for feat in LAZY_FEATURES :
319376 if feat .module_path in sys .modules and not feat .persistent_swagger_stub :
320377 continue
378+
379+ fragment = (snapshot or {}).get (feat .name )
380+ if fragment :
381+ for p , ops in fragment .get ("paths" , {}).items ():
382+ paths .setdefault (p , ops )
383+ for name , sch in fragment .get ("components" , {}).get ("schemas" , {}).items ():
384+ schemas .setdefault (name , sch )
385+ continue
386+
321387 prefix = feat .path_prefixes [0 ]
322388 if prefix in paths :
323389 continue
@@ -333,8 +399,12 @@ def inject_lazy_stubs(schema: Dict) -> Dict:
333399
334400def lazy_tag_to_prefix () -> Dict [str , str ]:
335401 """feature.name -> first prefix, used by the Swagger warmup JS plugin.
336- Excludes persistent-stub features (mounted sub-apps) — warming them
337- triggers a streaming hit and no useful new routes appear."""
402+ Returns empty when the snapshot is loaded — the plugin is unnecessary
403+ because /openapi.json already has full route info."""
404+ from litellm .proxy ._lazy_openapi_snapshot import load_snapshot
405+
406+ if load_snapshot ():
407+ return {}
338408 return {
339409 feat .name : feat .path_prefixes [0 ]
340410 for feat in LAZY_FEATURES
0 commit comments