fix(proxy): stabilize lazy openapi snapshot ids

This commit is contained in:
user 2026-04-30 14:42:50 -07:00
parent ad3a251eb8
commit 3800596d08
3 changed files with 44 additions and 8 deletions

View file

@ -26883,7 +26883,7 @@
"/toolset/{toolset_name}/mcp": {
"delete": {
"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_head",
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
"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_head",
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
"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_head",
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
"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_head",
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
"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_head",
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
"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_head",
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
"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_head",
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
"parameters": [
{
"in": "path",

View file

@ -8,9 +8,10 @@ any drift as a neutral check.
"""
import json
import re
import sys
from pathlib import Path
from typing import Dict, Optional
from typing import Dict, Iterable, Optional
SNAPSHOT_FILE = Path(__file__).parent / "_lazy_openapi_snapshot.json"
@ -25,6 +26,24 @@ def load_snapshot() -> Optional[Dict[str, Dict]]:
return None
def _stable_generate_unique_id(route) -> str:
operation_id = f"{route.name}{route.path_format}"
operation_id = re.sub(r"\W", "_", operation_id)
methods = sorted(route.methods or [])
if not methods:
return operation_id
return f"{operation_id}_{methods[0].lower()}"
def _set_stable_operation_ids(routes: Iterable) -> None:
for route in routes:
if getattr(route, "operation_id", None) is not None:
continue
if getattr(route, "methods", None) is None:
continue
route.operation_id = _stable_generate_unique_id(route)
def generate_snapshot() -> Dict[str, Dict]:
import importlib
@ -51,6 +70,7 @@ def generate_snapshot() -> Dict[str, Dict]:
]
if not feat_routes:
continue
_set_stable_operation_ids(feat_routes)
full = get_openapi(title=app.title, version=app.version, routes=feat_routes)
# Group all of a feature's routes under one tag.
for path_ops in full.get("paths", {}).values():

View file

@ -0,0 +1,16 @@
from types import SimpleNamespace
from litellm.proxy._lazy_openapi_snapshot import _stable_generate_unique_id
def test_stable_generate_unique_id_sorts_route_methods():
route = SimpleNamespace(
name="langfuse_proxy_route",
path_format="/langfuse/{endpoint}",
methods={"POST", "GET", "DELETE", "PATCH", "PUT"},
)
assert (
_stable_generate_unique_id(route)
== "langfuse_proxy_route_langfuse__endpoint__delete"
)