3
3
import asyncio
4
4
import logging
5
5
6
+ from contextlib import suppress
7
+
6
8
import pytest
7
9
from pytest import LogCaptureFixture as LogCap
8
10
from pytest_subtests import SubTests
13
15
from ..support import (
14
16
LLM_LOAD_CONFIG ,
15
17
EXPECTED_LLM ,
16
- EXPECTED_LLM_DEFAULT_ID ,
17
18
EXPECTED_LLM_ID ,
18
19
EXPECTED_EMBEDDING ,
19
- EXPECTED_EMBEDDING_DEFAULT_ID ,
20
20
EXPECTED_EMBEDDING_ID ,
21
21
EXPECTED_VLM_ID ,
22
+ SMALL_LLM_ID ,
22
23
TOOL_LLM_ID ,
23
24
check_sdk_error ,
24
25
)
@@ -291,16 +292,17 @@ async def test_get_or_load_when_unloaded_llm_async(caplog: LogCap) -> None:
291
292
caplog .set_level (logging .DEBUG )
292
293
async with AsyncClient () as client :
293
294
llm = client .llm
294
- await llm .unload (EXPECTED_LLM_ID )
295
- model = await llm .model (EXPECTED_LLM_DEFAULT_ID , config = LLM_LOAD_CONFIG )
296
- assert model .identifier == EXPECTED_LLM_DEFAULT_ID
295
+ with suppress (LMStudioModelNotFoundError ):
296
+ await llm .unload (EXPECTED_LLM_ID )
297
+ model = await llm .model (EXPECTED_LLM_ID , config = LLM_LOAD_CONFIG )
298
+ assert model .identifier == EXPECTED_LLM_ID
297
299
# LM Studio may default to JIT handling for models loaded with `getOrLoad`,
298
300
# so ensure we restore a regular non-JIT instance with no TTL set
299
- await llm .unload (EXPECTED_LLM_ID )
301
+ await model .unload ()
300
302
model = await llm .load_new_instance (
301
- EXPECTED_LLM_DEFAULT_ID , config = LLM_LOAD_CONFIG , ttl = None
303
+ EXPECTED_LLM_ID , config = LLM_LOAD_CONFIG , ttl = None
302
304
)
303
- assert model .identifier == EXPECTED_LLM_DEFAULT_ID
305
+ assert model .identifier == EXPECTED_LLM_ID
304
306
305
307
306
308
@pytest .mark .asyncio
@@ -310,13 +312,83 @@ async def test_get_or_load_when_unloaded_embedding_async(caplog: LogCap) -> None
310
312
caplog .set_level (logging .DEBUG )
311
313
async with AsyncClient () as client :
312
314
embedding = client .embedding
313
- await embedding .unload (EXPECTED_EMBEDDING_ID )
314
- model = await embedding .model (EXPECTED_EMBEDDING_DEFAULT_ID )
315
- assert model .identifier == EXPECTED_EMBEDDING_DEFAULT_ID
315
+ with suppress (LMStudioModelNotFoundError ):
316
+ await embedding .unload (EXPECTED_EMBEDDING_ID )
317
+ model = await embedding .model (EXPECTED_EMBEDDING_ID )
318
+ assert model .identifier == EXPECTED_EMBEDDING_ID
316
319
# LM Studio may default to JIT handling for models loaded with `getOrLoad`,
317
320
# so ensure we restore a regular non-JIT instance with no TTL set
318
- await embedding .unload (EXPECTED_EMBEDDING_ID )
319
- model = await embedding .load_new_instance (
320
- EXPECTED_EMBEDDING_DEFAULT_ID , ttl = None
321
+ await model .unload ()
322
+ model = await embedding .load_new_instance (EXPECTED_EMBEDDING_ID , ttl = None )
323
+ assert model .identifier == EXPECTED_EMBEDDING_ID
324
+
325
+
326
+ @pytest .mark .asyncio
327
+ @pytest .mark .slow
328
+ @pytest .mark .lmstudio
329
+ async def test_jit_unloading_async (caplog : LogCap ) -> None :
330
+ # For the time being, only test the embedding vs LLM cross-namespace
331
+ # JIT unloading (since that ensures the info type mixing is handled).
332
+ # Assuming LM Studio eventually switches to per-namespace JIT unloading,
333
+ # this can be split into separate LLM and embedding test cases at that time.
334
+ caplog .set_level (logging .DEBUG )
335
+ async with AsyncClient () as client :
336
+ # Unload the non-JIT instance of the embedding model
337
+ with suppress (LMStudioModelNotFoundError ):
338
+ await client .embedding .unload (EXPECTED_EMBEDDING_ID )
339
+ # Load a JIT instance of the embedding model
340
+ model1 = await client .embedding .model (EXPECTED_EMBEDDING_ID , ttl = 300 )
341
+ assert model1 .identifier == EXPECTED_EMBEDDING_ID
342
+ model1_info = await model1 .get_info ()
343
+ assert model1_info .identifier == model1 .identifier
344
+ # Load a JIT instance of the small testing LLM
345
+ # This will unload the JIT instance of the testing embedding model
346
+ model2 = await client .llm .model (SMALL_LLM_ID , ttl = 300 )
347
+ assert model2 .identifier == SMALL_LLM_ID
348
+ model2_info = await model2 .get_info ()
349
+ assert model2_info .identifier == model2 .identifier
350
+ # Attempting to query the now unloaded JIT embedding model will fail
351
+ with pytest .raises (LMStudioModelNotFoundError ):
352
+ await model1 .get_info ()
353
+ # Restore things to the way other test cases expect them to be
354
+ await model2 .unload ()
355
+ model = await client .embedding .load_new_instance (
356
+ EXPECTED_EMBEDDING_ID , ttl = None
321
357
)
322
- assert model .identifier == EXPECTED_EMBEDDING_DEFAULT_ID
358
+ assert model .identifier == EXPECTED_EMBEDDING_ID
359
+
360
+ # Check for expected log messages
361
+ jit_unload_event = "Unloading other JIT model"
362
+ jit_unload_messages_debug : list [str ] = []
363
+ jit_unload_messages_info : list [str ] = []
364
+ jit_unload_messages = {
365
+ logging .DEBUG : jit_unload_messages_debug ,
366
+ logging .INFO : jit_unload_messages_info ,
367
+ }
368
+ for _logger_name , log_level , message in caplog .record_tuples :
369
+ if jit_unload_event not in message :
370
+ continue
371
+ jit_unload_messages [log_level ].append (message )
372
+
373
+ assert len (jit_unload_messages_info ) == 1
374
+ assert len (jit_unload_messages_debug ) == 1
375
+
376
+ info_message = jit_unload_messages_info [0 ]
377
+ debug_message = jit_unload_messages_debug [0 ]
378
+ # Ensure info message omits model info, but includes config guidance
379
+ unload_notice = f'"event": "{ jit_unload_event } "'
380
+ assert unload_notice in info_message
381
+ loading_model_notice = f'"model_key": "{ SMALL_LLM_ID } "'
382
+ assert loading_model_notice in info_message
383
+ unloaded_model_notice = f'"unloaded_model_key": "{ EXPECTED_EMBEDDING_ID } "'
384
+ assert unloaded_model_notice in info_message
385
+ assert '"suggestion": ' in info_message
386
+ assert "disable this behavior" in info_message
387
+ assert '"unloaded_model": ' not in info_message
388
+ # Ensure debug message includes model info, but omits config guidance
389
+ assert unload_notice in debug_message
390
+ assert loading_model_notice in info_message
391
+ assert unloaded_model_notice in debug_message
392
+ assert '"suggestion": ' not in debug_message
393
+ assert "disable this behavior" not in debug_message
394
+ assert '"unloaded_model": ' in debug_message
0 commit comments