Skip to content

Commit 8538193

Browse files
committed
chore(proxy): stabilize lazy openapi snapshot
1 parent 5e7c9e6 commit 8538193

3 files changed

Lines changed: 87 additions & 16 deletions

File tree

‎litellm/proxy/_lazy_openapi_snapshot.json‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3572,7 +3572,7 @@
35723572
"/anthropic/{endpoint}": {
35733573
"delete": {
35743574
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion)",
3575-
"operationId": "anthropic_proxy_route_anthropic__endpoint__put",
3575+
"operationId": "anthropic_proxy_route_anthropic__endpoint__delete",
35763576
"parameters": [
35773577
{
35783578
"in": "path",
@@ -3616,7 +3616,7 @@
36163616
},
36173617
"get": {
36183618
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion)",
3619-
"operationId": "anthropic_proxy_route_anthropic__endpoint__put",
3619+
"operationId": "anthropic_proxy_route_anthropic__endpoint__get",
36203620
"parameters": [
36213621
{
36223622
"in": "path",
@@ -3660,7 +3660,7 @@
36603660
},
36613661
"patch": {
36623662
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion)",
3663-
"operationId": "anthropic_proxy_route_anthropic__endpoint__put",
3663+
"operationId": "anthropic_proxy_route_anthropic__endpoint__patch",
36643664
"parameters": [
36653665
{
36663666
"in": "path",
@@ -3704,7 +3704,7 @@
37043704
},
37053705
"post": {
37063706
"description": "[Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion)",
3707-
"operationId": "anthropic_proxy_route_anthropic__endpoint__put",
3707+
"operationId": "anthropic_proxy_route_anthropic__endpoint__post",
37083708
"parameters": [
37093709
{
37103710
"in": "path",
@@ -13260,7 +13260,7 @@
1326013260
"/langfuse/{endpoint}": {
1326113261
"delete": {
1326213262
"description": "Call Langfuse via LiteLLM proxy. Works with Langfuse SDK.\n\n[Docs](https://docs.litellm.ai/docs/pass_through/langfuse)",
13263-
"operationId": "langfuse_proxy_route_langfuse__endpoint__put",
13263+
"operationId": "langfuse_proxy_route_langfuse__endpoint__delete",
1326413264
"parameters": [
1326513265
{
1326613266
"in": "path",
@@ -13299,7 +13299,7 @@
1329913299
},
1330013300
"get": {
1330113301
"description": "Call Langfuse via LiteLLM proxy. Works with Langfuse SDK.\n\n[Docs](https://docs.litellm.ai/docs/pass_through/langfuse)",
13302-
"operationId": "langfuse_proxy_route_langfuse__endpoint__put",
13302+
"operationId": "langfuse_proxy_route_langfuse__endpoint__get",
1330313303
"parameters": [
1330413304
{
1330513305
"in": "path",
@@ -13338,7 +13338,7 @@
1333813338
},
1333913339
"patch": {
1334013340
"description": "Call Langfuse via LiteLLM proxy. Works with Langfuse SDK.\n\n[Docs](https://docs.litellm.ai/docs/pass_through/langfuse)",
13341-
"operationId": "langfuse_proxy_route_langfuse__endpoint__put",
13341+
"operationId": "langfuse_proxy_route_langfuse__endpoint__patch",
1334213342
"parameters": [
1334313343
{
1334413344
"in": "path",
@@ -13377,7 +13377,7 @@
1337713377
},
1337813378
"post": {
1337913379
"description": "Call Langfuse via LiteLLM proxy. Works with Langfuse SDK.\n\n[Docs](https://docs.litellm.ai/docs/pass_through/langfuse)",
13380-
"operationId": "langfuse_proxy_route_langfuse__endpoint__put",
13380+
"operationId": "langfuse_proxy_route_langfuse__endpoint__post",
1338113381
"parameters": [
1338213382
{
1338313383
"in": "path",
@@ -26883,7 +26883,7 @@
2688326883
"/toolset/{toolset_name}/mcp": {
2688426884
"delete": {
2688526885
"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.",
26886-
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_put",
26886+
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_delete",
2688726887
"parameters": [
2688826888
{
2688926889
"in": "path",
@@ -26922,7 +26922,7 @@
2692226922
},
2692326923
"get": {
2692426924
"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.",
26925-
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_put",
26925+
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_get",
2692626926
"parameters": [
2692726927
{
2692826928
"in": "path",
@@ -26961,7 +26961,7 @@
2696126961
},
2696226962
"head": {
2696326963
"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.",
26964-
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_put",
26964+
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_head",
2696526965
"parameters": [
2696626966
{
2696726967
"in": "path",
@@ -27000,7 +27000,7 @@
2700027000
},
2700127001
"options": {
2700227002
"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.",
27003-
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_put",
27003+
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_options",
2700427004
"parameters": [
2700527005
{
2700627006
"in": "path",
@@ -27039,7 +27039,7 @@
2703927039
},
2704027040
"patch": {
2704127041
"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.",
27042-
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_put",
27042+
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_patch",
2704327043
"parameters": [
2704427044
{
2704527045
"in": "path",
@@ -27078,7 +27078,7 @@
2707827078
},
2707927079
"post": {
2708027080
"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.",
27081-
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_put",
27081+
"operationId": "toolset_mcp_route_toolset__toolset_name__mcp_post",
2708227082
"parameters": [
2708327083
{
2708427084
"in": "path",

‎litellm/proxy/_lazy_openapi_snapshot.py‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import Dict, Optional
1414

1515
SNAPSHOT_FILE = Path(__file__).parent / "_lazy_openapi_snapshot.json"
16+
HTTP_METHODS = {"delete", "get", "head", "options", "patch", "post", "put"}
1617

1718

1819
def load_snapshot() -> Optional[Dict[str, Dict]]:
@@ -25,6 +26,39 @@ def load_snapshot() -> Optional[Dict[str, Dict]]:
2526
return None
2627

2728

29+
def _normalize_operation_ids(paths: Dict[str, Dict]) -> None:
30+
"""Make FastAPI-generated operation IDs stable for multi-method routes.
31+
32+
FastAPI derives the default operation ID suffix from the first item in the
33+
route's methods set. For routes registered with several HTTP methods, that
34+
set iteration order can vary between processes, which makes the snapshot
35+
drift even when no routes changed.
36+
"""
37+
for path_ops in paths.values():
38+
if not isinstance(path_ops, dict):
39+
continue
40+
41+
methods = {method for method in path_ops if method in HTTP_METHODS}
42+
if not methods:
43+
continue
44+
45+
for method, operation in path_ops.items():
46+
if method not in HTTP_METHODS or not isinstance(operation, dict):
47+
continue
48+
49+
operation_id = operation.get("operationId")
50+
if not isinstance(operation_id, str):
51+
continue
52+
53+
for suffix in methods:
54+
suffix_token = f"_{suffix}"
55+
if operation_id.endswith(suffix_token):
56+
operation["operationId"] = (
57+
operation_id[: -len(suffix_token)] + f"_{method}"
58+
)
59+
break
60+
61+
2862
def generate_snapshot() -> Dict[str, Dict]:
2963
import importlib
3064

@@ -52,13 +86,15 @@ def generate_snapshot() -> Dict[str, Dict]:
5286
if not feat_routes:
5387
continue
5488
full = get_openapi(title=app.title, version=app.version, routes=feat_routes)
89+
paths = full.get("paths", {})
90+
_normalize_operation_ids(paths)
5591
# Group all of a feature's routes under one tag.
56-
for path_ops in full.get("paths", {}).values():
92+
for path_ops in paths.values():
5793
for op in path_ops.values():
5894
if isinstance(op, dict):
5995
op["tags"] = [feat.name]
6096
fragments[feat.name] = {
61-
"paths": full.get("paths", {}),
97+
"paths": paths,
6298
"components": {"schemas": full.get("components", {}).get("schemas", {})},
6399
}
64100
return fragments
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from litellm.proxy._lazy_openapi_snapshot import _normalize_operation_ids
2+
3+
4+
def test_normalize_operation_ids_uses_each_http_method():
5+
paths = {
6+
"/proxy/{endpoint}": {
7+
"delete": {"operationId": "proxy_route_proxy__endpoint__put"},
8+
"get": {"operationId": "proxy_route_proxy__endpoint__put"},
9+
"post": {"operationId": "proxy_route_proxy__endpoint__put"},
10+
"put": {"operationId": "proxy_route_proxy__endpoint__put"},
11+
}
12+
}
13+
14+
_normalize_operation_ids(paths)
15+
16+
operations = paths["/proxy/{endpoint}"]
17+
assert operations["delete"]["operationId"] == "proxy_route_proxy__endpoint__delete"
18+
assert operations["get"]["operationId"] == "proxy_route_proxy__endpoint__get"
19+
assert operations["post"]["operationId"] == "proxy_route_proxy__endpoint__post"
20+
assert operations["put"]["operationId"] == "proxy_route_proxy__endpoint__put"
21+
22+
23+
def test_normalize_operation_ids_preserves_custom_ids():
24+
paths = {
25+
"/proxy/{endpoint}": {
26+
"get": {"operationId": "custom_operation"},
27+
"post": {"operationId": "custom_operation"},
28+
}
29+
}
30+
31+
_normalize_operation_ids(paths)
32+
33+
operations = paths["/proxy/{endpoint}"]
34+
assert operations["get"]["operationId"] == "custom_operation"
35+
assert operations["post"]["operationId"] == "custom_operation"

0 commit comments

Comments
 (0)