From 9db319fdcbb6dcfb4a0e10cc34ebcea284ce2772 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 15 Aug 2025 21:19:13 -0400 Subject: [PATCH 1/5] refactor: optimize file handling to avoid redundant buffer conversions - 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. --- 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 ff6e76af662d8b8537292d23711856f75da5d33f Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 15 Aug 2025 21:21:18 -0400 Subject: [PATCH 2/5] fix: address linter and type checker issues - Fix trailing comma in Buffer import - Remove redundant type annotation to fix mypy error - Add groups = dev to typecheck tox environment - Remove trailing whitespace --- 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/ From 40dc51194029989744b3dbd37f6e4061c87a0fee Mon Sep 17 00:00:00 2001 From: ArEnSc <6252325+ArEnSc@users.noreply.github.com> Date: Mon, 18 Aug 2025 09:34:59 -0400 Subject: [PATCH 3/5] Update src/lmstudio/history.py Co-authored-by: Alyssa Coghlan --- src/lmstudio/history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lmstudio/history.py b/src/lmstudio/history.py index 640af17..1414686 100644 --- a/src/lmstudio/history.py +++ b/src/lmstudio/history.py @@ -26,10 +26,10 @@ from typing_extensions import ( # Native in 3.11+ Self, - # Native in 3.13+ - TypeIs, # Native in Python 3.12+ Buffer, + # Native in 3.13+ + TypeIs, ) from msgspec import to_builtins From 9af61ca0b0c1796bb7e5bfc05b66d1a4106b24c2 Mon Sep 17 00:00:00 2001 From: ArEnSc <6252325+ArEnSc@users.noreply.github.com> Date: Mon, 18 Aug 2025 09:35:15 -0400 Subject: [PATCH 4/5] Update tox.ini Co-authored-by: Alyssa Coghlan --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7e911c6..d605fba 100644 --- a/tox.ini +++ b/tox.ini @@ -52,7 +52,6 @@ commands = ruff check {posargs} src/ tests/ examples/plugins [testenv:typecheck] -groups = dev allowlist_externals = mypy commands = mypy --strict {posargs} src/ tests/ From ff0c26c29193cb93c3a7e788e3f73ec38d0f7f15 Mon Sep 17 00:00:00 2001 From: Alyssa Coghlan Date: Tue, 19 Aug 2025 01:24:44 +1000 Subject: [PATCH 5/5] Refactor file data format determination --- src/lmstudio/history.py | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/lmstudio/history.py b/src/lmstudio/history.py index 1414686..5fe7bd9 100644 --- a/src/lmstudio/history.py +++ b/src/lmstudio/history.py @@ -18,7 +18,6 @@ Sequence, Tuple, TypeAlias, - Union, cast, get_args as get_typeform_args, runtime_checkable, @@ -540,23 +539,18 @@ def add_tool_result(self, result: ToolCallResultInput) -> ToolResultMessage: def _get_file_details(src: LocalFileInput) -> Tuple[str, Buffer]: """Read file contents as binary data and generate a suitable default name.""" - # 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: Buffer + if isinstance(src, Buffer): + if isinstance(src, memoryview): # 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"): + data = 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) + # Received raw file data without any name information + name = str(uuid.uuid4()) + elif hasattr(src, "read"): try: data = src.read() except OSError as exc: @@ -564,15 +558,14 @@ def _get_file_details(src: LocalFileInput) -> Tuple[str, Buffer]: 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_path_input) + src_path = Path(src) except Exception as exc: - err_msg = f"Expected file-like object, filesystem path, or buffer ({exc!r})" + err_msg = ( + f"Expected file-like object, filesystem path, or data buffer ({exc!r})" + ) raise LMStudioValueError(err_msg) from None try: data = src_path.read_bytes() @@ -581,8 +574,7 @@ def _get_file_details(src: LocalFileInput) -> Tuple[str, Buffer]: err_msg = f"Error while reading {str(src_path)!r} ({exc!r})" raise LMStudioOSError(err_msg) from None name = str(src_path.name) - # data is bytes here, which is a Buffer type - return name, data + return name, data _ContentHash: TypeAlias = bytes