From 71ff9b566450af3fe10269a3d00277b7e71cf959 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 15 Aug 2025 18:09:32 -0400 Subject: [PATCH 1/2] refactor: optimize file handling to avoid redundant buffer conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change _get_file_details() to return Buffer instead of bytes - Add special handling for memoryview inputs to avoid unnecessary conversion - Support all buffer protocol objects (bytes, bytearray, memoryview, array.array, etc.) - Update type annotations throughout to use Buffer type from typing_extensions - Improve error messages to mention "buffer" instead of just "bytes" This change reduces memory overhead when working with buffer protocol objects by preserving memoryview objects instead of converting them to bytes unnecessarily. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/lmstudio/history.py | 44 ++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/lmstudio/history.py b/src/lmstudio/history.py index eb1cdaa..1c747df 100644 --- a/src/lmstudio/history.py +++ b/src/lmstudio/history.py @@ -18,6 +18,7 @@ Sequence, Tuple, TypeAlias, + Union, cast, get_args as get_typeform_args, runtime_checkable, @@ -27,6 +28,8 @@ Self, # Native in 3.13+ TypeIs, + # Native in Python 3.12+ + Buffer ) from msgspec import to_builtins @@ -529,31 +532,47 @@ def add_tool_result(self, result: ToolCallResultInput) -> ToolResultMessage: return message -LocalFileInput = BinaryIO | bytes | str | os.PathLike[str] +LocalFileInput = BinaryIO | Buffer | str | os.PathLike[str] # Private until file handle caching support is part of the published SDK API -def _get_file_details(src: LocalFileInput) -> Tuple[str, bytes]: +def _get_file_details(src: LocalFileInput) -> Tuple[str, Buffer]: """Read file contents as binary data and generate a suitable default name.""" - if isinstance(src, bytes): - # We process bytes as raw data, not a bytes filesystem path - data = src - name = str(uuid.uuid4()) - elif hasattr(src, "read"): + # Try to handle buffer protocol objects first (unless it's a path) + if not isinstance(src, (str, os.PathLike)) and not hasattr(src, "read"): try: - data = src.read() + # If already a memoryview, just use it directly + if isinstance(src, memoryview): + data: Buffer = src + else: + # Try to create a memoryview - this will work for any buffer protocol object + # including bytes, bytearray, array.array, numpy arrays, etc. + data = memoryview(src) + name = str(uuid.uuid4()) + return name, data + except TypeError: + # Not a buffer protocol object, fall through to other checks + pass + + if hasattr(src, "read"): + try: + data: Buffer = src.read() except OSError as exc: # Note: OSError details remain available via raised_exc.__context__ err_msg = f"Error while reading {src!r} ({exc!r})" raise LMStudioOSError(err_msg) from None name = getattr(src, "name", str(uuid.uuid4())) + # data is bytes here, which is a Buffer type + return name, data else: + # At this point, src must be a path-like object + src_path_input = cast(Union[str, os.PathLike[str]], src) try: - src_path = Path(src) + src_path = Path(src_path_input) except Exception as exc: - err_msg = f"Expected file-like object, filesystem path, or bytes ({exc!r})" + err_msg = f"Expected file-like object, filesystem path, or buffer ({exc!r})" raise LMStudioValueError(err_msg) from None try: data = src_path.read_bytes() @@ -562,7 +581,8 @@ def _get_file_details(src: LocalFileInput) -> Tuple[str, bytes]: err_msg = f"Error while reading {str(src_path)!r} ({exc!r})" raise LMStudioOSError(err_msg) from None name = str(src_path.name) - return name, data + # data is bytes here, which is a Buffer type + return name, data _ContentHash: TypeAlias = bytes @@ -573,7 +593,7 @@ class _LocalFileData: """Local file data to be added to a chat history.""" name: str - raw_data: bytes + raw_data: Buffer def __init__(self, src: LocalFileInput, name: str | None = None) -> None: default_name, raw_data = _get_file_details(src) From b7bed1e090ba791c4249f8467f995f9d059243e5 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 15 Aug 2025 19:30:53 -0400 Subject: [PATCH 2/2] Address Linter issues. --- src/lmstudio/history.py | 6 +++--- tox.ini | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lmstudio/history.py b/src/lmstudio/history.py index 1c747df..640af17 100644 --- a/src/lmstudio/history.py +++ b/src/lmstudio/history.py @@ -29,7 +29,7 @@ # Native in 3.13+ TypeIs, # Native in Python 3.12+ - Buffer + Buffer, ) from msgspec import to_builtins @@ -555,10 +555,10 @@ def _get_file_details(src: LocalFileInput) -> Tuple[str, Buffer]: except TypeError: # Not a buffer protocol object, fall through to other checks pass - + if hasattr(src, "read"): try: - data: Buffer = src.read() + data = src.read() except OSError as exc: # Note: OSError details remain available via raised_exc.__context__ err_msg = f"Error while reading {src!r} ({exc!r})" diff --git a/tox.ini b/tox.ini index d605fba..7e911c6 100644 --- a/tox.ini +++ b/tox.ini @@ -52,6 +52,7 @@ commands = ruff check {posargs} src/ tests/ examples/plugins [testenv:typecheck] +groups = dev allowlist_externals = mypy commands = mypy --strict {posargs} src/ tests/