From ef2062cad306210d830fcff248764e32e9a9d5bf Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Fri, 3 Jul 2026 17:57:15 +0530 Subject: [PATCH 1/6] docs: fix parameter name mismatches in proxy endpoint docstrings - team_endpoints.py: ui_view_teams docstring described user_id / user_email search (copied from a user endpoint); it filters by team_id / team_alias and returns teams - internal_user_endpoints.py: `sso_ids` -> `sso_user_ids` in get_users - key_management_endpoints.py: delete_verification_tokens documented a stale `user_id` arg; replaced with the actual parameters --- .../proxy/management_endpoints/internal_user_endpoints.py | 4 ++-- .../proxy/management_endpoints/key_management_endpoints.py | 4 +++- litellm/proxy/management_endpoints/team_endpoints.py | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index d98dcf984b2..530d6fead89 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -1859,8 +1859,8 @@ async def get_users( - internal_user_viewer user_ids: Optional[str] Get list of users by user_ids. Comma separated list of user_ids. - sso_ids: Optional[str] - Get list of users by sso_ids. Comma separated list of sso_ids. + sso_user_ids: Optional[str] + Get list of users by sso_user_ids. Comma separated list of sso_user_ids. user_email: Optional[str] Filter users by partial email match team: Optional[str] diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index ade865a5378..e02cef8cb56 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -3902,7 +3902,9 @@ async def delete_verification_tokens( Args: tokens: List of tokens to delete - user_id: Optional user_id to filter by + user_api_key_cache: In-memory key cache to invalidate deleted tokens from + user_api_key_dict: User authentication information + litellm_changed_by: Optional username of the admin performing the change, for audit logs Returns: Tuple[Optional[Dict], List[LiteLLM_VerificationToken]]: diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 0ea2b9e05f9..850a526e1b0 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -4473,14 +4473,14 @@ async def ui_view_teams( [PROXY-ADMIN ONLY] Filter teams based on partial match of team_id or team_alias with pagination. Args: - user_id (Optional[str]): Partial user ID to search for - user_email (Optional[str]): Partial email to search for + team_id (Optional[str]): Partial team ID to search for + team_alias (Optional[str]): Partial team alias to search for page (int): Page number for pagination (starts at 1) page_size (int): Number of items per page (max 100) user_api_key_dict (UserAPIKeyAuth): User authentication information Returns: - List[LiteLLM_SpendLogs]: Paginated list of matching user records + List[LiteLLM_TeamTable]: Paginated list of matching team records """ from litellm.proxy.proxy_server import prisma_client From 868608ed4b147b3c9860f36296cae0872c163608 Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Wed, 22 Jul 2026 08:38:14 +0530 Subject: [PATCH 2/6] fix(core): normalize uppercase types inside a JSON Schema type list normalize_json_schema_types only lowercased `type` when it was a string, so a list of types - the standard way to mark a field nullable, e.g. ["STRING", "NULL"] - fell through to the generic list recursion, which returns bare strings untouched. Providers that emit uppercase types therefore kept them on every nullable field while sibling keys were normalized correctly: normalize_tool_schema({"function": {"parameters": { "type": "OBJECT", "properties": {"x": {"type": ["STRING", "NULL"]}}}}}) # -> parameters.type == "object" but x.type == ["STRING", "NULL"] Normalize each entry of a type list, leaving unrecognised entries alone. Adds tests for this module, which had none. --- .../json_validation_rule.py | 9 +++ .../test_json_validation_rule.py | 67 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/test_litellm/litellm_core_utils/test_json_validation_rule.py diff --git a/litellm/litellm_core_utils/json_validation_rule.py b/litellm/litellm_core_utils/json_validation_rule.py index c73b62f8a21..98901c433a3 100644 --- a/litellm/litellm_core_utils/json_validation_rule.py +++ b/litellm/litellm_core_utils/json_validation_rule.py @@ -52,6 +52,15 @@ def normalize_json_schema_types( for key, value in schema.items(): if key == "type" and isinstance(value, str) and value in type_mapping: normalized_schema[key] = type_mapping[value] + elif key == "type" and isinstance(value, list): + # JSON Schema also allows a list of types, which is the usual way + # to mark a field nullable (e.g. ["STRING", "NULL"]). Without this + # branch those entries fall through to the generic list recursion, + # which leaves the bare strings uppercase. + normalized_schema[key] = [ + type_mapping.get(entry, entry) if isinstance(entry, str) else entry + for entry in value + ] elif key == "properties" and isinstance(value, dict): # Recursively normalize properties normalized_schema[key] = { diff --git a/tests/test_litellm/litellm_core_utils/test_json_validation_rule.py b/tests/test_litellm/litellm_core_utils/test_json_validation_rule.py new file mode 100644 index 00000000000..5ff6bee986e --- /dev/null +++ b/tests/test_litellm/litellm_core_utils/test_json_validation_rule.py @@ -0,0 +1,67 @@ +from litellm.litellm_core_utils.json_validation_rule import ( + normalize_json_schema_types, + normalize_tool_schema, +) + + +def test_normalizes_a_plain_string_type(): + assert normalize_json_schema_types({"type": "STRING"}) == {"type": "string"} + + +def test_normalizes_a_list_of_types(): + """A list of types is how a nullable field is expressed. + + Regression: these entries fell through to the generic list recursion, which + returns bare strings untouched, so they stayed uppercase. + """ + assert normalize_json_schema_types({"type": ["STRING", "NULL"]}) == { + "type": ["string", "null"] + } + + +def test_normalizes_a_list_of_types_when_nested(): + schema = {"properties": {"a": {"type": ["INTEGER", "NULL"]}}} + + assert normalize_json_schema_types(schema) == { + "properties": {"a": {"type": ["integer", "null"]}} + } + + +def test_leaves_unknown_type_entries_alone(): + assert normalize_json_schema_types({"type": ["STRING", "custom"]}) == { + "type": ["string", "custom"] + } + + +def test_still_normalizes_properties_items_and_anyof(): + schema = { + "type": "OBJECT", + "properties": {"xs": {"type": "ARRAY", "items": {"type": "INTEGER"}}}, + "anyOf": [{"type": "STRING"}], + } + + assert normalize_json_schema_types(schema) == { + "type": "object", + "properties": {"xs": {"type": "array", "items": {"type": "integer"}}}, + "anyOf": [{"type": "string"}], + } + + +def test_tool_schema_normalizes_a_nullable_parameter(): + tool = { + "function": { + "parameters": { + "type": "OBJECT", + "properties": {"x": {"type": ["STRING", "NULL"]}}, + } + } + } + + assert normalize_tool_schema(tool) == { + "function": { + "parameters": { + "type": "object", + "properties": {"x": {"type": ["string", "null"]}}, + } + } + } From 2039c55e59ba0fecba32170cabf1f9703542d498 Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Sat, 1 Aug 2026 02:02:46 +0530 Subject: [PATCH 3/6] chore(ui): regenerate schema.d.ts for endpoint docstring fixes Sync the generated proxy API types with the corrected team-filter and list-users docstrings (npm run gen:api output). --- ui/litellm-dashboard/src/lib/http/schema.d.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index ddf2040cd04..07c9e78c227 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -13267,14 +13267,14 @@ export interface paths { * @description [PROXY-ADMIN ONLY] Filter teams based on partial match of team_id or team_alias with pagination. * * Args: - * user_id (Optional[str]): Partial user ID to search for - * user_email (Optional[str]): Partial email to search for + * team_id (Optional[str]): Partial team ID to search for + * team_alias (Optional[str]): Partial team alias to search for * page (int): Page number for pagination (starts at 1) * page_size (int): Number of items per page (max 100) * user_api_key_dict (UserAPIKeyAuth): User authentication information * * Returns: - * List[LiteLLM_SpendLogs]: Paginated list of matching user records + * List[LiteLLM_TeamTable]: Paginated list of matching team records */ get: operations["ui_view_teams_team_filter_ui_get"]; put?: never; @@ -14548,8 +14548,8 @@ export interface paths { * - internal_user_viewer * user_ids: Optional[str] * Get list of users by user_ids. Comma separated list of user_ids. - * sso_ids: Optional[str] - * Get list of users by sso_ids. Comma separated list of sso_ids. + * sso_user_ids: Optional[str] + * Get list of users by sso_user_ids. Comma separated list of sso_user_ids. * user_email: Optional[str] * Filter users by partial email match * team: Optional[str] From f1f2fff98018a819911ea97d50e4c0324bdf084b Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Mon, 3 Aug 2026 19:08:49 +0530 Subject: [PATCH 4/6] style: apply ruff format to json_validation_rule.py --- litellm/litellm_core_utils/json_validation_rule.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/json_validation_rule.py b/litellm/litellm_core_utils/json_validation_rule.py index 79f7df5eb41..861b0b9373a 100644 --- a/litellm/litellm_core_utils/json_validation_rule.py +++ b/litellm/litellm_core_utils/json_validation_rule.py @@ -58,8 +58,7 @@ def normalize_json_schema_types( # branch those entries fall through to the generic list recursion, # which leaves the bare strings uppercase. normalized_schema[key] = [ - type_mapping.get(entry, entry) if isinstance(entry, str) else entry - for entry in value + type_mapping.get(entry, entry) if isinstance(entry, str) else entry for entry in value ] elif key == "properties" and isinstance(value, dict): # Recursively normalize properties From affd76499ebb0ed8636b1ec6c3c17aedd96e70ee Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Mon, 3 Aug 2026 19:47:11 +0530 Subject: [PATCH 5/6] refactor: drop inline isinstance guard in type-list normalization JSON Schema type arrays contain only strings, so type_mapping.get(entry, entry) is sufficient and avoids an inline type guard. --- litellm/litellm_core_utils/json_validation_rule.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/litellm_core_utils/json_validation_rule.py b/litellm/litellm_core_utils/json_validation_rule.py index 861b0b9373a..32cb1eda357 100644 --- a/litellm/litellm_core_utils/json_validation_rule.py +++ b/litellm/litellm_core_utils/json_validation_rule.py @@ -57,9 +57,7 @@ def normalize_json_schema_types( # to mark a field nullable (e.g. ["STRING", "NULL"]). Without this # branch those entries fall through to the generic list recursion, # which leaves the bare strings uppercase. - normalized_schema[key] = [ - type_mapping.get(entry, entry) if isinstance(entry, str) else entry for entry in value - ] + normalized_schema[key] = [type_mapping.get(entry, entry) for entry in value] elif key == "properties" and isinstance(value, dict): # Recursively normalize properties normalized_schema[key] = { From 3db0c3215d90ee1744d125dcc7b6c89624bb6e3f Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Mon, 3 Aug 2026 20:22:03 +0530 Subject: [PATCH 6/6] fix(lint): annotate type-array list construction as mutable-ok The repo's type_discipline_gate (LIT002) flags the new list comprehension as a mutable-collection construction. A JSON Schema type array must round-trip as a JSON list, so a tuple would be wrong; annotate it with the sanctioned # mutable-ok reason instead. Verified with scripts/check_type_discipline.py (delta 0 vs base) and ruff format. --- .../json_validation_rule.py | 252 +++++++++--------- 1 file changed, 127 insertions(+), 125 deletions(-) diff --git a/litellm/litellm_core_utils/json_validation_rule.py b/litellm/litellm_core_utils/json_validation_rule.py index 32cb1eda357..5a03f635ce9 100644 --- a/litellm/litellm_core_utils/json_validation_rule.py +++ b/litellm/litellm_core_utils/json_validation_rule.py @@ -1,125 +1,127 @@ -import json -from typing import Any - -from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH - - -def normalize_json_schema_types( - schema: dict[str, Any] | list[Any] | Any, - depth: int = 0, - max_depth: int = DEFAULT_MAX_RECURSE_DEPTH, -) -> dict[str, Any] | list[Any] | Any: - """ - Normalize JSON schema types from uppercase to lowercase format. - - Some providers (like certain Google services) use uppercase types like 'BOOLEAN', 'STRING', 'ARRAY', 'OBJECT' - but standard JSON Schema requires lowercase: 'boolean', 'string', 'array', 'object' - - This function recursively normalizes all type fields in a schema to lowercase. - - Args: - schema: The schema to normalize (dict, list, or other) - depth: Current recursion depth - max_depth: Maximum recursion depth to prevent infinite loops - - Returns: - The normalized schema with lowercase types - """ - # Prevent infinite recursion - if depth >= max_depth: - return schema - - if not isinstance(schema, (dict, list)): - return schema - - # Type mapping from uppercase to lowercase - type_mapping = { - "BOOLEAN": "boolean", - "STRING": "string", - "ARRAY": "array", - "OBJECT": "object", - "NUMBER": "number", - "INTEGER": "integer", - "NULL": "null", - } - - if isinstance(schema, list): - return [normalize_json_schema_types(item, depth + 1, max_depth) for item in schema] - - if isinstance(schema, dict): - normalized_schema: dict[str, Any] = {} - - for key, value in schema.items(): - if key == "type" and isinstance(value, str) and value in type_mapping: - normalized_schema[key] = type_mapping[value] - elif key == "type" and isinstance(value, list): - # JSON Schema also allows a list of types, which is the usual way - # to mark a field nullable (e.g. ["STRING", "NULL"]). Without this - # branch those entries fall through to the generic list recursion, - # which leaves the bare strings uppercase. - normalized_schema[key] = [type_mapping.get(entry, entry) for entry in value] - elif key == "properties" and isinstance(value, dict): - # Recursively normalize properties - normalized_schema[key] = { - prop_key: normalize_json_schema_types(prop_value, depth + 1, max_depth) - for prop_key, prop_value in value.items() - } - elif key == "items" and isinstance(value, (dict, list)): - # Recursively normalize array items - normalized_schema[key] = normalize_json_schema_types(value, depth + 1, max_depth) - elif isinstance(value, (dict, list)): - # Recursively normalize any nested dict or list - normalized_schema[key] = normalize_json_schema_types(value, depth + 1, max_depth) - else: - normalized_schema[key] = value - - return normalized_schema - - return schema - - -def normalize_tool_schema(tool: dict[str, Any]) -> dict[str, Any]: - """ - Normalize a tool's parameter schema to use standard JSON Schema lowercase types. - - Args: - tool: The tool definition containing function parameters - - Returns: - The tool with normalized schema types - """ - if not isinstance(tool, dict): - return tool - - normalized_tool = tool.copy() - - # Normalize function parameters if present - if "function" in tool and isinstance(tool["function"], dict): - normalized_tool["function"] = tool["function"].copy() - if "parameters" in tool["function"]: - normalized_tool["function"]["parameters"] = normalize_json_schema_types(tool["function"]["parameters"]) - - return normalized_tool - - -def validate_schema(schema: dict, response: str): - """ - Validate if the returned json response follows the schema. - - Params: - - schema - dict: JSON schema - - response - str: Received json response as string. - """ - from jsonschema import ValidationError, validate - - from litellm import JSONSchemaValidationError - - try: - response_dict = json.loads(response) - except json.JSONDecodeError: - raise JSONSchemaValidationError(model="", llm_provider="", raw_response=response, schema=json.dumps(schema)) - - try: - validate(response_dict, schema=schema) - except ValidationError: - raise JSONSchemaValidationError(model="", llm_provider="", raw_response=response, schema=json.dumps(schema)) +import json +from typing import Any + +from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH + + +def normalize_json_schema_types( + schema: dict[str, Any] | list[Any] | Any, + depth: int = 0, + max_depth: int = DEFAULT_MAX_RECURSE_DEPTH, +) -> dict[str, Any] | list[Any] | Any: + """ + Normalize JSON schema types from uppercase to lowercase format. + + Some providers (like certain Google services) use uppercase types like 'BOOLEAN', 'STRING', 'ARRAY', 'OBJECT' + but standard JSON Schema requires lowercase: 'boolean', 'string', 'array', 'object' + + This function recursively normalizes all type fields in a schema to lowercase. + + Args: + schema: The schema to normalize (dict, list, or other) + depth: Current recursion depth + max_depth: Maximum recursion depth to prevent infinite loops + + Returns: + The normalized schema with lowercase types + """ + # Prevent infinite recursion + if depth >= max_depth: + return schema + + if not isinstance(schema, (dict, list)): + return schema + + # Type mapping from uppercase to lowercase + type_mapping = { + "BOOLEAN": "boolean", + "STRING": "string", + "ARRAY": "array", + "OBJECT": "object", + "NUMBER": "number", + "INTEGER": "integer", + "NULL": "null", + } + + if isinstance(schema, list): + return [normalize_json_schema_types(item, depth + 1, max_depth) for item in schema] + + if isinstance(schema, dict): + normalized_schema: dict[str, Any] = {} + + for key, value in schema.items(): + if key == "type" and isinstance(value, str) and value in type_mapping: + normalized_schema[key] = type_mapping[value] + elif key == "type" and isinstance(value, list): + # JSON Schema also allows a list of types, which is the usual way + # to mark a field nullable (e.g. ["STRING", "NULL"]). Without this + # branch those entries fall through to the generic list recursion, + # which leaves the bare strings uppercase. + normalized_schema[key] = [ # mutable-ok: a JSON Schema type array must round-trip as a JSON list + type_mapping.get(entry, entry) for entry in value + ] + elif key == "properties" and isinstance(value, dict): + # Recursively normalize properties + normalized_schema[key] = { + prop_key: normalize_json_schema_types(prop_value, depth + 1, max_depth) + for prop_key, prop_value in value.items() + } + elif key == "items" and isinstance(value, (dict, list)): + # Recursively normalize array items + normalized_schema[key] = normalize_json_schema_types(value, depth + 1, max_depth) + elif isinstance(value, (dict, list)): + # Recursively normalize any nested dict or list + normalized_schema[key] = normalize_json_schema_types(value, depth + 1, max_depth) + else: + normalized_schema[key] = value + + return normalized_schema + + return schema + + +def normalize_tool_schema(tool: dict[str, Any]) -> dict[str, Any]: + """ + Normalize a tool's parameter schema to use standard JSON Schema lowercase types. + + Args: + tool: The tool definition containing function parameters + + Returns: + The tool with normalized schema types + """ + if not isinstance(tool, dict): + return tool + + normalized_tool = tool.copy() + + # Normalize function parameters if present + if "function" in tool and isinstance(tool["function"], dict): + normalized_tool["function"] = tool["function"].copy() + if "parameters" in tool["function"]: + normalized_tool["function"]["parameters"] = normalize_json_schema_types(tool["function"]["parameters"]) + + return normalized_tool + + +def validate_schema(schema: dict, response: str): + """ + Validate if the returned json response follows the schema. + + Params: + - schema - dict: JSON schema + - response - str: Received json response as string. + """ + from jsonschema import ValidationError, validate + + from litellm import JSONSchemaValidationError + + try: + response_dict = json.loads(response) + except json.JSONDecodeError: + raise JSONSchemaValidationError(model="", llm_provider="", raw_response=response, schema=json.dumps(schema)) + + try: + validate(response_dict, schema=schema) + except ValidationError: + raise JSONSchemaValidationError(model="", llm_provider="", raw_response=response, schema=json.dumps(schema))