@@ -0,0 +1,191 @@
14+ "github.com/spf13/viper"
18+ ProviderLocal ModelProvider = "local"
20+ localModelsPath = "v1/models"
21+ lmStudioBetaModelsPath = "api/v0/models"
25+ if endpoint := os.Getenv("LOCAL_ENDPOINT"); endpoint != "" {
26+ localEndpoint, err := url.Parse(endpoint)
28+ slog.Debug("Failed to parse local endpoint",
30+ "endpoint", endpoint,
35+ load := func(url *url.URL, path string) []localModel {
37+ return listLocalModels(url.String())
40+ models := load(localEndpoint, lmStudioBetaModelsPath)
42+ if len(models) == 0 {
43+ models = load(localEndpoint, localModelsPath)
46+ if len(models) == 0 {
47+ slog.Debug("No local models found",
48+ "endpoint", endpoint,
53+ loadLocalModels(models)
55+ viper.SetDefault("providers.local.apiKey", "dummy")
56+ ProviderPopularity[ProviderLocal] = 0
60+type localModelList struct {
61+ Data []localModel `json:"data"`
64+type localModel struct {
65+ ID string `json:"id"`
66+ Object string `json:"object"`
67+ Type string `json:"type"`
68+ Publisher string `json:"publisher"`
69+ Arch string `json:"arch"`
70+ CompatibilityType string `json:"compatibility_type"`
71+ Quantization string `json:"quantization"`
72+ State string `json:"state"`
73+ MaxContextLength int64 `json:"max_context_length"`
74+ LoadedContextLength int64 `json:"loaded_context_length"`
77+func listLocalModels(modelsEndpoint string) []localModel {
78+ res, err := http.Get(modelsEndpoint)
80+ slog.Debug("Failed to list local models",
82+ "endpoint", modelsEndpoint,
85+ defer res.Body.Close()
87+ if res.StatusCode != http.StatusOK {
88+ slog.Debug("Failed to list local models",
89+ "status", res.StatusCode,
90+ "endpoint", modelsEndpoint,
94+ var modelList localModelList
95+ if err = json.NewDecoder(res.Body).Decode(&modelList); err != nil {
96+ slog.Debug("Failed to list local models",
98+ "endpoint", modelsEndpoint,
102+ var supportedModels []localModel
103+ for _, model := range modelList.Data {
104+ if strings.HasSuffix(modelsEndpoint, lmStudioBetaModelsPath) {
105+ if model.Object != "model" || model.Type != "llm" {
106+ slog.Debug("Skipping unsupported LMStudio model",
107+ "endpoint", modelsEndpoint,
109+ "object", model.Object,
117+ supportedModels = append(supportedModels, model)
120+ return supportedModels
123+func loadLocalModels(models []localModel) {
124+ for i, m := range models {
125+ model := convertLocalModel(m)
126+ SupportedModels[model.ID] = model
128+ if i == 1 || m.State == "loaded" {
129+ viper.SetDefault("agents.coder.model", model.ID)
130+ viper.SetDefault("agents.summarizer.model", model.ID)
131+ viper.SetDefault("agents.task.model", model.ID)
132+ viper.SetDefault("agents.title.model", model.ID)
137+func convertLocalModel(model localModel) Model {
139+ ID: ModelID("local." + model.ID),
140+ Name: friendlyModelName(model.ID),
141+ Provider: ProviderLocal,
143+ ContextWindow: cmp.Or(model.LoadedContextLength, 4096),
144+ DefaultMaxTokens: cmp.Or(model.LoadedContextLength, 4096),
146+ SupportsAttachments: true,
150+var modelInfoRegex = regexp.MustCompile(`(?i)^([a-z0-9]+)(?:[-_]?([rv]?\d[\.\d]*))?(?:[-_]?([a-z]+))?.*`)
152+func friendlyModelName(modelID string) string {
153+ match := modelInfoRegex.FindStringSubmatch(modelID)
158+ capitalize := func(s string) string {
163+ runes[0] = unicode.ToUpper(runes[0])
164+ return string(runes)
167+ family := capitalize(match[1])
171+ if len(match) > 2 && match[2] != "" {
172+ version = strings.ToUpper(match[2])
175+ if len(match) > 3 && match[3] != "" {
176+ label = capitalize(match[3])
181+ parts = append(parts, family)
184+ parts = append(parts, version)
187+ parts = append(parts, label)
190+ return strings.Join(parts, " ")