diff --git a/conf/models/volcengine.json b/conf/models/volcengine.json index 8253549370..76506ad134 100644 --- a/conf/models/volcengine.json +++ b/conf/models/volcengine.json @@ -6,7 +6,8 @@ "url_suffix": { "chat": "chat/completions", "files": "files", - "embedding": "embeddings/multimodal" + "embedding": "embeddings/multimodal", + "models": "models" }, "class": "volcengine", "models": [ @@ -29,4 +30,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/entity/models/volcengine.go b/internal/entity/models/volcengine.go index 8f3133aa41..806d228704 100644 --- a/internal/entity/models/volcengine.go +++ b/internal/entity/models/volcengine.go @@ -551,8 +551,12 @@ func (z *VolcEngine) ListModels(apiConfig *APIConfig) ([]string, error) { if baseURL == "" { return nil, fmt.Errorf("volcengine: no base URL configured for region %q", region) } + modelsSuffix := strings.Trim(strings.TrimSpace(z.URLSuffix.Models), "/") + if modelsSuffix == "" { + return nil, fmt.Errorf("volcengine: models URL suffix is not configured") + } - url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), z.URLSuffix.Models) + url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), modelsSuffix) req, err := http.NewRequest("GET", url, nil) if err != nil { diff --git a/internal/entity/models/volcengine_test.go b/internal/entity/models/volcengine_test.go new file mode 100644 index 0000000000..08da61b04f --- /dev/null +++ b/internal/entity/models/volcengine_test.go @@ -0,0 +1,110 @@ +package models + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +func newVolcEngineServer(t *testing.T, handler func(t *testing.T, r *http.Request, w http.ResponseWriter)) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler(t, r, w) + })) +} + +func newVolcEngineForTest(baseURL string) *VolcEngine { + return NewVolcEngine( + map[string]string{"default": baseURL}, + URLSuffix{ + Chat: "chat/completions", + Files: "files", + Embedding: "embeddings/multimodal", + Models: "/models", + }, + ) +} + +func TestVolcEngineConfigDeclaresModelsSuffix(t *testing.T) { + var provider struct { + URLSuffix URLSuffix `json:"url_suffix"` + } + + for _, candidate := range []string{ + filepath.Join("..", "..", "..", "conf", "models", "volcengine.json"), + filepath.Join("conf", "models", "volcengine.json"), + } { + data, err := os.ReadFile(candidate) + if err != nil { + continue + } + if err := json.Unmarshal(data, &provider); err != nil { + t.Fatalf("unmarshal %s: %v", candidate, err) + } + if provider.URLSuffix.Models != "models" { + t.Fatalf("models suffix=%q, want models", provider.URLSuffix.Models) + } + return + } + + t.Fatal("could not locate conf/models/volcengine.json") +} + +func TestVolcEngineListModelsHappyPath(t *testing.T) { + srv := newVolcEngineServer(t, func(t *testing.T, r *http.Request, w http.ResponseWriter) { + if r.Method != http.MethodGet { + t.Errorf("method=%s", r.Method) + } + if r.URL.Path != "/models" { + t.Errorf("path=%s", r.URL.Path) + } + if got := r.Header.Get("Authorization"); got != "Bearer test-key" { + t.Errorf("Authorization=%q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "object": "list", + "data": [ + {"id": "doubao-seed-2-0-pro-260215", "owned_by": "volcengine"}, + {"id": "doubao-embedding-vision-251215"} + ] + }`)) + }) + defer srv.Close() + + apiKey := "test-key" + models, err := newVolcEngineForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + if err != nil { + t.Fatalf("ListModels: %v", err) + } + if strings.Join(models, ",") != "doubao-seed-2-0-pro-260215@volcengine,doubao-embedding-vision-251215" { + t.Errorf("models=%v", models) + } +} + +func TestVolcEngineListModelsRejectsProviderError(t *testing.T) { + srv := newVolcEngineServer(t, func(t *testing.T, r *http.Request, w http.ResponseWriter) { + http.Error(w, "bad key", http.StatusUnauthorized) + }) + defer srv.Close() + + apiKey := "test-key" + _, err := newVolcEngineForTest(srv.URL).ListModels(&APIConfig{ApiKey: &apiKey}) + if err == nil || !strings.Contains(err.Error(), "401 Unauthorized") || !strings.Contains(err.Error(), "bad key") { + t.Fatalf("expected provider error with status and body, got %v", err) + } +} + +func TestVolcEngineListModelsRequiresModelsSuffix(t *testing.T) { + apiKey := "test-key" + model := NewVolcEngine(map[string]string{"default": "http://unused"}, URLSuffix{}) + + _, err := model.ListModels(&APIConfig{ApiKey: &apiKey}) + if err == nil || !strings.Contains(err.Error(), "models URL suffix is not configured") { + t.Fatalf("expected missing models suffix error, got %v", err) + } +}