Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/_lazy_features.py: 75%
169 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2Lazy registration for optional feature routers. Each LAZY_FEATURES entry
3imports its module only on the first request matching its path prefix,
4saving ~700 MB at idle for deployments that don't use these features.
5First hit pays the import cost (1-3 s for heavy modules); /openapi.json
6omits each feature's routes until the feature is warmed.
7"""
9import asyncio
10import importlib
11from collections.abc import Callable, Mapping, Sequence
12from collections.abc import Set as AbstractSet
13from dataclasses import dataclass, field
14from itertools import chain
15from types import MappingProxyType
16from typing import TYPE_CHECKING, Final
18from starlette.routing import BaseRoute, Match
19from starlette.types import ASGIApp, Receive, Scope, Send
21from litellm._logging import verbose_proxy_logger
22from litellm.proxy.route_priority import hot_routes_first
24if TYPE_CHECKING: 24 ↛ 25line 24 didn't jump to line 25 because the condition on line 24 was never true
25 from fastapi import APIRouter, FastAPI
28def _include_router(attr_name: str = "router") -> Callable[["FastAPI", object], None]:
29 def _register(app: "FastAPI", module: object) -> None:
30 app.include_router(getattr(module, attr_name))
32 return _register
35def _mount_app(prefix: str, attr_name: str = "app") -> Callable[["FastAPI", object], None]:
36 def _register(app: "FastAPI", module: object) -> None:
37 app.mount(path=prefix, app=getattr(module, attr_name))
39 return _register
42@dataclass(frozen=True)
43class LazyFeature:
44 name: str
45 module_path: str
46 path_prefixes: tuple[str, ...]
47 register_fn: Callable[["FastAPI", object], None] = field(default_factory=lambda: _include_router("router"))
48 # For routes whose path has a leading parameter (e.g. /{server}/authorize)
49 # — startswith can't match those, so the matcher also checks endswith.
50 path_suffixes: tuple[str, ...] = ()
51 # Keep the stub injected even after load — for mounted ASGI sub-apps
52 # whose routes don't appear in the parent app's openapi spec.
53 persistent_swagger_stub: bool = False
55 def matches(self, path: str) -> bool:
56 return any(path.startswith(p) for p in self.path_prefixes) or any(path.endswith(s) for s in self.path_suffixes)
59LAZY_FEATURES: Final[tuple[LazyFeature, ...]] = (
60 LazyFeature(
61 name="guardrails",
62 module_path="litellm.proxy.guardrails.guardrail_endpoints",
63 path_prefixes=(
64 "/guardrails",
65 "/v2/guardrails",
66 "/apply_guardrail",
67 "/policies/usage",
68 ),
69 ),
70 LazyFeature(
71 name="policies",
72 module_path="litellm.proxy.management_endpoints.policy_endpoints",
73 # Trailing slash to avoid matching /policies/... (policy_engine).
74 path_prefixes=("/policy/", "/utils/test_policies_and_guardrails"),
75 ),
76 LazyFeature(
77 name="policy_engine",
78 module_path="litellm.proxy.policy_engine.policy_endpoints",
79 path_prefixes=("/policies",),
80 ),
81 LazyFeature(
82 name="policy_resolve",
83 module_path="litellm.proxy.policy_engine.policy_resolve_endpoints",
84 path_prefixes=("/policies/resolve", "/policies/attachments/estimate-impact"),
85 ),
86 LazyFeature(
87 name="agents",
88 module_path="litellm.proxy.agent_endpoints.endpoints",
89 path_prefixes=("/v1/agents", "/agents", "/agent/"),
90 ),
91 LazyFeature(
92 name="gemini_agents",
93 module_path="litellm.proxy.google_endpoints.agents_endpoints",
94 path_prefixes=("/v1beta/agents",),
95 ),
96 LazyFeature(
97 name="a2a",
98 module_path="litellm.proxy.agent_endpoints.a2a_endpoints",
99 # ``/v1/a2a/{agent_id}/message/send`` is caught via the suffix so the
100 # ``/v1/a2a`` prefix doesn't subsume the discover prefix below.
101 path_prefixes=("/a2a",),
102 path_suffixes=("/message/send",),
103 ),
104 LazyFeature(
105 name="a2a_registration",
106 module_path="litellm.proxy.a2a.endpoints",
107 path_prefixes=("/v1/a2a/discover",),
108 ),
109 LazyFeature(
110 name="vector_stores",
111 module_path="litellm.proxy.vector_store_endpoints.endpoints",
112 path_prefixes=("/v1/vector_stores", "/vector_stores", "/v1/indexes"),
113 ),
114 LazyFeature(
115 name="vector_store_management",
116 module_path="litellm.proxy.vector_store_endpoints.management_endpoints",
117 # Trailing slash to avoid matching /vector_stores/... (vector_stores).
118 path_prefixes=("/vector_store/", "/v1/vector_store/"),
119 ),
120 LazyFeature(
121 name="vector_store_files",
122 # Routes appear under both /v1/vector_stores/{id}/files and the
123 # un-versioned form, so both prefixes must trigger the load.
124 module_path="litellm.proxy.vector_store_files_endpoints.endpoints",
125 path_prefixes=("/v1/vector_stores", "/vector_stores"),
126 ),
127 LazyFeature(
128 name="tools",
129 module_path="litellm.proxy.management_endpoints.tool_management_endpoints",
130 path_prefixes=("/v1/tool", "/tool"),
131 ),
132 LazyFeature(
133 name="search_tools",
134 module_path="litellm.proxy.search_endpoints.search_tool_management",
135 path_prefixes=("/search_tools",),
136 ),
137 # mcp_management owns most /v1/mcp/* admin routes; mcp_app is the mounted
138 # streaming sub-app at /mcp.
139 LazyFeature(
140 name="mcp_management",
141 module_path="litellm.proxy.management_endpoints.mcp_management_endpoints",
142 path_prefixes=("/v1/mcp/",),
143 ),
144 LazyFeature(
145 # Also serves /.well-known/oauth-* (OAuth metadata discovery).
146 # No /mcp/oauth prefix here: the mounted /mcp sub-app would
147 # shadow it, and there are no actual routes there anyway.
148 name="mcp_byok_oauth",
149 module_path="litellm.proxy._experimental.mcp_server.byok_oauth_endpoints",
150 path_prefixes=("/v1/mcp/oauth", "/.well-known/oauth-"),
151 ),
152 LazyFeature(
153 # Serves OAuth dance endpoints (/authorize, /token, /callback,
154 # /register) plus several /.well-known/ discovery URLs at the proxy
155 # root — needed for MCP-over-OAuth flows even before /mcp is hit.
156 name="mcp_discoverable",
157 module_path="litellm.proxy._experimental.mcp_server.discoverable_endpoints",
158 path_prefixes=(
159 "/.well-known/oauth-",
160 "/.well-known/openid-configuration",
161 "/.well-known/jwks.json",
162 "/.well-known/litellm-cli-auth",
163 "/authorize",
164 "/token",
165 "/callback",
166 "/register",
167 "/revoke",
168 "/introspect",
169 ),
170 # Catches the /{mcp_server_name}/authorize|token|register variants.
171 path_suffixes=("/authorize", "/token", "/register"),
172 ),
173 LazyFeature(
174 name="mcp_rest",
175 module_path="litellm.proxy._experimental.mcp_server.rest_endpoints",
176 path_prefixes=("/mcp-rest",),
177 ),
178 LazyFeature(
179 # Hardcoded /mcp matches BASE_MCP_ROUTE; importing the constant
180 # here would defeat lazy loading.
181 name="mcp_app",
182 module_path="litellm.proxy._experimental.mcp_server.server",
183 path_prefixes=("/mcp",),
184 register_fn=_mount_app("/mcp", attr_name="app"),
185 persistent_swagger_stub=True,
186 ),
187 LazyFeature(
188 name="config_overrides",
189 module_path="litellm.proxy.management_endpoints.config_override_endpoints",
190 path_prefixes=("/config_overrides",),
191 ),
192 LazyFeature(
193 name="llm_passthrough",
194 module_path="litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints",
195 path_prefixes=(
196 "/anthropic/",
197 "/assemblyai/",
198 "/azure/",
199 "/azure_ai/",
200 "/azure_speech/",
201 "/bedrock/",
202 "/cohere/",
203 "/comprehendmedical",
204 "/cursor/",
205 "/deepgram/",
206 "/eu.assemblyai/",
207 "/fal_ai/",
208 "/gemini/",
209 "/gigachat/",
210 "/milvus/",
211 "/mistral/",
212 "/nvidia_nim/",
213 "/openai/",
214 "/openai_passthrough/",
215 "/tinyfish/",
216 "/transcribe",
217 "/typesafe/",
218 "/openrouter/",
219 "/vertex-ai/",
220 "/vertex_ai/",
221 "/vllm/",
222 "/watsonx/",
223 ),
224 ),
225 LazyFeature(
226 name="realtime",
227 module_path="litellm.proxy.realtime_endpoints.endpoints",
228 path_prefixes=("/openai/v1/realtime", "/v1/realtime", "/realtime"),
229 ),
230 LazyFeature(
231 name="anthropic_passthrough",
232 module_path="litellm.proxy.anthropic_endpoints.endpoints",
233 path_prefixes=("/v1/messages", "/anthropic", "/api/event_logging"),
234 ),
235 LazyFeature(
236 name="anthropic_skills",
237 module_path="litellm.proxy.anthropic_endpoints.skills_endpoints",
238 path_prefixes=("/v1/skills", "/skills"),
239 ),
240 LazyFeature(
241 name="claude_code_gateway",
242 module_path="litellm.proxy.anthropic_endpoints.gateway_endpoints",
243 path_prefixes=("/claude_code_gateway",),
244 ),
245 LazyFeature(
246 name="langfuse_passthrough",
247 module_path="litellm.proxy.vertex_ai_endpoints.langfuse_endpoints",
248 path_prefixes=("/langfuse",),
249 ),
250 LazyFeature(
251 name="evals",
252 module_path="litellm.proxy.openai_evals_endpoints.endpoints",
253 path_prefixes=("/v1/evals", "/evals"),
254 ),
255 LazyFeature(
256 name="decisions",
257 module_path="litellm.proxy.decisions_endpoints.endpoints",
258 path_prefixes=("/v1/decisions", "/decisions", "/v1/systemone", "/systemone"),
259 ),
260 LazyFeature(
261 name="claude_code_marketplace",
262 module_path="litellm.proxy.anthropic_endpoints.claude_code_endpoints",
263 path_prefixes=("/claude-code",),
264 register_fn=_include_router("claude_code_marketplace_router"),
265 ),
266 LazyFeature(
267 name="scim",
268 module_path="litellm.proxy.management_endpoints.scim.scim_v2",
269 path_prefixes=("/scim",),
270 register_fn=_include_router("scim_router"),
271 ),
272 LazyFeature(
273 name="cloudzero",
274 module_path="litellm.proxy.spend_tracking.cloudzero_endpoints",
275 path_prefixes=("/cloudzero",),
276 ),
277 LazyFeature(
278 name="vantage",
279 module_path="litellm.proxy.spend_tracking.vantage_endpoints",
280 path_prefixes=("/vantage",),
281 ),
282 LazyFeature(
283 name="usage_ai",
284 module_path="litellm.proxy.management_endpoints.usage_endpoints",
285 path_prefixes=("/usage/ai",),
286 ),
287 LazyFeature(
288 name="prompts",
289 module_path="litellm.proxy.prompts.prompt_endpoints",
290 path_prefixes=("/prompts", "/utils/dotprompt_json_converter"),
291 ),
292 LazyFeature(
293 name="jwt_mappings",
294 module_path="litellm.proxy.management_endpoints.jwt_key_mapping_endpoints",
295 path_prefixes=("/jwt/key/mapping",),
296 ),
297 LazyFeature(
298 name="compliance",
299 module_path="litellm.proxy.management_endpoints.compliance_endpoints",
300 path_prefixes=("/compliance",),
301 ),
302 LazyFeature(
303 name="access_groups",
304 module_path="litellm.proxy.management_endpoints.access_group_endpoints",
305 path_prefixes=("/access_group", "/v1/access_group", "/v1/unified_access_group"),
306 ),
307)
310class LazyFeatureMiddleware:
311 """ASGI middleware that imports + registers a feature router on first
312 matching request. Idempotent; once loaded, subsequent requests skip."""
314 def __init__(
315 self,
316 app: ASGIApp,
317 fastapi_app: "FastAPI",
318 features: tuple[LazyFeature, ...] = LAZY_FEATURES,
319 ):
320 self.app = app
321 self._fastapi_app = fastapi_app
322 self._features = features
323 # SERVER_ROOT_PATH is a process-startup env var, cache the normalized
324 # form once instead of recomputing per request. Lazy import to avoid
325 # pulling proxy.utils into this module's import graph at startup
326 # (proxy_server imports both).
327 from litellm.proxy.utils import get_server_root_path
329 self._root_path = get_server_root_path().rstrip("/")
330 # Loaded set / per-feature locks live on app.state so the warm endpoint
331 # and the middleware share them — preventing duplicate registrations
332 # when both paths fire for the same feature.
333 if not hasattr(fastapi_app.state, "lazy_loaded"): 333 ↛ exitline 333 didn't return from function '__init__' because the condition on line 333 was always true
334 fastapi_app.state.lazy_loaded = set()
335 fastapi_app.state.lazy_locks = {}
337 @property
338 def _loaded(self) -> set:
339 return self._fastapi_app.state.lazy_loaded
341 async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
342 # Short-circuit once every feature has loaded.
343 if scope["type"] in ("http", "websocket") and len(self._loaded) < len(self._features):
344 path = scope.get("path", "")
345 # Strip the request's root_path so prefix matching works under a
346 # server root path. Without this, requests like /api/v1/policies/...
347 # never match the registered prefixes (/policies/...) and lazy
348 # features stay unloaded — every endpoint under them returns 404.
349 # scope["root_path"] wins over the cached env scalar: FastAPI
350 # stamps SERVER_ROOT_PATH there, and PerRequestRootPathMiddleware
351 # resolves SERVER_ROOT_PATHS prefixes there per request. The
352 # `+ "/"` boundary prevents false-positive matches (e.g. /apiv2
353 # against root /api); a pre-stripped path is left alone.
354 root_path: Final = str(scope.get("root_path", "")).rstrip("/") or self._root_path
355 if root_path and path.startswith(root_path + "/"): 355 ↛ 356line 355 didn't jump to line 356 because the condition on line 355 was never true
356 path = path[len(root_path) :] # rebind-ok: local strip after the boundary check above
357 for feat in self._features:
358 if feat.module_path in self._loaded or not feat.matches(path):
359 continue
360 if _eager_route_wins(self._fastapi_app, feat, scope):
361 continue
362 await _force_load(self._fastapi_app, feat, self._features)
363 await self.app(scope, receive, send)
366def _lazy_slots(app: "FastAPI") -> Mapping[str, BaseRoute | None]:
367 return app.state.lazy_slots if hasattr(app.state, "lazy_slots") else MappingProxyType({})
370def _lazy_routes(app: "FastAPI") -> Mapping[str, tuple[BaseRoute, ...]]:
371 return app.state.lazy_routes if hasattr(app.state, "lazy_routes") else MappingProxyType({})
374def reserve_lazy_slot(app: "FastAPI", name: str, features: tuple[LazyFeature, ...] = LAZY_FEATURES) -> None:
375 """Record the route the feature's router used to be included after, so its routes
376 are spliced back in there once it loads and keep the same precedence. Anchoring on
377 the route rather than its index survives later reordering of the table."""
378 feat: Final = next(f for f in features if f.name == name)
379 anchor: Final = app.router.routes[-1] if app.router.routes else None
380 app.state.lazy_slots = MappingProxyType({**_lazy_slots(app), feat.module_path: anchor})
383def _slot_index(routes: Sequence[BaseRoute], anchor: BaseRoute | None) -> int:
384 if anchor is None: 384 ↛ 385line 384 didn't jump to line 385 because the condition on line 384 was never true
385 return 0
386 return next((i + 1 for i, route in enumerate(routes) if route is anchor), len(routes))
389def _eager_route_wins(app: "FastAPI", feat: LazyFeature, scope: Scope) -> bool:
390 """Routes ahead of a feature's reserved slot beat its routes in Starlette's scan,
391 so a request one of them fully matches never needs the feature loaded."""
392 slots: Final = _lazy_slots(app)
393 if feat.module_path not in slots:
394 return False
395 ahead: Final = app.router.routes[: _slot_index(app.router.routes, slots[feat.module_path])]
396 return any(route.matches(scope)[0] is Match.FULL for route in ahead)
399def _in_registry_order(
400 routes: Sequence[BaseRoute],
401 lazy_routes: Mapping[str, tuple[BaseRoute, ...]],
402 features: tuple[LazyFeature, ...],
403 slots: Mapping[str, BaseRoute | None],
404) -> tuple[BaseRoute, ...]:
405 """Lazy routers land in registry order, not first-request order, so overlapping
406 paths (/openai/{endpoint:path} vs /openai/v1/realtime/calls) resolve the same
407 way no matter which feature a deployment happens to hit first. Features with a
408 reserved slot go back where they were eagerly included; the rest follow every
409 eager route."""
410 rank: Final = MappingProxyType({f.module_path: i for i, f in enumerate(features)})
411 modules: Final = tuple(sorted(lazy_routes, key=lambda m: rank.get(m, len(rank))))
412 lazy_ids: Final = frozenset(id(route) for module_path in modules for route in lazy_routes[module_path])
413 eager: Final = tuple(route for route in routes if id(route) not in lazy_ids)
415 def slot_of(module_path: str) -> int:
416 return _slot_index(eager, slots[module_path]) if module_path in slots else len(eager)
418 return tuple(
419 route
420 for index in range(len(eager) + 1)
421 for route in (
422 *(r for module_path in modules if slot_of(module_path) == index for r in lazy_routes[module_path]),
423 *eager[index : index + 1],
424 )
425 )
428async def _force_load(app: "FastAPI", feat: LazyFeature, features: tuple[LazyFeature, ...] = LAZY_FEATURES) -> bool:
429 """Import + register a lazy feature exactly once per (app, module).
430 Shared by the middleware and the /lazy/warm endpoint."""
431 if not hasattr(app.state, "lazy_loaded"): 431 ↛ 432line 431 didn't jump to line 432 because the condition on line 431 was never true
432 app.state.lazy_loaded = set()
433 app.state.lazy_locks = {}
434 lock: Final = app.state.lazy_locks.setdefault(feat.module_path, asyncio.Lock())
435 async with lock:
436 if feat.module_path in app.state.lazy_loaded: 436 ↛ 437line 436 didn't jump to line 437 because the condition on line 436 was never true
437 return False
438 try:
439 # Import on a thread (heavy modules take 1-3 s). register_fn
440 # mutates app.router.routes, so it stays on the loop thread.
441 loop: Final = asyncio.get_running_loop()
442 module: Final = await loop.run_in_executor(None, importlib.import_module, feat.module_path)
443 before: Final = len(app.router.routes)
444 feat.register_fn(app, module)
445 previous: Final[Mapping[str, tuple[BaseRoute, ...]]] = (
446 app.state.lazy_routes if hasattr(app.state, "lazy_routes") else MappingProxyType({})
447 )
448 lazy_routes: Final[Mapping[str, tuple[BaseRoute, ...]]] = MappingProxyType(
449 {**previous, feat.module_path: tuple(app.router.routes[before:])}
450 )
451 app.state.lazy_routes = lazy_routes # rebind-ok: the app owns the record of which routes each feature added
452 app.router.routes[:] = hot_routes_first( # rebind-ok: the app owns its route table
453 _in_registry_order(app.router.routes, lazy_routes, features, _lazy_slots(app))
454 )
455 app.state.lazy_loaded.add(feat.module_path)
456 app.openapi_schema = None
457 verbose_proxy_logger.info(
458 "Lazy-loaded optional feature %r (module: %s)",
459 feat.name,
460 feat.module_path,
461 )
462 return True
463 except Exception as exc:
464 # Mark loaded anyway so we don't retry on every request.
465 app.state.lazy_loaded.add(feat.module_path)
466 verbose_proxy_logger.warning(
467 "Failed to lazy-load optional feature %r (module: %s): %s. "
468 "This feature's endpoints will return 404 until restart.",
469 feat.name,
470 feat.module_path,
471 exc,
472 )
473 return False
476def attach_lazy_features(app: "FastAPI") -> None:
477 app.include_router(_make_warmup_router(app))
478 app.add_middleware(LazyFeatureMiddleware, fastapi_app=app)
481def _make_warmup_router(app: "FastAPI") -> "APIRouter":
482 """POST /lazy/warm/{name}: load a feature and return its partial openapi
483 so the Swagger plugin can merge in-place without a full /openapi.json refetch.
484 Requires auth — anyone who can hit the proxy can already trigger the same
485 imports by sending a real request to a feature's prefix, but gating this
486 debug endpoint avoids unauthenticated callers forcing the import chain."""
487 from fastapi import APIRouter, Depends, HTTPException
488 from fastapi.openapi.utils import get_openapi
490 from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
492 router: Final = APIRouter()
494 @router.post(
495 "/lazy/warm/{name}",
496 include_in_schema=False,
497 dependencies=[Depends(user_api_key_auth)],
498 )
499 async def warm(name: str):
500 feat: Final = next((f for f in LAZY_FEATURES if f.name == name), None)
501 if feat is None:
502 raise HTTPException(404, f"unknown lazy feature: {name}")
503 if feat.persistent_swagger_stub:
504 return {"stub_path": None, "paths": {}, "components": {"schemas": {}}}
506 await _force_load(app, feat)
508 feat_routes: Final = [r for r in app.routes if feat.matches(getattr(r, "path", ""))]
509 full: Final = get_openapi(title=app.title, version=app.version, routes=feat_routes)
510 # Force all operations under one tag so they group under a single Swagger
511 # section — many lazy modules tag routes inconsistently.
512 for path_ops in full.get("paths", {}).values():
513 for op in path_ops.values():
514 if isinstance(op, dict):
515 op["tags"] = [feat.name]
516 return {
517 "stub_path": feat.path_prefixes[0],
518 "paths": full.get("paths", {}),
519 "components": {"schemas": full.get("components", {}).get("schemas", {})},
520 }
522 return router
525def lazy_owned_routes(app: "FastAPI") -> frozenset[int]:
526 """ids of the routes lazy features have registered on this app. A route added later at
527 one of their paths (a config pass-through at /v1/decisions) goes ahead of them, the
528 precedence lazy mode gives it when the feature has not loaded by the time the config is read."""
529 return frozenset(id(route) for route in chain.from_iterable(_lazy_routes(app).values()))
532def loaded_lazy_modules(app: "FastAPI") -> frozenset[str]:
533 """The set of lazy feature modules whose routers are actually registered
534 on this app (tracked by _force_load), empty before the middleware ever ran.
535 sys.modules is the wrong signal: boot code imports several feature modules
536 (mcp_management, cloudzero, vantage, config_overrides) without mounting
537 their routers, and their stubs must still be injected."""
538 loaded: Final = getattr(app.state, "lazy_loaded", None)
539 if not isinstance(loaded, set): 539 ↛ 540line 539 didn't jump to line 540 because the condition on line 539 was never true
540 return frozenset()
541 return frozenset(m for m in loaded if isinstance(m, str))
544def inject_lazy_stubs(
545 schema: dict,
546 loaded_modules: AbstractSet[str],
547 features: tuple[LazyFeature, ...] = LAZY_FEATURES,
548) -> dict:
549 """Inject openapi entries for features not in loaded_modules. Uses the
550 snapshot file when available (full route info), otherwise falls back to a
551 single placeholder per feature. Any failure logs and returns the schema
552 unchanged so /openapi.json never 500s on a cosmetic injection bug."""
553 try:
554 from litellm.proxy._lazy_openapi_snapshot import load_snapshot
556 snapshot: Final = load_snapshot()
557 paths: Final = schema.setdefault("paths", {})
558 schemas: Final = schema.setdefault("components", {}).setdefault("schemas", {})
560 for feat in features:
561 if feat.module_path in loaded_modules and not feat.persistent_swagger_stub: 561 ↛ 562line 561 didn't jump to line 562 because the condition on line 561 was never true
562 continue
564 fragment = (snapshot or {}).get(feat.name)
565 if fragment: 565 ↛ 572line 565 didn't jump to line 572 because the condition on line 565 was always true
566 for p, ops in fragment.get("paths", {}).items():
567 paths.setdefault(p, ops)
568 for name, sch in fragment.get("components", {}).get("schemas", {}).items():
569 schemas.setdefault(name, sch)
570 continue
572 prefix = feat.path_prefixes[0]
573 if prefix in paths:
574 continue
575 paths[prefix] = {
576 "get": {
577 "tags": [feat.name],
578 "summary": feat.name,
579 "responses": {"200": {"description": "OK"}},
580 }
581 }
582 except Exception as exc:
583 verbose_proxy_logger.warning("inject_lazy_stubs failed: %s", exc)
584 return schema
587def lazy_tag_to_prefix() -> dict[str, str]:
588 """feature.name -> first prefix, used by the Swagger warmup JS plugin.
589 Returns empty when the snapshot is loaded — the plugin is unnecessary
590 because /openapi.json already has full route info."""
591 from litellm.proxy._lazy_openapi_snapshot import load_snapshot
593 if load_snapshot(): 593 ↛ 595line 593 didn't jump to line 595 because the condition on line 593 was always true
594 return {}
595 return {feat.name: feat.path_prefixes[0] for feat in LAZY_FEATURES if not feat.persistent_swagger_stub}