fix(providers): send Kimi headers when listing models

This commit is contained in:
Goon committed 2026-06-15 10:16:47 +07:00
1 parent cc9334fdd9
commit 540dc11301
3 files changed
+97 -8

No files matched your search

+26 -7
View File
@@ -15,8 +15,8 @@ import (
// ModelInfo is a normalized model entry returned by the list-models endpoint.
type ModelInfo struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
ID string `json:"id"`
Name string `json:"name,omitempty"`
Reasoning *providers.ReasoningCapability `json:"reasoning,omitempty"`
}
@@ -109,11 +109,8 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
models = minimaxModels()
default:
// All other types use OpenAI-compatible /models endpoint
apiBase := strings.TrimRight(h.resolveAPIBase(p), "/")
if apiBase == "" {
apiBase = "https://api.openai.com/v1"
}
models, err = fetchOpenAIModels(ctx, apiBase, p.APIKey)
apiBase := openAIModelsAPIBase(p.ProviderType, h.resolveAPIBase(p))
models, err = fetchOpenAIModels(ctx, apiBase, p.APIKey, openAIModelsExtraHeaders(p.ProviderType))
}
if err != nil {
@@ -126,6 +123,28 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
respond(withReasoningCapabilities(models))
}
func openAIModelsAPIBase(providerType, apiBase string) string {
base := strings.TrimRight(apiBase, "/")
if base != "" {
return base
}
switch providerType {
case store.ProviderKimiCoding:
return store.KimiCodingDefaultAPIBase
default:
return "https://api.openai.com/v1"
}
}
func openAIModelsExtraHeaders(providerType string) map[string]string {
if providerType != store.ProviderKimiCoding {
return nil
}
return map[string]string{
"User-Agent": store.KimiCodingRequiredUserAgent,
}
}
func reasoningDefaultsForModels(
settings []byte,
models []ModelInfo,
+4 -1
View File
@@ -91,12 +91,15 @@ func fetchGeminiModels(ctx context.Context, apiKey string) ([]ModelInfo, error)
}
// fetchOpenAIModels calls an OpenAI-compatible /models endpoint.
func fetchOpenAIModels(ctx context.Context, apiBase, apiKey string) ([]ModelInfo, error) {
func fetchOpenAIModels(ctx context.Context, apiBase, apiKey string, extraHeaders map[string]string) ([]ModelInfo, error) {
req, err := http.NewRequestWithContext(ctx, "GET", apiBase+"/models", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
for k, v := range extraHeaders {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
+67
View File
@@ -179,6 +179,73 @@ func TestProvidersHandlerListProviderModelsOpenAICompatAnnotatesKnownModels(t *t
}
}
func TestProvidersHandlerListProviderModelsKimiCodingSendsRequiredUserAgent(t *testing.T) {
token := setupProvidersAdminToken(t)
var capturedAuth, capturedUserAgent string
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedAuth = r.Header.Get("Authorization")
capturedUserAgent = r.Header.Get("User-Agent")
if r.URL.Path != "/models" {
http.NotFound(w, r)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]string{
{"id": store.KimiCodingDefaultModel},
},
})
}))
t.Cleanup(upstream.Close)
providerStore := newMockProviderStore()
provider := &store.LLMProviderData{
BaseModel: store.BaseModel{ID: uuid.New()},
Name: "kimi-coding",
ProviderType: store.ProviderKimiCoding,
APIBase: upstream.URL,
APIKey: "kimi-key",
Enabled: true,
}
if err := providerStore.CreateProvider(t.Context(), provider); err != nil {
t.Fatalf("CreateProvider() error = %v", err)
}
handler := NewProvidersHandler(providerStore, newMockSecretsStore(), nil, "")
mux := http.NewServeMux()
handler.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/v1/providers/"+provider.ID.String()+"/models", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status code = %d, want %d, body=%s", w.Code, http.StatusOK, w.Body.String())
}
if capturedAuth != "Bearer kimi-key" {
t.Fatalf("Authorization = %q, want Bearer kimi-key", capturedAuth)
}
if capturedUserAgent != store.KimiCodingRequiredUserAgent {
t.Fatalf("User-Agent = %q, want %q", capturedUserAgent, store.KimiCodingRequiredUserAgent)
}
var result ProviderModelsResponse
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
t.Fatalf("Decode() error = %v", err)
}
if len(result.Models) != 1 || result.Models[0].ID != store.KimiCodingDefaultModel {
t.Fatalf("models = %#v, want %q", result.Models, store.KimiCodingDefaultModel)
}
}
func TestOpenAIModelsAPIBaseDefaultsKimiCoding(t *testing.T) {
if got := openAIModelsAPIBase(store.ProviderKimiCoding, ""); got != store.KimiCodingDefaultAPIBase {
t.Fatalf("Kimi default api base = %q, want %q", got, store.KimiCodingDefaultAPIBase)
}
if got := openAIModelsAPIBase(store.ProviderOpenAICompat, ""); got != "https://api.openai.com/v1" {
t.Fatalf("OpenAI compat default api base = %q", got)
}
}
// TestProvidersHandlerListProviderModelsOllamaRichMetadata verifies that the
// handler fetches /api/tags from Ollama and maps rich details (family,
// parameter_size, quantization_level) into the display name.