Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions src/lmstudio/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
Sequence,
Tuple,
TypeAlias,
Union,
cast,
get_args as get_typeform_args,
runtime_checkable,
Expand All @@ -27,6 +28,8 @@
Self,
# Native in 3.13+
TypeIs,
# Native in Python 3.12+
Buffer,
)

from msgspec import to_builtins
Expand Down Expand Up @@ -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:
# 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 = 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()
Expand All @@ -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
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
Loading