@@ -212,11 +212,12 @@ describe("LMStudio Fetcher", () => {
212212 consoleInfoSpy . mockRestore ( )
213213 } )
214214
215- it ( "should return an empty object and log error if listDownloadedModels fails" , async ( ) => {
216- const consoleErrorSpy = vi . spyOn ( console , "error " ) . mockImplementation ( ( ) => { } )
215+ it ( "should return an empty object and log warning if listLoaded fails" , async ( ) => {
216+ const consoleWarnSpy = vi . spyOn ( console , "warn " ) . mockImplementation ( ( ) => { } )
217217 const listError = new Error ( "LMStudio SDK internal error" )
218218
219219 mockedAxios . get . mockResolvedValueOnce ( { data : { } } )
220+ mockListDownloadedModels . mockRejectedValueOnce ( new Error ( "Failed to list downloaded" ) )
220221 mockListLoaded . mockRejectedValueOnce ( listError )
221222
222223 const result = await getLMStudioModels ( baseUrl )
@@ -225,11 +226,86 @@ describe("LMStudio Fetcher", () => {
225226 expect ( MockedLMStudioClientConstructor ) . toHaveBeenCalledTimes ( 1 )
226227 expect ( MockedLMStudioClientConstructor ) . toHaveBeenCalledWith ( { baseUrl : lmsUrl } )
227228 expect ( mockListLoaded ) . toHaveBeenCalledTimes ( 1 )
228- expect ( consoleErrorSpy ) . toHaveBeenCalledWith (
229- `Error fetching LMStudio models: ${ JSON . stringify ( listError , Object . getOwnPropertyNames ( listError ) , 2 ) } ` ,
229+ // Now it should log a warning for failed SDK methods, not an error
230+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith (
231+ "Failed to list downloaded models, falling back to loaded models only" ,
230232 )
233+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith ( "Failed to list loaded models via SDK" )
231234 expect ( result ) . toEqual ( { } )
232- consoleErrorSpy . mockRestore ( )
235+ consoleWarnSpy . mockRestore ( )
236+ } )
237+
238+ it ( "should fall back to OpenAI API models when SDK methods fail" , async ( ) => {
239+ const consoleLogSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
240+ const consoleWarnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
241+
242+ // Mock OpenAI API response with models
243+ const openAiModels = {
244+ data : [
245+ { id : "openai/gpt-oss-20b" , object : "model" , owned_by : "organization_owner" } ,
246+ { id : "unsloth/gpt-oss-20b" , object : "model" , owned_by : "organization_owner" } ,
247+ { id : "qwen/qwen3-coder-30b" , object : "model" , owned_by : "organization_owner" } ,
248+ ] ,
249+ object : "list" ,
250+ }
251+
252+ mockedAxios . get . mockResolvedValueOnce ( { data : openAiModels } )
253+
254+ // Make SDK methods fail
255+ mockListDownloadedModels . mockRejectedValueOnce ( new Error ( "SDK not available" ) )
256+ mockListLoaded . mockRejectedValueOnce ( new Error ( "SDK not available" ) )
257+
258+ const result = await getLMStudioModels ( baseUrl )
259+
260+ // Should have called the OpenAI endpoint
261+ expect ( mockedAxios . get ) . toHaveBeenCalledWith ( `${ baseUrl } /v1/models` )
262+
263+ // Should have tried SDK methods
264+ expect ( MockedLMStudioClientConstructor ) . toHaveBeenCalledWith ( { baseUrl : lmsUrl } )
265+ expect ( mockListDownloadedModels ) . toHaveBeenCalled ( )
266+ expect ( mockListLoaded ) . toHaveBeenCalled ( )
267+
268+ // Should have logged the fallback
269+ expect ( consoleLogSpy ) . toHaveBeenCalledWith ( "Falling back to OpenAI-compatible API models" )
270+
271+ // Should return models from OpenAI API
272+ expect ( Object . keys ( result ) ) . toHaveLength ( 3 )
273+ expect ( result [ "openai/gpt-oss-20b" ] ) . toBeDefined ( )
274+ expect ( result [ "openai/gpt-oss-20b" ] . description ) . toBe ( "openai/gpt-oss-20b" )
275+ expect ( result [ "unsloth/gpt-oss-20b" ] ) . toBeDefined ( )
276+ expect ( result [ "qwen/qwen3-coder-30b" ] ) . toBeDefined ( )
277+
278+ consoleLogSpy . mockRestore ( )
279+ consoleWarnSpy . mockRestore ( )
280+ } )
281+
282+ it ( "should not use OpenAI API fallback if SDK returns models" , async ( ) => {
283+ const consoleLogSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
284+
285+ // Mock OpenAI API response with models
286+ const openAiModels = {
287+ data : [ { id : "openai/gpt-oss-20b" , object : "model" , owned_by : "organization_owner" } ] ,
288+ object : "list" ,
289+ }
290+
291+ mockedAxios . get . mockResolvedValueOnce ( { data : openAiModels } )
292+
293+ // SDK returns models successfully
294+ mockListDownloadedModels . mockResolvedValueOnce ( [ ] )
295+ mockListLoaded . mockResolvedValueOnce ( [ { getModelInfo : mockGetModelInfo } ] )
296+ mockGetModelInfo . mockResolvedValueOnce ( mockRawModel )
297+
298+ const result = await getLMStudioModels ( baseUrl )
299+
300+ // Should NOT log the fallback message
301+ expect ( consoleLogSpy ) . not . toHaveBeenCalledWith ( "Falling back to OpenAI-compatible API models" )
302+
303+ // Should return SDK models, not OpenAI API models
304+ expect ( Object . keys ( result ) ) . toHaveLength ( 1 )
305+ expect ( result [ mockRawModel . modelKey ] ) . toBeDefined ( )
306+ expect ( result [ "openai/gpt-oss-20b" ] ) . toBeUndefined ( )
307+
308+ consoleLogSpy . mockRestore ( )
233309 } )
234310 } )
235311} )
0 commit comments