From 2fd8cdc3cc40b28ebd6ff578b9ec095c5cdce8ad Mon Sep 17 00:00:00 2001 From: Panda Dev <56657208+pandadev66@users.noreply.github.com> Date: Fri, 8 May 2026 06:00:10 +0200 Subject: [PATCH] fix(go): wire CheckConnection to ListModels in ollama, lm-studio, and vllm (#14614) ### What problem does this PR solve? Three Go drivers had `CheckConnection` returning a hardcoded `no such method` error, even though each one already has a working `ListModels` that hits the configured base URL with the configured API key. So the "Check connection" button in the model provider UI always failed for these three providers, even when the underlying setup was fine. Affected drivers: - `internal/entity/models/ollama.go` - `internal/entity/models/lmstudio.go` - `internal/entity/models/vllm.go` This is a real user-facing gap because Ollama and LM Studio are two of the most popular local LLM runners, and vLLM is widely used for self-hosted deployments. ### What this PR includes For each of the three drivers, replace the stub with a small implementation that calls `ListModels` and returns its error: ```go func (o *OllamaModel) CheckConnection(apiConfig *APIConfig) error { _, err := o.ListModels(apiConfig) return err } ``` This is the exact pattern that xai, moonshot, deepseek, aliyun, and gitee already use for the same method. No JSON change. No factory change. No interface change. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) ### How was this tested? - `go build ./internal/entity/models/...` in a clean go 1.25 image (the go.mod minimum) returns exit 0. - The full ModelDriver interface still resolves on each driver (NewInstance, Name, ChatWithMessages, ChatStreamlyWithSender, Encode, Rerank, ListModels, Balance, CheckConnection). - Pattern parity with the existing xai, moonshot, deepseek, aliyun, and gitee CheckConnection methods. Closes #14609 --- internal/entity/models/lmstudio.go | 42 +++++++++++++++++++++++++++--- internal/entity/models/ollama.go | 42 +++++++++++++++++++++++++++--- internal/entity/models/vllm.go | 42 +++++++++++++++++++++++++++--- 3 files changed, 114 insertions(+), 12 deletions(-) diff --git a/internal/entity/models/lmstudio.go b/internal/entity/models/lmstudio.go index 55122bedc..ec6a47323 100644 --- a/internal/entity/models/lmstudio.go +++ b/internal/entity/models/lmstudio.go @@ -363,11 +363,19 @@ func (l *LmStudioModel) Rerank(modelName *string, query string, texts []string, // ListModels list supported models func (l *LmStudioModel) ListModels(apiConfig *APIConfig) ([]string, error) { var region = "default" - if apiConfig.Region != nil { + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { region = *apiConfig.Region } - url := fmt.Sprintf("%s/%s", l.BaseURL[region], l.URLSuffix.Models) + baseURL := l.BaseURL[region] + if baseURL == "" { + baseURL = l.BaseURL["default"] + } + if baseURL == "" { + return nil, fmt.Errorf("missing base URL: please configure the local access address for LM Studio (e.g., http://127.0.0.1:1234/v1)") + } + + url := fmt.Sprintf("%s/%s", baseURL, l.URLSuffix.Models) reqBody := map[string]interface{}{} @@ -382,7 +390,12 @@ func (l *LmStudioModel) ListModels(apiConfig *APIConfig) ([]string, error) { } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + // LM Studio is a local provider and the API key is optional. Only + // set the Authorization header when a non-empty key was supplied. + // This also avoids a nil-pointer dereference on apiConfig or ApiKey. + if apiConfig != nil && apiConfig.ApiKey != nil && *apiConfig.ApiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } resp, err := l.httpClient.Do(req) if err != nil { @@ -420,6 +433,27 @@ func (l *LmStudioModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e return nil, fmt.Errorf("no such method") } +// CheckConnection verifies that the configured LM Studio base URL +// is reachable and that the API key (if any) is accepted, by issuing +// a lightweight ListModels call. The empty-URL guard runs first so +// a user who has not yet set the local access address gets a clear, +// actionable error instead of a low-level transport message. func (l *LmStudioModel) CheckConnection(apiConfig *APIConfig) error { - return fmt.Errorf("no such method") + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + baseURL := l.BaseURL[region] + if baseURL == "" { + baseURL = l.BaseURL["default"] + } + if baseURL == "" { + return fmt.Errorf("missing base URL: please configure the local access address for LM Studio (e.g., http://127.0.0.1:1234/v1)") + } + + if _, err := l.ListModels(apiConfig); err != nil { + return fmt.Errorf("connection check failed: %w", err) + } + return nil } diff --git a/internal/entity/models/ollama.go b/internal/entity/models/ollama.go index 9cc1907f5..4f680e0cc 100644 --- a/internal/entity/models/ollama.go +++ b/internal/entity/models/ollama.go @@ -361,11 +361,19 @@ func (o *OllamaModel) Rerank(modelName *string, query string, texts []string, ap func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]string, error) { var region = "default" - if apiConfig.Region != nil { + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { region = *apiConfig.Region } - url := fmt.Sprintf("%s/%s", o.BaseURL[region], o.URLSuffix.Models) + baseURL := o.BaseURL[region] + if baseURL == "" { + baseURL = o.BaseURL["default"] + } + if baseURL == "" { + return nil, fmt.Errorf("missing base URL: please configure the local access address for Ollama (e.g., http://127.0.0.1:11434/v1)") + } + + url := fmt.Sprintf("%s/%s", baseURL, o.URLSuffix.Models) reqBody := map[string]interface{}{} @@ -380,7 +388,12 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]string, error) { } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + // Ollama is a local provider and the API key is optional. Only set + // the Authorization header when a non-empty key was supplied. This + // also avoids a nil-pointer dereference on apiConfig or ApiKey. + if apiConfig != nil && apiConfig.ApiKey != nil && *apiConfig.ApiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } resp, err := o.httpClient.Do(req) if err != nil { @@ -418,6 +431,27 @@ func (o *OllamaModel) Balance(apiConfig *APIConfig) (map[string]interface{}, err return nil, fmt.Errorf("no such method") } +// CheckConnection verifies that the configured Ollama base URL is +// reachable and that the API key (if any) is accepted, by issuing a +// lightweight ListModels call. The empty-URL guard runs first so a +// user who has not yet set the local access address gets a clear, +// actionable error instead of a low-level transport message. func (o *OllamaModel) CheckConnection(apiConfig *APIConfig) error { - return fmt.Errorf("no such method") + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + baseURL := o.BaseURL[region] + if baseURL == "" { + baseURL = o.BaseURL["default"] + } + if baseURL == "" { + return fmt.Errorf("missing base URL: please configure the local access address for Ollama (e.g., http://127.0.0.1:11434/v1)") + } + + if _, err := o.ListModels(apiConfig); err != nil { + return fmt.Errorf("connection check failed: %w", err) + } + return nil } diff --git a/internal/entity/models/vllm.go b/internal/entity/models/vllm.go index 8d675f904..1497012a7 100644 --- a/internal/entity/models/vllm.go +++ b/internal/entity/models/vllm.go @@ -376,11 +376,19 @@ func (z *VllmModel) Encode(modelName *string, texts []string, apiConfig *APIConf func (z *VllmModel) ListModels(apiConfig *APIConfig) ([]string, error) { var region = "default" - if apiConfig.Region != nil { + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { region = *apiConfig.Region } - url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Models) + baseURL := z.BaseURL[region] + if baseURL == "" { + baseURL = z.BaseURL["default"] + } + if baseURL == "" { + return nil, fmt.Errorf("missing base URL: please configure the local access address for vLLM (e.g., http://127.0.0.1:8000/v1)") + } + + url := fmt.Sprintf("%s/%s", baseURL, z.URLSuffix.Models) reqBody := map[string]interface{}{} @@ -395,7 +403,12 @@ func (z *VllmModel) ListModels(apiConfig *APIConfig) ([]string, error) { } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + // vLLM is a local provider and the API key is optional. Only set + // the Authorization header when a non-empty key was supplied. This + // also avoids a nil-pointer dereference on apiConfig or ApiKey. + if apiConfig != nil && apiConfig.ApiKey != nil && *apiConfig.ApiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } resp, err := z.httpClient.Do(req) if err != nil { @@ -433,8 +446,29 @@ func (z *VllmModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error return nil, fmt.Errorf("no such method") } +// CheckConnection verifies that the configured vLLM base URL is +// reachable and that the API key (if any) is accepted, by issuing a +// lightweight ListModels call. The empty-URL guard runs first so a +// user who has not yet set the local access address gets a clear, +// actionable error instead of a low-level transport message. func (z *VllmModel) CheckConnection(apiConfig *APIConfig) error { - return fmt.Errorf("no such method") + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + baseURL := z.BaseURL[region] + if baseURL == "" { + baseURL = z.BaseURL["default"] + } + if baseURL == "" { + return fmt.Errorf("missing base URL: please configure the local access address for vLLM (e.g., http://127.0.0.1:8000/v1)") + } + + if _, err := z.ListModels(apiConfig); err != nil { + return fmt.Errorf("connection check failed: %w", err) + } + return nil } // Rerank calculates similarity scores between query and texts