mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
chore(proxy): split lazy openapi multi-method routes
This commit is contained in:
parent
eca1f252e2
commit
6ca6220679
3 changed files with 89 additions and 22 deletions
|
|
@ -3616,7 +3616,7 @@
|
|||
},
|
||||
"get": {
|
||||
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion)",
|
||||
"operationId": "anthropic_proxy_route_anthropic__endpoint__delete",
|
||||
"operationId": "anthropic_proxy_route_anthropic__endpoint__get",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -3660,7 +3660,7 @@
|
|||
},
|
||||
"patch": {
|
||||
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion)",
|
||||
"operationId": "anthropic_proxy_route_anthropic__endpoint__delete",
|
||||
"operationId": "anthropic_proxy_route_anthropic__endpoint__patch",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -3704,7 +3704,7 @@
|
|||
},
|
||||
"post": {
|
||||
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion)",
|
||||
"operationId": "anthropic_proxy_route_anthropic__endpoint__delete",
|
||||
"operationId": "anthropic_proxy_route_anthropic__endpoint__post",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -3748,7 +3748,7 @@
|
|||
},
|
||||
"put": {
|
||||
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion)",
|
||||
"operationId": "anthropic_proxy_route_anthropic__endpoint__delete",
|
||||
"operationId": "anthropic_proxy_route_anthropic__endpoint__put",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -13299,7 +13299,7 @@
|
|||
},
|
||||
"get": {
|
||||
"description": "Call Langfuse via LiteLLM proxy. Works with Langfuse SDK.\n\n[Docs](https://docs.litellm.ai/docs/pass_through/langfuse)",
|
||||
"operationId": "langfuse_proxy_route_langfuse__endpoint__delete",
|
||||
"operationId": "langfuse_proxy_route_langfuse__endpoint__get",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -13338,7 +13338,7 @@
|
|||
},
|
||||
"patch": {
|
||||
"description": "Call Langfuse via LiteLLM proxy. Works with Langfuse SDK.\n\n[Docs](https://docs.litellm.ai/docs/pass_through/langfuse)",
|
||||
"operationId": "langfuse_proxy_route_langfuse__endpoint__delete",
|
||||
"operationId": "langfuse_proxy_route_langfuse__endpoint__patch",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -13377,7 +13377,7 @@
|
|||
},
|
||||
"post": {
|
||||
"description": "Call Langfuse via LiteLLM proxy. Works with Langfuse SDK.\n\n[Docs](https://docs.litellm.ai/docs/pass_through/langfuse)",
|
||||
"operationId": "langfuse_proxy_route_langfuse__endpoint__delete",
|
||||
"operationId": "langfuse_proxy_route_langfuse__endpoint__post",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -13416,7 +13416,7 @@
|
|||
},
|
||||
"put": {
|
||||
"description": "Call Langfuse via LiteLLM proxy. Works with Langfuse SDK.\n\n[Docs](https://docs.litellm.ai/docs/pass_through/langfuse)",
|
||||
"operationId": "langfuse_proxy_route_langfuse__endpoint__delete",
|
||||
"operationId": "langfuse_proxy_route_langfuse__endpoint__put",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -26922,7 +26922,7 @@
|
|||
},
|
||||
"get": {
|
||||
"description": "Namespace a toolset as its own MCP endpoint.\n\nConnecting to /toolset/<name>/mcp exposes exactly the tools defined in\nthe toolset. Access is enforced: non-admin API keys must have the toolset\nlisted in their object_permission.mcp_toolsets grant list, or the request\nwill be rejected with a 403.",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_get",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -26961,7 +26961,7 @@
|
|||
},
|
||||
"head": {
|
||||
"description": "Namespace a toolset as its own MCP endpoint.\n\nConnecting to /toolset/<name>/mcp exposes exactly the tools defined in\nthe toolset. Access is enforced: non-admin API keys must have the toolset\nlisted in their object_permission.mcp_toolsets grant list, or the request\nwill be rejected with a 403.",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_head",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -27000,7 +27000,7 @@
|
|||
},
|
||||
"options": {
|
||||
"description": "Namespace a toolset as its own MCP endpoint.\n\nConnecting to /toolset/<name>/mcp exposes exactly the tools defined in\nthe toolset. Access is enforced: non-admin API keys must have the toolset\nlisted in their object_permission.mcp_toolsets grant list, or the request\nwill be rejected with a 403.",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_options",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -27039,7 +27039,7 @@
|
|||
},
|
||||
"patch": {
|
||||
"description": "Namespace a toolset as its own MCP endpoint.\n\nConnecting to /toolset/<name>/mcp exposes exactly the tools defined in\nthe toolset. Access is enforced: non-admin API keys must have the toolset\nlisted in their object_permission.mcp_toolsets grant list, or the request\nwill be rejected with a 403.",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_patch",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -27078,7 +27078,7 @@
|
|||
},
|
||||
"post": {
|
||||
"description": "Namespace a toolset as its own MCP endpoint.\n\nConnecting to /toolset/<name>/mcp exposes exactly the tools defined in\nthe toolset. Access is enforced: non-admin API keys must have the toolset\nlisted in their object_permission.mcp_toolsets grant list, or the request\nwill be rejected with a 403.",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_post",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
@ -27117,7 +27117,7 @@
|
|||
},
|
||||
"put": {
|
||||
"description": "Namespace a toolset as its own MCP endpoint.\n\nConnecting to /toolset/<name>/mcp exposes exactly the tools defined in\nthe toolset. Access is enforced: non-admin API keys must have the toolset\nlisted in their object_permission.mcp_toolsets grant list, or the request\nwill be rejected with a 403.",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
|
||||
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_put",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ any drift as a neutral check.
|
|||
import json
|
||||
import re
|
||||
import sys
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
SNAPSHOT_FILE = Path(__file__).parent / "_lazy_openapi_snapshot.json"
|
||||
|
||||
|
|
@ -28,11 +30,36 @@ def load_snapshot() -> Optional[Dict[str, Dict]]:
|
|||
return None
|
||||
|
||||
|
||||
def _stable_unique_id(route: APIRoute) -> str:
|
||||
def _stable_unique_id(route: APIRoute, method: str) -> str:
|
||||
operation_id = f"{route.name}{route.path_format}"
|
||||
operation_id = re.sub(r"\W", "_", operation_id)
|
||||
method = sorted(route.methods or [""])[0].lower()
|
||||
return f"{operation_id}_{method}"
|
||||
return f"{operation_id}_{method.lower()}"
|
||||
|
||||
|
||||
def _routes_with_stable_unique_ids(routes: List[BaseRoute]) -> List[BaseRoute]:
|
||||
stable_routes: List[BaseRoute] = []
|
||||
for route in routes:
|
||||
if not isinstance(route, APIRoute) or not route.methods:
|
||||
stable_routes.append(route)
|
||||
continue
|
||||
|
||||
methods = sorted(route.methods)
|
||||
has_multiple_methods = len(methods) > 1
|
||||
for method in methods:
|
||||
method_route = copy(route)
|
||||
method_route.methods = {method}
|
||||
if route.operation_id is not None:
|
||||
method_route.operation_id = (
|
||||
f"{route.operation_id}_{method.lower()}"
|
||||
if has_multiple_methods
|
||||
else route.operation_id
|
||||
)
|
||||
method_route.unique_id = method_route.operation_id
|
||||
else:
|
||||
method_route.unique_id = _stable_unique_id(route, method)
|
||||
stable_routes.append(method_route)
|
||||
|
||||
return stable_routes
|
||||
|
||||
|
||||
def generate_snapshot() -> Dict[str, Dict]:
|
||||
|
|
@ -61,10 +88,11 @@ def generate_snapshot() -> Dict[str, Dict]:
|
|||
]
|
||||
if not feat_routes:
|
||||
continue
|
||||
for route in feat_routes:
|
||||
if isinstance(route, APIRoute):
|
||||
route.unique_id = _stable_unique_id(route)
|
||||
full = get_openapi(title=app.title, version=app.version, routes=feat_routes)
|
||||
full = get_openapi(
|
||||
title=app.title,
|
||||
version=app.version,
|
||||
routes=_routes_with_stable_unique_ids(feat_routes),
|
||||
)
|
||||
# Group all of a feature's routes under one tag.
|
||||
for path_ops in full.get("paths", {}).values():
|
||||
for op in path_ops.values():
|
||||
|
|
|
|||
39
tests/test_litellm/proxy/test_lazy_openapi_snapshot.py
Normal file
39
tests/test_litellm/proxy/test_lazy_openapi_snapshot.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
from litellm.proxy._lazy_openapi_snapshot import _routes_with_stable_unique_ids
|
||||
|
||||
|
||||
def test_routes_with_stable_unique_ids_splits_multi_method_routes() -> None:
|
||||
app = FastAPI()
|
||||
|
||||
async def proxy_route() -> dict:
|
||||
return {}
|
||||
|
||||
app.add_api_route(
|
||||
"/proxy/{endpoint:path}",
|
||||
proxy_route,
|
||||
methods=["GET", "POST", "DELETE"],
|
||||
)
|
||||
|
||||
routes = [route for route in app.routes if isinstance(route, APIRoute)]
|
||||
stable_routes = _routes_with_stable_unique_ids(routes)
|
||||
|
||||
assert [route.methods for route in stable_routes] == [
|
||||
{"DELETE"},
|
||||
{"GET"},
|
||||
{"POST"},
|
||||
]
|
||||
|
||||
openapi = get_openapi(title="test", version="1", routes=stable_routes)
|
||||
path_ops = openapi["paths"]["/proxy/{endpoint}"]
|
||||
|
||||
operation_ids = {
|
||||
method: operation["operationId"] for method, operation in path_ops.items()
|
||||
}
|
||||
assert operation_ids == {
|
||||
"delete": "proxy_route_proxy__endpoint__delete",
|
||||
"get": "proxy_route_proxy__endpoint__get",
|
||||
"post": "proxy_route_proxy__endpoint__post",
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue