Skip to content

Commit 83c3f19

Browse files
authored
Drop async API stability warning (#127)
* mark some async types as internal implementation details * fix a Python 3.10 compatibility issue in the sync API timeout handling Closes #47
1 parent a96cfec commit 83c3f19

File tree

14 files changed

+90
-90
lines changed

14 files changed

+90
-90
lines changed

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ markers = [
9797
# Warnings should only be emitted when being specifically tested
9898
filterwarnings = [
9999
"error",
100-
"ignore:.*the async API is not yet stable:FutureWarning"
101100
]
102101
# Capture log info from network client libraries
103102
log_format = "%(asctime)s %(levelname)s %(message)s"

src/lmstudio/_ws_impl.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
import asyncio
1313

14-
from concurrent.futures import Future as SyncFuture
14+
15+
# Python 3.10 compatibility: use concurrent.futures.TimeoutError instead of the builtin
16+
# In 3.11+, these are the same type, in 3.10 futures have their own timeout exception
17+
from concurrent.futures import Future as SyncFuture, TimeoutError as SyncFutureTimeout
1518
from contextlib import AsyncExitStack, contextmanager
1619
from functools import partial
1720
from typing import (
@@ -47,6 +50,12 @@
4750
# and omits the generalised features that the SDK doesn't need)
4851
T = TypeVar("T")
4952

53+
__all__ = [
54+
"SyncFutureTimeout",
55+
"AsyncTaskManager",
56+
"AsyncWebsocketHandler",
57+
]
58+
5059

5160
class AsyncTaskManager:
5261
def __init__(self, *, on_activation: Callable[[], Any] | None = None) -> None:
@@ -429,7 +438,7 @@ def _rx_queue_get_threadsafe(self, rx_queue: RxQueue, timeout: float | None) ->
429438
future = self._task_manager.run_coroutine_threadsafe(rx_queue.get())
430439
try:
431440
return future.result(timeout)
432-
except TimeoutError:
441+
except SyncFutureTimeout:
433442
future.cancel()
434443
raise
435444

src/lmstudio/async_api.py

Lines changed: 41 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Async I/O protocol implementation for the LM Studio remote access API."""
22

33
import asyncio
4-
import warnings
54

65
from abc import abstractmethod
76
from contextlib import AsyncExitStack, asynccontextmanager
@@ -109,8 +108,8 @@
109108
# and similar tasks is published from `json_api`.
110109
# Bypassing the high level API, and working more
111110
# directly with the underlying websocket(s) is
112-
# supported (hence the public names), but they're
113-
# not exported via the top-level `lmstudio` API.
111+
# not supported due to the complexity of the task
112+
# management details (hence the private names).
114113
__all__ = [
115114
"AnyAsyncDownloadedModel",
116115
"AsyncClient",
@@ -215,7 +214,7 @@ async def receive_result(self) -> Any:
215214
return self._rpc.handle_rx_message(message)
216215

217216

218-
class AsyncLMStudioWebsocket(LMStudioWebsocket[AsyncWebSocketSession]):
217+
class _AsyncLMStudioWebsocket(LMStudioWebsocket[AsyncWebSocketSession]):
219218
"""Asynchronous websocket client that handles demultiplexing of reply messages."""
220219

221220
def __init__(
@@ -331,7 +330,7 @@ async def remote_call(
331330
return await rpc.receive_result()
332331

333332

334-
class AsyncSession(ClientSession["AsyncClient", AsyncLMStudioWebsocket]):
333+
class _AsyncSession(ClientSession["AsyncClient", _AsyncLMStudioWebsocket]):
335334
"""Async client session interfaces applicable to all API namespaces."""
336335

337336
def __init__(self, client: "AsyncClient") -> None:
@@ -354,7 +353,7 @@ async def __aexit__(self, *args: Any) -> None:
354353
await self.disconnect()
355354

356355
@sdk_public_api_async()
357-
async def connect(self) -> AsyncLMStudioWebsocket:
356+
async def connect(self) -> _AsyncLMStudioWebsocket:
358357
"""Connect the client session."""
359358
self._fail_if_connected("Attempted to connect already connected session")
360359
api_host = self._client.api_host
@@ -367,7 +366,7 @@ async def connect(self) -> AsyncLMStudioWebsocket:
367366
resources = self._resource_manager
368367
client = self._client
369368
self._lmsws = lmsws = await resources.enter_async_context(
370-
AsyncLMStudioWebsocket(
369+
_AsyncLMStudioWebsocket(
371370
client._task_manager, session_url, client._auth_details
372371
)
373372
)
@@ -411,7 +410,7 @@ async def remote_call(
411410

412411

413412
TAsyncSessionModel = TypeVar(
414-
"TAsyncSessionModel", bound="AsyncSessionModel[Any, Any, Any, Any]"
413+
"TAsyncSessionModel", bound="_AsyncSessionModel[Any, Any, Any, Any]"
415414
)
416415
TAsyncModelHandle = TypeVar("TAsyncModelHandle", bound="AsyncModelHandle[Any]")
417416

@@ -467,7 +466,7 @@ async def model(
467466
class AsyncDownloadedEmbeddingModel(
468467
AsyncDownloadedModel[
469468
EmbeddingModelInfo,
470-
"AsyncSessionEmbedding",
469+
"_AsyncSessionEmbedding",
471470
EmbeddingLoadModelConfig,
472471
EmbeddingLoadModelConfigDict,
473472
"AsyncEmbeddingModel",
@@ -476,7 +475,7 @@ class AsyncDownloadedEmbeddingModel(
476475
"""Asynchronous download listing for an embedding model."""
477476

478477
def __init__(
479-
self, model_info: DictObject, session: "AsyncSessionEmbedding"
478+
self, model_info: DictObject, session: "_AsyncSessionEmbedding"
480479
) -> None:
481480
"""Initialize downloaded embedding model details."""
482481
super().__init__(EmbeddingModelInfo, model_info, session)
@@ -485,23 +484,23 @@ def __init__(
485484
class AsyncDownloadedLlm(
486485
AsyncDownloadedModel[
487486
LlmInfo,
488-
"AsyncSessionLlm",
487+
"_AsyncSessionLlm",
489488
LlmLoadModelConfig,
490489
LlmLoadModelConfigDict,
491490
"AsyncLLM",
492491
]
493492
):
494493
"""Asynchronous ownload listing for an LLM."""
495494

496-
def __init__(self, model_info: DictObject, session: "AsyncSessionLlm") -> None:
495+
def __init__(self, model_info: DictObject, session: "_AsyncSessionLlm") -> None:
497496
"""Initialize downloaded embedding model details."""
498497
super().__init__(LlmInfo, model_info, session)
499498

500499

501500
AnyAsyncDownloadedModel: TypeAlias = AsyncDownloadedModel[Any, Any, Any, Any, Any]
502501

503502

504-
class AsyncSessionSystem(AsyncSession):
503+
class _AsyncSessionSystem(_AsyncSession):
505504
"""Async client session for the system namespace."""
506505

507506
API_NAMESPACE = "system"
@@ -531,7 +530,7 @@ def _process_download_listing(
531530
)
532531

533532

534-
class _AsyncSessionFiles(AsyncSession):
533+
class _AsyncSessionFiles(_AsyncSession):
535534
"""Async client session for the files namespace."""
536535

537536
API_NAMESPACE = "files"
@@ -562,7 +561,7 @@ async def prepare_image(
562561
return await self._fetch_file_handle(file_data)
563562

564563

565-
class AsyncModelDownloadOption(ModelDownloadOptionBase[AsyncSession]):
564+
class AsyncModelDownloadOption(ModelDownloadOptionBase[_AsyncSession]):
566565
"""A single download option for a model search result."""
567566

568567
@sdk_public_api_async()
@@ -577,10 +576,10 @@ async def download(
577576
return await channel.wait_for_result()
578577

579578

580-
class AsyncAvailableModel(AvailableModelBase[AsyncSession]):
579+
class AsyncAvailableModel(AvailableModelBase[_AsyncSession]):
581580
"""A model available for download from the model repository."""
582581

583-
_session: AsyncSession
582+
_session: _AsyncSession
584583

585584
@sdk_public_api_async()
586585
async def get_download_options(
@@ -595,7 +594,7 @@ async def get_download_options(
595594
return final
596595

597596

598-
class AsyncSessionRepository(AsyncSession):
597+
class _AsyncSessionRepository(_AsyncSession):
599598
"""Async client session for the repository namespace."""
600599

601600
API_NAMESPACE = "repository"
@@ -616,8 +615,8 @@ async def search_models(
616615
TAsyncDownloadedModel = TypeVar("TAsyncDownloadedModel", bound=AnyAsyncDownloadedModel)
617616

618617

619-
class AsyncSessionModel(
620-
AsyncSession,
618+
class _AsyncSessionModel(
619+
_AsyncSession,
621620
Generic[
622621
TAsyncModelHandle,
623622
TLoadConfig,
@@ -630,7 +629,7 @@ class AsyncSessionModel(
630629
_API_TYPES: Type[ModelSessionTypes[TLoadConfig]]
631630

632631
@property
633-
def _system_session(self) -> AsyncSessionSystem:
632+
def _system_session(self) -> _AsyncSessionSystem:
634633
return self._client.system
635634

636635
@property
@@ -922,8 +921,8 @@ async def cancel(self) -> None:
922921
await self._channel.cancel()
923922

924923

925-
class AsyncSessionLlm(
926-
AsyncSessionModel[
924+
class _AsyncSessionLlm(
925+
_AsyncSessionModel[
927926
"AsyncLLM",
928927
LlmLoadModelConfig,
929928
LlmLoadModelConfigDict,
@@ -1028,8 +1027,8 @@ async def _apply_prompt_template(
10281027
return response.get("formatted", "") if response else ""
10291028

10301029

1031-
class AsyncSessionEmbedding(
1032-
AsyncSessionModel[
1030+
class _AsyncSessionEmbedding(
1031+
_AsyncSessionModel[
10331032
"AsyncEmbeddingModel",
10341033
EmbeddingLoadModelConfig,
10351034
EmbeddingLoadModelConfigDict,
@@ -1115,7 +1114,7 @@ async def get_context_length(self) -> int:
11151114
AnyAsyncModel: TypeAlias = AsyncModelHandle[Any]
11161115

11171116

1118-
class AsyncLLM(AsyncModelHandle[AsyncSessionLlm]):
1117+
class AsyncLLM(AsyncModelHandle[_AsyncSessionLlm]):
11191118
"""Reference to a loaded LLM model."""
11201119

11211120
@sdk_public_api_async()
@@ -1258,7 +1257,7 @@ async def apply_prompt_template(
12581257
)
12591258

12601259

1261-
class AsyncEmbeddingModel(AsyncModelHandle[AsyncSessionEmbedding]):
1260+
class AsyncEmbeddingModel(AsyncModelHandle[_AsyncSessionEmbedding]):
12621261
"""Reference to a loaded embedding model."""
12631262

12641263
# Alas, type hints don't properly support distinguishing str vs Iterable[str]:
@@ -1271,24 +1270,17 @@ async def embed(
12711270
return await self._session._embed(self.identifier, input)
12721271

12731272

1274-
TAsyncSession = TypeVar("TAsyncSession", bound=AsyncSession)
1275-
1276-
_ASYNC_API_STABILITY_WARNING = """\
1277-
Note the async API is not yet stable and is expected to change in future releases
1278-
"""
1273+
TAsyncSession = TypeVar("TAsyncSession", bound=_AsyncSession)
12791274

12801275

12811276
class AsyncClient(ClientBase):
12821277
"""Async SDK client interface."""
12831278

12841279
def __init__(self, api_host: str | None = None) -> None:
12851280
"""Initialize API client."""
1286-
# Warn about the async API stability, since we expect it to change
1287-
# (in particular, accepting coroutine functions as callbacks)
1288-
warnings.warn(_ASYNC_API_STABILITY_WARNING, FutureWarning)
12891281
super().__init__(api_host)
12901282
self._resources = AsyncExitStack()
1291-
self._sessions: dict[str, AsyncSession] = {}
1283+
self._sessions: dict[str, _AsyncSession] = {}
12921284
self._task_manager = AsyncTaskManager()
12931285
# Unlike the sync API, we don't support GC-based resource
12941286
# management in the async API. Structured concurrency
@@ -1301,12 +1293,12 @@ def __init__(self, api_host: str | None = None) -> None:
13011293
# TODO: revisit lazy connections given the task manager implementation
13021294
# (for example, eagerly start tasks for all sessions, and lazily
13031295
# trigger events that allow them to initiate their connection)
1304-
_ALL_SESSIONS: tuple[Type[AsyncSession], ...] = (
1305-
AsyncSessionEmbedding,
1296+
_ALL_SESSIONS: tuple[Type[_AsyncSession], ...] = (
1297+
_AsyncSessionEmbedding,
13061298
_AsyncSessionFiles,
1307-
AsyncSessionLlm,
1308-
AsyncSessionRepository,
1309-
AsyncSessionSystem,
1299+
_AsyncSessionLlm,
1300+
_AsyncSessionRepository,
1301+
_AsyncSessionSystem,
13101302
)
13111303

13121304
async def __aenter__(self) -> Self:
@@ -1342,30 +1334,30 @@ def _get_session(self, cls: Type[TAsyncSession]) -> TAsyncSession:
13421334

13431335
@property
13441336
@sdk_public_api()
1345-
def llm(self) -> AsyncSessionLlm:
1337+
def llm(self) -> _AsyncSessionLlm:
13461338
"""Return the LLM API client session."""
1347-
return self._get_session(AsyncSessionLlm)
1339+
return self._get_session(_AsyncSessionLlm)
13481340

13491341
@property
13501342
@sdk_public_api()
1351-
def embedding(self) -> AsyncSessionEmbedding:
1343+
def embedding(self) -> _AsyncSessionEmbedding:
13521344
"""Return the embedding model API client session."""
1353-
return self._get_session(AsyncSessionEmbedding)
1345+
return self._get_session(_AsyncSessionEmbedding)
13541346

13551347
@property
1356-
def system(self) -> AsyncSessionSystem:
1348+
def system(self) -> _AsyncSessionSystem:
13571349
"""Return the system API client session."""
1358-
return self._get_session(AsyncSessionSystem)
1350+
return self._get_session(_AsyncSessionSystem)
13591351

13601352
@property
13611353
def files(self) -> _AsyncSessionFiles:
13621354
"""Return the files API client session."""
13631355
return self._get_session(_AsyncSessionFiles)
13641356

13651357
@property
1366-
def repository(self) -> AsyncSessionRepository:
1358+
def repository(self) -> _AsyncSessionRepository:
13671359
"""Return the repository API client session."""
1368-
return self._get_session(AsyncSessionRepository)
1360+
return self._get_session(_AsyncSessionRepository)
13691361

13701362
# Convenience methods
13711363
# Not yet implemented (server API only supports the same file types as prepare_image)

src/lmstudio/plugin/cli.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@ def main(argv: Sequence[str] | None = None) -> int:
4444
warnings.filterwarnings(
4545
"ignore", ".*the plugin API is not yet stable", FutureWarning
4646
)
47-
warnings.filterwarnings(
48-
"ignore", ".*the async API is not yet stable", FutureWarning
49-
)
5047
log_level = logging.DEBUG if args.debug else logging.INFO
5148
logging.basicConfig(level=log_level)
5249
if not args.dev:

src/lmstudio/plugin/hooks/common.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from anyio import move_on_after
2020

21-
from ...async_api import AsyncSession
21+
from ...async_api import _AsyncSession
2222
from ...schemas import DictObject
2323
from ..._sdk_models import (
2424
# TODO: Define aliases at schema generation time
@@ -32,13 +32,13 @@
3232

3333
# Available as lmstudio.plugin.hooks.*
3434
__all__ = [
35-
"AsyncSessionPlugins",
35+
"_AsyncSessionPlugins",
3636
"TPluginConfigSchema",
3737
"TGlobalConfigSchema",
3838
]
3939

4040

41-
class AsyncSessionPlugins(AsyncSession):
41+
class _AsyncSessionPlugins(_AsyncSession):
4242
"""Async client session for the plugins namespace."""
4343

4444
API_NAMESPACE = "plugins"
@@ -63,7 +63,7 @@ class HookController(Generic[TPluginRequest, TPluginConfigSchema, TGlobalConfigS
6363

6464
def __init__(
6565
self,
66-
session: AsyncSessionPlugins,
66+
session: _AsyncSessionPlugins,
6767
request: TPluginRequest,
6868
plugin_config_schema: type[TPluginConfigSchema],
6969
global_config_schema: type[TGlobalConfigSchema],

0 commit comments

Comments
 (0)