v0.3.96 c5883a4

lmstudio.go RawHistoryFolder

1package provider
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"net/http"
8	"net/url"
9	"strings"
10	"time"
11)
12
13const LMStudioProviderID = "lmstudio"
14
15// NormalizeLMStudioURL accepts a server root or either API base URL.
16func NormalizeLMStudioURL(value string) (string, error) {
17	u, err := url.Parse(strings.TrimSpace(value))
18	if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
19		return "", fmt.Errorf("LM Studio server URL must be an http or https URL without credentials, query, or fragment")
20	}
21	u.Path = strings.TrimRight(u.Path, "/")
22	if strings.HasSuffix(u.Path, "/api/v1") {
23		u.Path = strings.TrimSuffix(u.Path, "/api/v1")
24	} else {
25		u.Path = strings.TrimSuffix(u.Path, "/v1")
26	}
27	u.RawPath = ""
28	return strings.TrimRight(u.String(), "/"), nil
29}
30
31// DiscoverLMStudio lists models without loading them. Only older servers that
32// lack the native endpoint fall back to the less descriptive OpenAI endpoint.
33func DiscoverLMStudio(ctx context.Context, serverURL, apiKey string) ([]Model, error) {
34	root, err := NormalizeLMStudioURL(serverURL)
35	if err != nil {
36		return nil, err
37	}
38	client := &http.Client{Timeout: 3 * time.Second}
39	get := func(path string, out any) (int, error) {
40		req, err := http.NewRequestWithContext(ctx, http.MethodGet, root+path, nil)
41		if err != nil {
42			return 0, err
43		}
44		if apiKey != "" {
45			req.Header.Set("Authorization", "Bearer "+apiKey)
46		}
47		resp, err := client.Do(req)
48		if err != nil {
49			if ctx.Err() != nil {
50				return 0, ctx.Err()
51			}
52			return 0, fmt.Errorf("LM Studio discovery request failed")
53		}
54		defer resp.Body.Close()
55		if resp.StatusCode != http.StatusOK {
56			return resp.StatusCode, fmt.Errorf("LM Studio discovery HTTP %d", resp.StatusCode)
57		}
58		if err := json.NewDecoder(http.MaxBytesReader(nil, resp.Body, 8<<20)).Decode(out); err != nil {
59			return resp.StatusCode, fmt.Errorf("LM Studio discovery returned invalid model data")
60		}
61		return resp.StatusCode, nil
62	}
63	var native struct {
64		Models []struct {
65			Type    string `json:"type"`
66			Key     string `json:"key"`
67			Name    string `json:"display_name"`
68			Context int    `json:"max_context_length"`
69			Loaded  []struct {
70				ID     string `json:"id"`
71				Config struct {
72					Context int `json:"context_length"`
73				} `json:"config"`
74			} `json:"loaded_instances"`
75		} `json:"models"`
76	}
77	status, err := get("/api/v1/models", &native)
78	var models []Model
79	seen := map[string]bool{}
80	add := func(id, name string, contextWindow int) {
81		if strings.TrimSpace(id) == "" || seen[id] {
82			return
83		}
84		seen[id] = true
85		if name == "" {
86			name = id
87		}
88		if contextWindow <= 0 {
89			contextWindow = 32768
90		}
91		maxOutput := min(4096, max(1, contextWindow/4))
92		models = append(models, Model{Provider: LMStudioProviderID, ID: id, DisplayName: name, ContextWindow: contextWindow, MaxOutput: maxOutput, BaseURL: root + "/v1", Source: "live"})
93	}
94	if status == http.StatusNotFound || status == http.StatusMethodNotAllowed || status == http.StatusNotImplemented {
95		var fallback struct {
96			Data []struct {
97				ID string `json:"id"`
98			} `json:"data"`
99		}
100		if _, err := get("/v1/models", &fallback); err != nil {
101			return nil, err
102		}
103		if fallback.Data == nil {
104			return nil, fmt.Errorf("LM Studio discovery response is missing data")
105		}
106		for _, m := range fallback.Data {
107			if strings.Contains(strings.ToLower(m.ID), "embed") {
108				continue
109			}
110			add(m.ID, m.ID, 0)
111		}
112		return models, nil
113	}
114	if err != nil {
115		return nil, err
116	}
117	if native.Models == nil {
118		return nil, fmt.Errorf("LM Studio discovery response is missing models")
119	}
120	for _, m := range native.Models {
121		if m.Type != "llm" {
122			continue
123		}
124		if len(m.Loaded) == 0 {
125			add(m.Key, m.Name, m.Context)
126			continue
127		}
128		for _, instance := range m.Loaded {
129			id := instance.ID
130			if id == "" {
131				id = m.Key
132			}
133			// Unknown loaded context uses a conservative default, not the
134			// model's theoretical training limit.
135			add(id, m.Name, instance.Config.Context)
136		}
137	}
138	return models, nil
139}

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 477

↑↓ moveenter openesc close