package gateway import ( "context" "net/http" "net/http/httptest" "strings" "testing" ) // TestComposeImageEndpoint_NoVersionBaseLeavesDefault is the counterpart of the // /v1 case: when the base carries no version segment, discovery must emit "" so // the gateway's own default "/v1/images/generations" composes. // // This is the shape every currently-working source uses — siliconflow, the only // source in production with kind:image, has base_url // "https://api.siliconflow.cn" — so writing a non-empty value here would break // exactly the configuration that is known to work today. func TestComposeImageEndpoint_NoVersionBaseLeavesDefault(t *testing.T) { if got := composeImageEndpoint("https://api.siliconflow.cn"); got != "" { t.Errorf("composeImageEndpoint = %q, want \"\" so the built-in default composes", got) } // And the composition it implies must be the vendor's real path. base := "https://api.siliconflow.cn" composed := base + "/v1/images/generations" if !strings.HasSuffix(composed, "/v1/images/generations") { t.Errorf("composed URL %q lost the image path", composed) } } // TestDiscovery_BareHostNeedsVersionOnEndpointWhenCatalogIsVersioned // distinguishes two cases that look identical from the outside. // // bare host + catalog at "/models" → chat is "/chat/completions" (the // adapter default, no endpoint written) // bare host + catalog at "/v1/models" → chat is "/v1/chat/completions", // so an endpoint MUST be written // // The catalog path is the only evidence of whether the upstream wants the // version segment, and getting this backwards sends every chat request to a // path the vendor does not serve. func TestDiscovery_BareHostNeedsVersionOnEndpointWhenCatalogIsVersioned(t *testing.T) { cases := []struct { name string catalogPath string wantEndpoint string wantImage string }{ { name: "catalog at the bare /models needs no endpoint", catalogPath: "/models", wantEndpoint: "", wantImage: "", }, { name: "catalog at /v1/models requires /v1 on the chat endpoint", catalogPath: "/v1/models", wantEndpoint: "/v1/chat/completions", wantImage: "", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != tc.catalogPath { w.WriteHeader(http.StatusNotFound) return } _, _ = w.Write([]byte(`{"data":[{"id":"m1"},{"id":"m2"}]}`)) })) defer srv.Close() res, err := DiscoverModels(context.Background(), srv.URL, "k") if err != nil { t.Fatalf("discovery failed: %v", err) } if res.BaseURL != srv.URL { t.Errorf("base = %q, want %q", res.BaseURL, srv.URL) } if res.Endpoint != tc.wantEndpoint { t.Errorf("endpoint = %q, want %q", res.Endpoint, tc.wantEndpoint) } if res.ImageEndpoint != tc.wantImage { t.Errorf("image_endpoint = %q, want %q", res.ImageEndpoint, tc.wantImage) } // Whatever it wrote, the chat URL must reach the same version // prefix the catalog was served under. ep := res.Endpoint if ep == "" { ep = "/chat/completions" } if got := res.BaseURL + ep; strings.Contains(got, "/v1/v1") { t.Errorf("chat URL %q has a doubled version", got) } }) } } // TestComposeEndpoints_BothFormsProduceWorkingURLs is the end-to-end // invariant across the two shapes, stated as the URLs the gateway will build. // // A base with a version and no image_endpoint produces "/v1/v1/…" — the exact // 404 observed before composeImageEndpoint existed. A base without one and a // non-empty image_endpoint produces a path missing the version. Both are // silent, expensive mistakes, so they are asserted literally. func TestComposeEndpoints_BothFormsProduceWorkingURLs(t *testing.T) { cases := []struct { name string base string wantChat string wantImage string }{ { name: "bare host: adapter and image defaults compose", base: "https://h", wantChat: "https://h/chat/completions", // openai's adapter endpoint is /chat/completions (verified in // internal/lua/adapters/openai.lua: adapter.endpoint). wantImage: "https://h/v1/images/generations", }, { name: "versioned base: the version moves onto both endpoints", base: "https://h/v1", wantChat: "https://h/v1/chat/completions", wantImage: "https://h/v1/images/generations", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ep := composeEndpoint("/models", tc.base, tc.base) if ep == "" { ep = "/chat/completions" // the adapter default } iep := composeImageEndpoint(tc.base) if iep == "" { iep = "/v1/images/generations" // the gateway default } gotChat := tc.base + ep gotImage := tc.base + iep if gotChat != tc.wantChat { t.Errorf("chat URL = %q, want %q", gotChat, tc.wantChat) } if gotImage != tc.wantImage { t.Errorf("image URL = %q, want %q", gotImage, tc.wantImage) } for _, u := range []string{gotChat, gotImage} { if strings.Contains(u, "/v1/v1") { t.Errorf("%q has a doubled version segment", u) } } }) } }