mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-06 16:24:22 +00:00
- Add `dimensions` field to EmbeddingSettings for per-provider truncation - Default to 1536 dims to match pgvector schema, preventing INSERT failures - Replace hardcoded Gemini-only dimension truncation with data-driven config - Fix verify endpoint to apply dimensions and return mismatch warning - Expand curated embedding models: Gemini (gemini-embedding-001), Cohere (embed-v4), DashScope (text-embedding-v3), OpenAI (text-embedding-3-large) - Add dimensions input to provider detail UI with i18n (en/vi/zh) - Add anthropic_native to NoEmbeddingTypes (incompatible auth format) - System settings verify always requests 1536d to match production behavior
151 lines
4.6 KiB
Go
151 lines
4.6 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/memory"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// handleVerifyEmbedding tests a provider's embedding capability with a minimal API call.
|
|
//
|
|
// POST /v1/providers/{id}/verify-embedding
|
|
// Body: {"model": "text-embedding-3-small"} (optional, falls back to settings.embedding.model)
|
|
// Response: {"valid": true, "dimensions": 1536} or {"valid": false, "error": "..."}
|
|
func (h *ProvidersHandler) handleVerifyEmbedding(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
id, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "provider")})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Model string `json:"model"`
|
|
Dimensions int `json:"dimensions"` // optional: truncate output to N dims
|
|
}
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16)).Decode(&req); err != nil && err.Error() != "EOF" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
|
return
|
|
}
|
|
|
|
p, err := h.store.GetProvider(r.Context(), id)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "provider", id.String())})
|
|
return
|
|
}
|
|
|
|
// Provider types that cannot serve embeddings
|
|
if store.NoEmbeddingTypes[p.ProviderType] {
|
|
writeJSON(w, http.StatusOK, map[string]any{"valid": false, "error": "provider type does not support embeddings"})
|
|
return
|
|
}
|
|
|
|
// Parse embedding settings once for model/apiBase/dimensions resolution.
|
|
es := store.ParseEmbeddingSettings(p.Settings)
|
|
|
|
// Resolve model: request body → settings.embedding.model → default
|
|
model := req.Model
|
|
if model == "" && es != nil && es.Model != "" {
|
|
model = es.Model
|
|
}
|
|
if model == "" {
|
|
model = "text-embedding-3-small"
|
|
}
|
|
|
|
// Resolve API base: settings.embedding.api_base → provider api_base → resolved base
|
|
apiBase := h.resolveAPIBase(p)
|
|
if es != nil && es.APIBase != "" {
|
|
apiBase = es.APIBase
|
|
}
|
|
|
|
ep := memory.NewOpenAIEmbeddingProvider(p.Name, p.APIKey, apiBase, model)
|
|
|
|
// Apply dimension truncation: request body → provider settings → none.
|
|
// Clamp to reasonable range to avoid sending absurd values upstream.
|
|
truncDims := req.Dimensions
|
|
if truncDims <= 0 && es != nil && es.Dimensions > 0 {
|
|
truncDims = es.Dimensions
|
|
}
|
|
if truncDims > 0 && truncDims <= 8192 {
|
|
ep.WithDimensions(truncDims)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
vectors, embErr := ep.Embed(ctx, []string{"test"})
|
|
if embErr != nil {
|
|
writeJSON(w, http.StatusOK, map[string]any{"valid": false, "error": friendlyVerifyError(embErr)})
|
|
return
|
|
}
|
|
|
|
dims := 0
|
|
if len(vectors) > 0 && len(vectors[0]) > 0 {
|
|
dims = len(vectors[0])
|
|
}
|
|
result := map[string]any{"valid": true, "dimensions": dims}
|
|
if dims > 0 && dims != 1536 {
|
|
result["dimension_mismatch"] = true
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
// handleEmbeddingStatus returns the current embedding system configuration.
|
|
//
|
|
// GET /v1/embedding/status
|
|
// Response: {"configured": true, "provider": "openai", "model": "text-embedding-3-small"}
|
|
// or: {"configured": false}
|
|
func (h *ProvidersHandler) handleEmbeddingStatus(w http.ResponseWriter, r *http.Request) {
|
|
// Primary: check system_configs for embedding.provider/model
|
|
if h.sysConfigStore != nil {
|
|
provName, _ := h.sysConfigStore.Get(r.Context(), "embedding.provider")
|
|
model, _ := h.sysConfigStore.Get(r.Context(), "embedding.model")
|
|
if provName != "" {
|
|
if model == "" {
|
|
model = "text-embedding-3-small"
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"configured": true,
|
|
"provider": provName,
|
|
"provider_name": provName,
|
|
"model": model,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Fallback: check provider-level embedding settings
|
|
providerList, err := h.store.ListProviders(r.Context())
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, map[string]any{"configured": false})
|
|
return
|
|
}
|
|
for _, p := range providerList {
|
|
if !p.Enabled || store.NoEmbeddingTypes[p.ProviderType] {
|
|
continue
|
|
}
|
|
es := store.ParseEmbeddingSettings(p.Settings)
|
|
if es != nil && es.Enabled {
|
|
model := es.Model
|
|
if model == "" {
|
|
model = "text-embedding-3-small"
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"configured": true,
|
|
"provider": p.DisplayName,
|
|
"provider_name": p.Name,
|
|
"model": model,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"configured": false})
|
|
}
|