1package provider
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11)
12
13func TestDiscoverLMStudio(t *testing.T) {
14 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15 if r.URL.Path != "/api/v1/models" || r.Header.Get("Authorization") != "Bearer test-key" {
16 t.Errorf("unexpected discovery request: %s", r.URL.Path)
17 }
18 fmt.Fprint(w, `{"models":[
19 {"type":"llm","key":"model-key","display_name":"Local Model","max_context_length":262144,"loaded_instances":[{"id":"instance","config":{"context_length":50176}}]},
20 {"type":"llm","key":"unloaded@q4","max_context_length":8192},
21 {"type":"llm","key":"unloaded@q4"},
22 {"type":"embedding","key":"embedding"},
23 {"type":"llm","key":""}]}`)
24 }))
25 defer server.Close()
26 models, err := DiscoverLMStudio(context.Background(), server.URL+"/v1/", "test-key")
27 if err != nil {
28 t.Fatal(err)
29 }
30 if len(models) != 2 {
31 t.Fatalf("models = %+v", models)
32 }
33 m := models[0]
34 if m.ID != "instance" || m.ContextWindow != 50176 || m.DisplayName != "Local Model" || m.BaseURL != server.URL+"/v1" || m.Provider != LMStudioProviderID || m.Reasoning || m.PriceInput != 0 {
35 t.Fatalf("unexpected metadata: %+v", m)
36 }
37 if models[1].ID != "unloaded@q4" || models[1].MaxOutput != 2048 {
38 t.Fatalf("unloaded model = %+v", models[1])
39 }
40}
41
42func TestDiscoverLMStudioFallbackAndErrors(t *testing.T) {
43 for _, status := range []int{404, 405, 501, 401, 500, 200} {
44 t.Run(fmt.Sprint(status), func(t *testing.T) {
45 fallbackCalls := 0
46 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
47 if r.Header.Get("Authorization") != "" {
48 t.Error("keyless request sent authorization")
49 }
50 if r.URL.Path == "/api/v1/models" {
51 w.WriteHeader(status)
52 fmt.Fprint(w, `{`)
53 return
54 }
55 fallbackCalls++
56 fmt.Fprint(w, `{"data":[{"id":"qwen/local@q4"},{"id":"qwen/local@q4"},{"id":"text-embedding-test"}]}`)
57 }))
58 defer server.Close()
59 models, err := DiscoverLMStudio(context.Background(), server.URL, "")
60 if status == 404 || status == 405 || status == 501 {
61 if err != nil || fallbackCalls != 1 || len(models) != 1 || models[0].ContextWindow != 32768 {
62 t.Fatalf("models=%+v error=%v calls=%d", models, err, fallbackCalls)
63 }
64 } else if err == nil || fallbackCalls != 0 {
65 t.Fatalf("error=%v fallback calls=%d", err, fallbackCalls)
66 }
67 })
68 }
69}
70
71func TestDiscoverLMStudioEmptyAndCanceled(t *testing.T) {
72 for _, body := range []string{`{"models":[]}`, `{}`, `null`} {
73 t.Run(body, func(t *testing.T) {
74 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, body) }))
75 defer server.Close()
76 models, err := DiscoverLMStudio(context.Background(), server.URL, "")
77 if body == `{"models":[]}` {
78 if err != nil || len(models) != 0 {
79 t.Fatalf("models=%v error=%v", models, err)
80 }
81 } else if err == nil {
82 t.Fatal("missing model list accepted")
83 }
84 ctx, cancel := context.WithCancel(context.Background())
85 cancel()
86 if _, err := DiscoverLMStudio(ctx, server.URL, ""); !errors.Is(err, context.Canceled) {
87 t.Fatalf("cancel error=%v", err)
88 }
89 })
90 }
91}
92
93func TestNormalizeLMStudioURL(t *testing.T) {
94 for _, suffix := range []string{"", "/", "/v1", "/api/v1/"} {
95 got, err := NormalizeLMStudioURL("http://localhost:1234/proxy" + suffix)
96 if err != nil || got != "http://localhost:1234/proxy" {
97 t.Fatalf("normalize %q: %q, %v", suffix, got, err)
98 }
99 }
100 for _, value := range []string{"", "localhost:1234", "file:///tmp", "https://secret@example.com", "http://localhost?token=secret"} {
101 if _, err := NormalizeLMStudioURL(value); err == nil || strings.Contains(err.Error(), "secret") {
102 t.Fatalf("invalid URL error: %v", err)
103 }
104 }
105}
106
107func TestManagedLocalProvidersRemainIndependent(t *testing.T) {
108 SetManagedModels(nil)
109 t.Cleanup(func() { SetManagedModels(nil) })
110 SetManagedModelsForProvider(LlamaCPPProviderID, []Model{{Provider: LlamaCPPProviderID, ID: "llama"}})
111 SetManagedModelsForProvider(LMStudioProviderID, []Model{{Provider: LMStudioProviderID, ID: "studio"}})
112 SetManagedModelsForProvider(LlamaCPPProviderID, nil)
113 if _, err := FindModel(LMStudioProviderID, "studio"); err != nil {
114 t.Fatal(err)
115 }
116 if _, err := FindModel(LlamaCPPProviderID, "llama"); err == nil {
117 t.Fatal("stale llama model")
118 }
119 SetUserModels([]Model{{Provider: LMStudioProviderID, ID: "studio", ContextWindow: 12345, Source: "user"}})
120 t.Cleanup(func() { SetLiveModels(nil) })
121 model, err := FindModel(LMStudioProviderID, "studio")
122 if err != nil || model.ContextWindow != 12345 {
123 t.Fatalf("user override lost: %+v, %v", model, err)
124 }
125}