v0.4.3 2a19df1

lmstudio_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close