Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/remaining-table-complexity-053477

This commit is contained in:
Yuneng Jiang 2026-07-20 20:08:50 -07:00
commit fe57eedb5c
No known key found for this signature in database
4 changed files with 175 additions and 0 deletions

View file

@ -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",

View file

@ -280,6 +280,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]]

View file

@ -5376,3 +5376,41 @@ 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"
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"):
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"

View file

@ -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