mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(prompt_templates): keep moved private helpers importable from common_utils (#50721)
* fix(prompt_templates): keep moved private helpers importable from common_utils The split moved 83 private helpers out of common_utils, so code importing them by their old path got ImportError. A module __getattr__ now serves each one from its new module. Also renames the new test files to the directory's naming convention * fix(prompt_templates): hide the old-path getattr from type checkers and serve only released names Type checkers stop seeing the module __getattr__, so a typo'd import from common_utils fails lint again. The old path now serves only the 69 moved helpers that common_utils had in a tagged release, since the other 14 were never importable from it * fix(prompt_templates): let patches through the old common_utils path reach moved helpers and warn on old-path use * fix(prompt_templates): sync old-path patches of re-exported helpers and restore moved helpers when warnings are errors * fix(prompt_templates): reset a moved helper on old-path delete instead of removing it, and warn on old-path writes --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
6162ebab85
commit
1c807aef19
9 changed files with 361 additions and 1 deletions
|
|
@ -2,10 +2,14 @@
|
|||
Common utility functions used for translating messages across providers
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import sys
|
||||
import warnings
|
||||
from collections.abc import Hashable, Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from itertools import chain, groupby
|
||||
from types import MappingProxyType, ModuleType
|
||||
from typing import TYPE_CHECKING, Final, Literal, TypeAlias, cast
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
|
@ -222,6 +226,108 @@ _extract_reasoning_content: Final = extract_reasoning_content
|
|||
_get_image_mime_type_from_url: Final = get_image_mime_type_from_url
|
||||
_parse_content_for_reasoning: Final = parse_content_for_reasoning
|
||||
|
||||
_MOVED_PRIVATE_NAME_MODULES: Final[Mapping[str, str]] = MappingProxyType(
|
||||
{
|
||||
**dict.fromkeys(
|
||||
("_decode_vector_store_ids_in_tools",),
|
||||
"file_id_utils",
|
||||
),
|
||||
**dict.fromkeys(
|
||||
(
|
||||
"_EMPTY_SCHEMA",
|
||||
"_LOCAL_SCHEMA_REF_PREFIXES",
|
||||
"_MAX_SCHEMA_FLATTEN_DEPTH",
|
||||
"_MAX_SCHEMA_NESTING",
|
||||
"_OPENAI_REJECTED_TOP_LEVEL_SCHEMA_KEYS",
|
||||
"_REGEX_LOOKAROUND_RE",
|
||||
"_SCHEMA_LIST",
|
||||
"_SUBSCHEMA_KEYWORDS",
|
||||
"_SUBSCHEMA_LIST_KEYWORDS",
|
||||
"_SUBSCHEMA_MAP_KEYWORDS",
|
||||
"_TOP_LEVEL_SCHEMA_COMBINATORS",
|
||||
"_any_of",
|
||||
"_combinator_required_names",
|
||||
"_dropped_pattern_properties",
|
||||
"_flatten_schema_against_root",
|
||||
"_grammar_branches",
|
||||
"_is_not_python_regex",
|
||||
"_is_object_schema",
|
||||
"_is_open_object",
|
||||
"_is_union",
|
||||
"_keyword_value_rebuilt",
|
||||
"_list_at",
|
||||
"_may_be_string",
|
||||
"_mergeable_branch",
|
||||
"_named_schemas",
|
||||
"_node_without_rejected_regex",
|
||||
"_optional_property_names",
|
||||
"_property_schemas",
|
||||
"_refs_in",
|
||||
"_resolve_local_schema_ref",
|
||||
"_root_definitions",
|
||||
"_schema_at",
|
||||
"_schema_branches",
|
||||
"_schema_levels",
|
||||
"_schema_nodes",
|
||||
"_schema_properties",
|
||||
"_schema_required_names",
|
||||
"_schema_without_rejected_regex",
|
||||
"_subschemas",
|
||||
"_uses_regex_lookaround",
|
||||
),
|
||||
"json_schema_utils",
|
||||
),
|
||||
**dict.fromkeys(
|
||||
(
|
||||
"_BREAKPOINT_BLOCKS",
|
||||
"_BREAKPOINT_BLOCK_LIST_KEYS",
|
||||
"_BREAKPOINT_FIELDS",
|
||||
"_MarkedT",
|
||||
"_PROMPT_CACHE_BREAKPOINT",
|
||||
"_breakpoints_stripped_from",
|
||||
"_without_prompt_cache_breakpoint",
|
||||
),
|
||||
"prompt_cache_breakpoint_utils",
|
||||
),
|
||||
**dict.fromkeys(
|
||||
(
|
||||
"_carries_encrypted_reasoning",
|
||||
"_converse_wrapped_payload",
|
||||
"_encrypted_reasoning_field",
|
||||
"_provider_text_or_none",
|
||||
"_readable_thinking_text",
|
||||
"_reasoning_item_from_block_group",
|
||||
"_reasoning_replay_group_key",
|
||||
"_strip_encrypted_reasoning_from_blocks",
|
||||
),
|
||||
"reasoning_utils",
|
||||
),
|
||||
**dict.fromkeys(
|
||||
(
|
||||
"_DECODED_JSON",
|
||||
"_attempt_json_repair",
|
||||
),
|
||||
"tool_call_json_repair",
|
||||
),
|
||||
**dict.fromkeys(
|
||||
(
|
||||
"_TPMMediaProjection",
|
||||
"_tpm_blank_fields",
|
||||
"_tpm_code_output_media",
|
||||
"_tpm_document_media",
|
||||
"_tpm_file_media",
|
||||
"_tpm_image_part",
|
||||
"_tpm_input_media",
|
||||
"_tpm_media_leaf",
|
||||
"_tpm_media_list",
|
||||
"_tpm_replace_field",
|
||||
"_tpm_tool_result_media",
|
||||
),
|
||||
"tpm_media_utils",
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LoggingClass
|
||||
|
||||
|
|
@ -1578,3 +1684,72 @@ def invalid_type_message(field_path: str, value: object) -> str:
|
|||
|
||||
def is_unhashable_value_behind(error: TypeError, value: object) -> bool:
|
||||
return not isinstance(value, Hashable) and f"unhashable type: '{type(value).__name__}'" in str(error)
|
||||
|
||||
|
||||
_UNBOUND: Final = object()
|
||||
|
||||
|
||||
def _module(module_name: str) -> ModuleType:
|
||||
return importlib.import_module(f"litellm.litellm_core_utils.prompt_templates.{module_name}")
|
||||
|
||||
|
||||
def _namespace(module: ModuleType) -> Mapping[str, object]:
|
||||
return vars(module)
|
||||
|
||||
|
||||
def _moved_name_home(name: str) -> ModuleType:
|
||||
module_name: Final = _MOVED_PRIVATE_NAME_MODULES.get(name)
|
||||
if module_name is None:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
return _module(module_name)
|
||||
|
||||
|
||||
def _split_modules_sharing(name: str, value: object) -> tuple[ModuleType, ...]:
|
||||
if value is _UNBOUND:
|
||||
return ()
|
||||
split_modules: Final = (_module(module_name) for module_name in frozenset(_MOVED_PRIVATE_NAME_MODULES.values()))
|
||||
return tuple(module for module in split_modules if _namespace(module).get(name, _UNBOUND) is value)
|
||||
|
||||
|
||||
def _warn_old_path(name: str, home: ModuleType) -> None:
|
||||
warnings.warn(
|
||||
f"{__name__}.{name} moved to {home.__name__}, import or patch it there. "
|
||||
"The old path will be removed in a future release",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
|
||||
_RELEASED_MOVED_VALUES: Final = MappingProxyType(
|
||||
{name: _namespace(_moved_name_home(name))[name] for name in _MOVED_PRIVATE_NAME_MODULES}
|
||||
)
|
||||
|
||||
|
||||
class _CommonUtilsModule(ModuleType):
|
||||
def __getattr__(self, name: str) -> object:
|
||||
home: Final = _moved_name_home(name)
|
||||
value: Final = _namespace(home).get(name, _UNBOUND)
|
||||
if value is _UNBOUND:
|
||||
raise AttributeError(f"module {home.__name__!r} has no attribute {name!r}")
|
||||
_warn_old_path(name, home)
|
||||
return value
|
||||
|
||||
def __setattr__(self, name: str, value: object) -> None:
|
||||
if name in _MOVED_PRIVATE_NAME_MODULES:
|
||||
home: Final = _moved_name_home(name)
|
||||
_warn_old_path(name, home)
|
||||
setattr(home, name, value)
|
||||
return
|
||||
for module in _split_modules_sharing(name, _namespace(self).get(name, _UNBOUND)):
|
||||
setattr(module, name, value)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def __delattr__(self, name: str) -> None:
|
||||
released: Final = _RELEASED_MOVED_VALUES.get(name, _UNBOUND)
|
||||
if released is _UNBOUND:
|
||||
super().__delattr__(name)
|
||||
return
|
||||
setattr(_moved_name_home(name), name, released)
|
||||
|
||||
|
||||
sys.modules[__name__].__class__ = _CommonUtilsModule
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import contextlib
|
||||
import copy
|
||||
import importlib
|
||||
import json
|
||||
import warnings
|
||||
from collections.abc import Mapping
|
||||
from typing import Final, cast
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.litellm_core_utils.prompt_templates import common_utils
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
TOOL_RESULT_IMAGE_BOUNDARY,
|
||||
TOOL_RESULT_IMAGE_PLACEHOLDER,
|
||||
|
|
@ -21,6 +26,10 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
strip_litellm_response_only_fields,
|
||||
system_messages_first,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.json_schema_utils import (
|
||||
flatten_combinators_and_drop_non_python_regex_patterns,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.tool_call_json_repair import parse_tool_call_arguments
|
||||
from litellm.types.llms.openai import ChatCompletionAudioObject, ChatCompletionFileObject, ChatCompletionImageObject
|
||||
from litellm.types.utils import ChatCompletionMessageToolCall, Choices, Function, Message, ModelResponse
|
||||
|
||||
|
|
@ -987,6 +996,180 @@ def test_content_list_flattening_returns_new_messages_and_leaves_the_callers_unt
|
|||
assert messages == messages_before
|
||||
|
||||
|
||||
_MOVED_PRIVATE_NAMES_BY_MODULE: Final = (
|
||||
(
|
||||
"file_id_utils",
|
||||
("_decode_vector_store_ids_in_tools",),
|
||||
),
|
||||
(
|
||||
"json_schema_utils",
|
||||
(
|
||||
"_EMPTY_SCHEMA",
|
||||
"_LOCAL_SCHEMA_REF_PREFIXES",
|
||||
"_MAX_SCHEMA_FLATTEN_DEPTH",
|
||||
"_MAX_SCHEMA_NESTING",
|
||||
"_OPENAI_REJECTED_TOP_LEVEL_SCHEMA_KEYS",
|
||||
"_REGEX_LOOKAROUND_RE",
|
||||
"_SCHEMA_LIST",
|
||||
"_SUBSCHEMA_KEYWORDS",
|
||||
"_SUBSCHEMA_LIST_KEYWORDS",
|
||||
"_SUBSCHEMA_MAP_KEYWORDS",
|
||||
"_TOP_LEVEL_SCHEMA_COMBINATORS",
|
||||
"_any_of",
|
||||
"_combinator_required_names",
|
||||
"_dropped_pattern_properties",
|
||||
"_flatten_schema_against_root",
|
||||
"_grammar_branches",
|
||||
"_is_not_python_regex",
|
||||
"_is_object_schema",
|
||||
"_is_open_object",
|
||||
"_is_union",
|
||||
"_keyword_value_rebuilt",
|
||||
"_list_at",
|
||||
"_may_be_string",
|
||||
"_mergeable_branch",
|
||||
"_named_schemas",
|
||||
"_node_without_rejected_regex",
|
||||
"_optional_property_names",
|
||||
"_property_schemas",
|
||||
"_refs_in",
|
||||
"_resolve_local_schema_ref",
|
||||
"_root_definitions",
|
||||
"_schema_at",
|
||||
"_schema_branches",
|
||||
"_schema_levels",
|
||||
"_schema_nodes",
|
||||
"_schema_properties",
|
||||
"_schema_required_names",
|
||||
"_schema_without_rejected_regex",
|
||||
"_subschemas",
|
||||
"_uses_regex_lookaround",
|
||||
),
|
||||
),
|
||||
(
|
||||
"prompt_cache_breakpoint_utils",
|
||||
(
|
||||
"_BREAKPOINT_BLOCKS",
|
||||
"_BREAKPOINT_BLOCK_LIST_KEYS",
|
||||
"_BREAKPOINT_FIELDS",
|
||||
"_MarkedT",
|
||||
"_PROMPT_CACHE_BREAKPOINT",
|
||||
"_breakpoints_stripped_from",
|
||||
"_without_prompt_cache_breakpoint",
|
||||
),
|
||||
),
|
||||
(
|
||||
"reasoning_utils",
|
||||
(
|
||||
"_carries_encrypted_reasoning",
|
||||
"_converse_wrapped_payload",
|
||||
"_encrypted_reasoning_field",
|
||||
"_provider_text_or_none",
|
||||
"_readable_thinking_text",
|
||||
"_reasoning_item_from_block_group",
|
||||
"_reasoning_replay_group_key",
|
||||
"_strip_encrypted_reasoning_from_blocks",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tool_call_json_repair",
|
||||
(
|
||||
"_DECODED_JSON",
|
||||
"_attempt_json_repair",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tpm_media_utils",
|
||||
(
|
||||
"_TPMMediaProjection",
|
||||
"_tpm_blank_fields",
|
||||
"_tpm_code_output_media",
|
||||
"_tpm_document_media",
|
||||
"_tpm_file_media",
|
||||
"_tpm_image_part",
|
||||
"_tpm_input_media",
|
||||
"_tpm_media_leaf",
|
||||
"_tpm_media_list",
|
||||
"_tpm_replace_field",
|
||||
"_tpm_tool_result_media",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("module_name", "names"), _MOVED_PRIVATE_NAMES_BY_MODULE)
|
||||
def test_moved_private_helpers_still_import_from_common_utils(module_name: str, names: tuple[str, ...]) -> None:
|
||||
new_home: Final = importlib.import_module(f"litellm.litellm_core_utils.prompt_templates.{module_name}")
|
||||
unreachable: Final = [name for name in names if getattr(common_utils, name, None) is not getattr(new_home, name)]
|
||||
assert unreachable == []
|
||||
|
||||
|
||||
def test_unknown_private_name_still_raises_attribute_error_on_common_utils() -> None:
|
||||
missing_name: Final = "_not_a_moved_helper"
|
||||
with pytest.raises(AttributeError, match=missing_name):
|
||||
getattr(common_utils, missing_name)
|
||||
|
||||
|
||||
def test_old_path_access_warns_from_the_callers_line_with_the_new_home() -> None:
|
||||
moved_name: Final = "_any_of"
|
||||
with pytest.warns(DeprecationWarning, match=r"common_utils\._any_of moved to .*\.json_schema_utils") as caught:
|
||||
getattr(common_utils, moved_name)
|
||||
assert [warning.filename for warning in caught] == [__file__]
|
||||
|
||||
|
||||
def test_patching_a_moved_helper_at_its_old_path_reaches_the_callers_in_its_new_home() -> None:
|
||||
patched_result: Final = {"patched": True}
|
||||
with mock.patch(
|
||||
"litellm.litellm_core_utils.prompt_templates.common_utils._attempt_json_repair", return_value=patched_result
|
||||
):
|
||||
assert parse_tool_call_arguments('{"a": 1') == patched_result
|
||||
assert parse_tool_call_arguments('{"a": 1') == {"a": 1}
|
||||
|
||||
|
||||
def test_patching_a_re_exported_helper_at_its_old_path_reaches_the_callers_in_its_new_home() -> None:
|
||||
schema: Final = {"anyOf": [{"type": "object", "properties": {"a": {"type": "string"}}}]}
|
||||
unpatched: Final = flatten_combinators_and_drop_non_python_regex_patterns(schema)
|
||||
patched_result: Final = {"patched": True}
|
||||
with mock.patch(
|
||||
"litellm.litellm_core_utils.prompt_templates.common_utils.flatten_top_level_schema_combinators",
|
||||
return_value=patched_result,
|
||||
):
|
||||
assert flatten_combinators_and_drop_non_python_regex_patterns(schema) == patched_result
|
||||
assert flatten_combinators_and_drop_non_python_regex_patterns(schema) == unpatched
|
||||
|
||||
|
||||
def test_old_path_patch_restores_the_moved_helper_when_warnings_become_errors_inside_it() -> None:
|
||||
with warnings.catch_warnings(), contextlib.suppress(DeprecationWarning):
|
||||
with mock.patch(
|
||||
"litellm.litellm_core_utils.prompt_templates.common_utils._attempt_json_repair", return_value=None
|
||||
):
|
||||
warnings.simplefilter("error", DeprecationWarning)
|
||||
assert parse_tool_call_arguments('{"a": 1') == {"a": 1}
|
||||
|
||||
|
||||
def test_old_path_patch_with_create_leaves_the_moved_helper_in_place() -> None:
|
||||
with mock.patch(
|
||||
"litellm.litellm_core_utils.prompt_templates.common_utils._attempt_json_repair", return_value=None, create=True
|
||||
):
|
||||
assert parse_tool_call_arguments("{}") == {}
|
||||
assert parse_tool_call_arguments('{"a": 1') == {"a": 1}
|
||||
|
||||
|
||||
def test_overriding_a_moved_helper_through_the_old_path_warns_and_deleting_it_restores_the_released_one() -> None:
|
||||
moved_name: Final = "_attempt_json_repair"
|
||||
with pytest.warns(DeprecationWarning, match=r"common_utils\._attempt_json_repair moved to .*\.tool_call_json_repair"):
|
||||
setattr(common_utils, moved_name, mock.Mock(return_value={"overridden": True}))
|
||||
assert parse_tool_call_arguments('{"a": 1') == {"overridden": True}
|
||||
delattr(common_utils, moved_name)
|
||||
assert parse_tool_call_arguments('{"a": 1') == {"a": 1}
|
||||
|
||||
|
||||
def test_patching_a_name_common_utils_still_defines_stays_on_common_utils() -> None:
|
||||
with mock.patch.object(common_utils, "invalid_type_message", return_value="patched"):
|
||||
assert common_utils.invalid_type_message("field", 1) == "patched"
|
||||
assert common_utils.invalid_type_message("field", 1) != "patched"
|
||||
|
||||
|
||||
def test_get_content_from_model_response_includes_server_tool_calls_before_client_calls() -> None:
|
||||
server_call: Final = ChatCompletionMessageToolCall(
|
||||
id="srvtoolu_01", type="function", function=Function(name="web_search", arguments='{"query": "persimmon"}')
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ Regression tests for:
|
|||
- litellm/llms/vertex_ai/gemini/transformation.py
|
||||
- litellm/llms/gemini/chat/transformation.py
|
||||
- litellm/litellm_core_utils/prompt_templates/common_utils.py
|
||||
(migrate_file_to_image_url raises on missing `file`; file-id helpers skip non-OpenAI shapes)
|
||||
(migrate_file_to_image_url raises on missing `file`)
|
||||
- litellm/litellm_core_utils/prompt_templates/file_id_utils.py
|
||||
(file-id helpers skip non-OpenAI shapes)
|
||||
- litellm/litellm_core_utils/prompt_templates/factory.py (Bedrock + Anthropic)
|
||||
- litellm/llms/openai/chat/gpt_transformation.py
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue