33import asyncio
44import logging
55
6+ from contextlib import suppress
7+
68import pytest
79from pytest import LogCaptureFixture as LogCap
810from pytest_subtests import SubTests
1315from ..support import (
1416 LLM_LOAD_CONFIG ,
1517 EXPECTED_LLM ,
16- EXPECTED_LLM_DEFAULT_ID ,
1718 EXPECTED_LLM_ID ,
1819 EXPECTED_EMBEDDING ,
19- EXPECTED_EMBEDDING_DEFAULT_ID ,
2020 EXPECTED_EMBEDDING_ID ,
2121 EXPECTED_VLM_ID ,
22+ SMALL_LLM_ID ,
2223 TOOL_LLM_ID ,
2324 check_sdk_error ,
2425)
@@ -291,16 +292,17 @@ async def test_get_or_load_when_unloaded_llm_async(caplog: LogCap) -> None:
291292 caplog .set_level (logging .DEBUG )
292293 async with AsyncClient () as client :
293294 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
297299 # LM Studio may default to JIT handling for models loaded with `getOrLoad`,
298300 # so ensure we restore a regular non-JIT instance with no TTL set
299- await llm .unload (EXPECTED_LLM_ID )
301+ await model .unload ()
300302 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
302304 )
303- assert model .identifier == EXPECTED_LLM_DEFAULT_ID
305+ assert model .identifier == EXPECTED_LLM_ID
304306
305307
306308@pytest .mark .asyncio
@@ -310,13 +312,83 @@ async def test_get_or_load_when_unloaded_embedding_async(caplog: LogCap) -> None
310312 caplog .set_level (logging .DEBUG )
311313 async with AsyncClient () as client :
312314 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
316319 # LM Studio may default to JIT handling for models loaded with `getOrLoad`,
317320 # 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
321357 )
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