From 1e6a34850dfd2922412b0c5f01ef89e6d3b25ac6 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:46:30 -0700 Subject: [PATCH 1/3] fix(router): propagate capability flags to shared backend cost map key --- litellm/types/utils.py | 2 + .../test_router_model_cost_isolation.py | 52 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 5b98e8be8d2..173a5b6cfa1 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -270,6 +270,8 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False): "realtime", ] ] + supported_endpoints: Optional[List[str]] + use_openai_responses_path: Optional[bool] tpm: Optional[int] rpm: Optional[int] provider_specific_entry: Optional[Dict[str, float]] diff --git a/tests/test_litellm/test_router_model_cost_isolation.py b/tests/test_litellm/test_router_model_cost_isolation.py index c7f5513b94b..672b5b36197 100644 --- a/tests/test_litellm/test_router_model_cost_isolation.py +++ b/tests/test_litellm/test_router_model_cost_isolation.py @@ -803,6 +803,8 @@ def test_shared_backend_model_info_keeps_schema_fields_and_drops_the_rest(): "litellm_provider": "openai", "max_tokens": 128000, "supports_vision": True, + "supported_endpoints": ["/v1/responses"], + "use_openai_responses_path": True, "input_cost_per_token": 0.99, "output_cost_per_token": 0.99, "id": "deploy-a", @@ -818,9 +820,59 @@ def test_shared_backend_model_info_keeps_schema_fields_and_drops_the_rest(): "litellm_provider": "openai", "max_tokens": 128000, "supports_vision": True, + "supported_endpoints": ["/v1/responses"], + "use_openai_responses_path": True, } +def test_capability_flags_propagate_from_deployment_model_info_to_shared_key(): + """Backend-model capability facts (supported_endpoints, + use_openai_responses_path) declared in a deployment's model_info must reach + the shared backend key: the Bedrock Mantle routing gates read them raw off + litellm.model_cost and document proxy model_info as an override path for + models missing from the built-in cost map. + """ + from litellm.llms.bedrock_mantle.common_utils import ( + mantle_base_segment, + mantle_supports_responses, + ) + + bare_model = "somelab.lit4544-unmapped-model" + backend_model = f"bedrock_mantle/{bare_model}" + deploy_id = "lit4544-mantle-deploy" + + model_keys = { + key: copy.deepcopy(litellm.model_cost.get(key)) + for key in (bare_model, backend_model, deploy_id) + } + try: + Router( + model_list=[ + { + "model_name": "mantle-alias", + "litellm_params": { + "model": backend_model, + "api_key": "fake-key", + }, + "model_info": { + "id": deploy_id, + "supported_endpoints": ["/v1/responses"], + "use_openai_responses_path": True, + }, + }, + ], + ) + + shared_entry = litellm.model_cost.get(backend_model) or {} + assert shared_entry.get("supported_endpoints") == ["/v1/responses"] + assert shared_entry.get("use_openai_responses_path") is True + assert "id" not in shared_entry + assert mantle_supports_responses(bare_model, litellm.model_cost) is True + assert mantle_base_segment(bare_model, litellm.model_cost) == "openai/v1" + finally: + _restore_model_cost_entries(model_keys) + + def test_wildcard_zero_cost_request_does_not_poison_named_deployment_pricing(): """LIT-3991 end to end: a proxy has a named text-embedding-3-small deployment relying on built-in pricing plus an ``openai/*`` wildcard with From 3334ee9b134ce7b6dd681e7478d542c399d0172a Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Mon, 20 Jul 2026 19:02:00 -0700 Subject: [PATCH 2/3] fix(mcp): add Google Sheets, Drive, Calendar, and Docs to the OpenAPI registry --- litellm/proxy/openapi_registry.json | 83 +++++++++++++++++++ .../test_mcp_management_endpoints.py | 37 +++++++++ 2 files changed, 120 insertions(+) diff --git a/litellm/proxy/openapi_registry.json b/litellm/proxy/openapi_registry.json index d525b504a7b..19f46908855 100644 --- a/litellm/proxy/openapi_registry.json +++ b/litellm/proxy/openapi_registry.json @@ -92,6 +92,89 @@ { "name": "trash_message", "description": "Move a message to trash" } ] }, + { + "name": "google_sheets", + "title": "Google Sheets", + "description": "Read, write, and format data in Google Sheets spreadsheets", + "icon_url": "https://cdn.simpleicons.org/googlesheets", + "spec_url": "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/sheets/v4/openapi.yaml", + "oauth": { + "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth", + "token_url": "https://oauth2.googleapis.com/token", + "pkce": true, + "docs_url": "https://developers.google.com/sheets/api/guides/authorizing" + }, + "key_tools": [ + { "name": "create_spreadsheet", "description": "Create a new spreadsheet" }, + { "name": "get_spreadsheet", "description": "Get spreadsheet metadata and sheet properties" }, + { "name": "get_values", "description": "Read cell values from a range" }, + { "name": "update_values", "description": "Write cell values to a range" }, + { "name": "append_values", "description": "Append rows of values to a range" }, + { "name": "clear_values", "description": "Clear cell values in a range" }, + { "name": "batch_update", "description": "Apply batched formatting and structural updates" } + ] + }, + { + "name": "google_drive", + "title": "Google Drive", + "description": "List, read, upload, and manage files in Google Drive", + "icon_url": "https://cdn.simpleicons.org/googledrive", + "spec_url": "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/drive/v3/openapi.yaml", + "oauth": { + "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth", + "token_url": "https://oauth2.googleapis.com/token", + "pkce": true, + "docs_url": "https://developers.google.com/drive/api/guides/api-specific-auth" + }, + "key_tools": [ + { "name": "list_files", "description": "List and search files" }, + { "name": "get_file", "description": "Get file metadata" }, + { "name": "create_file", "description": "Create a file or folder" }, + { "name": "update_file", "description": "Update file metadata or content" }, + { "name": "copy_file", "description": "Copy a file" }, + { "name": "delete_file", "description": "Delete a file" }, + { "name": "list_permissions", "description": "List sharing permissions on a file" } + ] + }, + { + "name": "google_calendar", + "title": "Google Calendar", + "description": "Read and manage Google Calendar events and calendars", + "icon_url": "https://cdn.simpleicons.org/googlecalendar", + "spec_url": "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/calendar/v3/openapi.yaml", + "oauth": { + "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth", + "token_url": "https://oauth2.googleapis.com/token", + "pkce": true, + "docs_url": "https://developers.google.com/workspace/calendar/api/guides/auth" + }, + "key_tools": [ + { "name": "list_events", "description": "List events on a calendar" }, + { "name": "get_event", "description": "Get a single event" }, + { "name": "insert_event", "description": "Create an event" }, + { "name": "update_event", "description": "Update an event" }, + { "name": "delete_event", "description": "Delete an event" }, + { "name": "query_freebusy", "description": "Query free/busy availability" } + ] + }, + { + "name": "google_docs", + "title": "Google Docs", + "description": "Create, read, and edit Google Docs documents", + "icon_url": "https://cdn.simpleicons.org/googledocs", + "spec_url": "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/docs/v1/openapi.yaml", + "oauth": { + "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth", + "token_url": "https://oauth2.googleapis.com/token", + "pkce": true, + "docs_url": "https://developers.google.com/docs/api/how-tos/authorizing" + }, + "key_tools": [ + { "name": "create_document", "description": "Create a new document" }, + { "name": "get_document", "description": "Get a document's full content" }, + { "name": "batch_update_document", "description": "Apply batched edits to a document" } + ] + }, { "name": "stripe", "title": "Stripe", diff --git a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py index a669a277d2b..82992cd7ba6 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py @@ -5376,3 +5376,40 @@ async def test_edit_mcp_server_snapshot_failure_skips_purge_but_edit_succeeds(): assert result.server_id == server_id mock_purge.assert_not_awaited() + + +def test_bundled_openapi_registry_parses_and_entries_are_well_formed(): + """The OpenAPI quick-picker registry ships as a bundled JSON file; a malformed file or entry + silently degrades the picker to empty (the endpoint swallows load errors), so pin the file's + shape here: it must parse, and every entry needs the fields the create-form prefill reads. + OAuth-capable entries must carry both endpoint URLs; a catalog entry with a blank + authorization_url would recreate the exact 400 ("authorization url is not set") the catalog + exists to prevent for spec-only servers, which never run OAuth endpoint discovery.""" + import json + import os + + registry_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", "..", "..", "..", "litellm", "proxy", "openapi_registry.json", + ) + with open(registry_path) as f: + registry = json.load(f) + + apis = registry["apis"] + assert apis, "registry must not be empty" + names = [entry["name"] for entry in apis] + assert len(names) == len(set(names)), "duplicate registry entry names" + assert "google_sheets" in names, "LIT-4629: Google Sheets must be in the catalog" + + for entry in apis: + for required in ("name", "title", "description", "icon_url", "spec_url"): + assert entry.get(required), f"{entry.get('name')}: missing {required}" + assert entry["spec_url"].startswith("https://"), f"{entry['name']}: non-https spec_url" + oauth = entry.get("oauth") + if oauth is not None: + for required in ("authorization_url", "token_url"): + assert oauth.get(required, "").startswith("https://"), ( + f"{entry['name']}: oauth.{required} must be a non-empty https URL" + ) + for tool in entry.get("key_tools", []): + assert tool.get("name") and tool.get("description"), f"{entry['name']}: malformed key_tool" From 39cdfbdd28c202eef608d6e9fa5a343825556470 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Mon, 20 Jul 2026 19:18:42 -0700 Subject: [PATCH 3/3] test(mcp): pin all four Google registry entries in the shape test --- .../management_endpoints/test_mcp_management_endpoints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py index 82992cd7ba6..3e5bd3e9b7f 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py @@ -5399,7 +5399,8 @@ def test_bundled_openapi_registry_parses_and_entries_are_well_formed(): assert apis, "registry must not be empty" names = [entry["name"] for entry in apis] assert len(names) == len(set(names)), "duplicate registry entry names" - assert "google_sheets" in names, "LIT-4629: Google Sheets must be in the catalog" + for google_entry in ("google_sheets", "google_drive", "google_calendar", "google_docs"): + assert google_entry in names, f"LIT-4629: {google_entry} must be in the catalog" for entry in apis: for required in ("name", "title", "description", "icon_url", "spec_url"):