diff --git a/provider_openai_test.go b/provider_openai_test.go index 93a2978..58d078d 100644 --- a/provider_openai_test.go +++ b/provider_openai_test.go @@ -708,6 +708,24 @@ func TestBuildAdapters(t *testing.T) { } }) + t.Run("bad: metadataPath without a leading slash is a constructor error (review fix)", func(t *testing.T) { + cfg := &Config{Providers: map[string]*ProviderConfig{ + "lmstudio": {Type: "openai", MetadataPath: "api/v0/models"}, + }} + if _, err := buildAdapters(cfg); err == nil { + t.Error("want an error for a metadataPath missing its leading slash") + } + }) + + t.Run("good: empty metadataPath is valid (feature disabled)", func(t *testing.T) { + cfg := &Config{Providers: map[string]*ProviderConfig{ + "p1": {Type: "openai", APIKey: "sk-test"}, + }} + if _, err := buildAdapters(cfg); err != nil { + t.Errorf("buildAdapters: %v, want no error for an empty metadataPath", err) + } + }) + t.Run("good: explicit base URL trailing slash trimmed", func(t *testing.T) { cfg := &Config{Providers: map[string]*ProviderConfig{ "p1": {Type: "openai", BaseURL: "https://custom.example.com/", APIKey: "sk-test"}, diff --git a/providers.go b/providers.go index fbce066..76b7485 100644 --- a/providers.go +++ b/providers.go @@ -367,6 +367,25 @@ func validateConfigName(kind, name string) error { return nil } +// validateMetadataPath rejects a non-empty ProviderConfig.MetadataPath +// (feature v0.23) that does not start with "/" (review fix, folded +// minor) — fetchModelMetadata (provider_openai.go) builds its request +// URL by plain string concatenation, a.baseURL+a.metadataPath; a value +// missing the leading slash would silently glue onto baseURL's own path +// with no separator (e.g. "http://host:1234api/v0/models"), a malformed +// URL that only surfaces as a confusing runtime fetch failure instead of +// a clear, named construction error. Empty (the default, metadata +// capture disabled) is always valid. +func validateMetadataPath(providerName, metadataPath string) error { + if metadataPath == "" { + return nil + } + if !strings.HasPrefix(metadataPath, "/") { + return fmt.Errorf("llmgateway: provider %q: metadataPath %q must start with \"/\"", providerName, metadataPath) + } + return nil +} + // buildAdapters resolves cfg.Providers into a map of providerAdapter keyed // by provider name. Every provider name is validated (validateConfigName) // and every ProviderConfig value must be non-nil before anything else runs, @@ -413,6 +432,10 @@ func buildAdapters(cfg *Config) (map[string]providerAdapter, error) { base = defaultBaseURLByType[pc.Type] } + if err := validateMetadataPath(name, pc.MetadataPath); err != nil { + return nil, err + } + switch pc.Type { case providerTypeOpenAI: a := newOpenAIAdapter(name, base, apiKey)