mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 12:14:40 +00:00
fix(memory): guard provider embedding dimensions to 1536 schema (#563)
- Reject incompatible explicit provider embedding dimensions on create/update - Ignore previously saved incompatible dimensions at runtime, fall back to 1536 - Share RequiredMemoryEmbeddingDimensions constant from store package - Remove dimensions input from provider UI (always 1536 per pgvector schema) - Add HTTP, runtime, and validation unit tests Closes #548 Supersedes #410
This commit is contained in:
1 parent
53932cf532
commit
65d0d62acf
14 files changed
+312
-47
No files matched your search
@@ -123,11 +123,12 @@ func buildEmbeddingProvider(
|
||||
apiBase = memCfg.EmbeddingAPIBase
|
||||
}
|
||||
|
||||
// Dimension truncation: provider settings override, default to 1536 to match pgvector schema.
|
||||
// Dimension truncation: default to RequiredMemoryEmbeddingDimensions to match pgvector schema.
|
||||
// Models that natively output 1536 ignore the parameter; models with larger native dims get truncated.
|
||||
dims := 1536
|
||||
if es != nil && es.Dimensions > 0 {
|
||||
dims = es.Dimensions
|
||||
dims := store.RequiredMemoryEmbeddingDimensions
|
||||
if es != nil && es.Dimensions > 0 && es.Dimensions != store.RequiredMemoryEmbeddingDimensions {
|
||||
slog.Warn("ignoring incompatible provider embedding dimensions for memory schema",
|
||||
"provider", dbp.Name, "requested", es.Dimensions, "required", store.RequiredMemoryEmbeddingDimensions)
|
||||
}
|
||||
|
||||
// Try registry first for the actual API key / base (handles runtime-registered providers)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
func captureEmbeddingRequest(t *testing.T, es *store.EmbeddingSettings) map[string]any {
|
||||
t.Helper()
|
||||
|
||||
var requestBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
t.Fatalf("Decode() error = %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":[{"embedding":[0.1,0.2]}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := &store.LLMProviderData{
|
||||
Name: "embedding-provider",
|
||||
ProviderType: store.ProviderOpenAICompat,
|
||||
APIKey: "test-key",
|
||||
APIBase: server.URL,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
ep := buildEmbeddingProvider(provider, es, nil, nil)
|
||||
if ep == nil {
|
||||
t.Fatal("buildEmbeddingProvider() = nil, want provider")
|
||||
}
|
||||
if _, err := ep.Embed(context.Background(), []string{"hello"}); err != nil {
|
||||
t.Fatalf("Embed() error = %v", err)
|
||||
}
|
||||
return requestBody
|
||||
}
|
||||
|
||||
func TestBuildEmbeddingProviderDefaultsTo1536Dimensions(t *testing.T) {
|
||||
requestBody := captureEmbeddingRequest(t, nil)
|
||||
if got := requestBody["dimensions"]; got != float64(1536) {
|
||||
t.Fatalf("dimensions = %v, want 1536", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEmbeddingProviderIgnoresIncompatibleStoredDimensions(t *testing.T) {
|
||||
requestBody := captureEmbeddingRequest(t, &store.EmbeddingSettings{
|
||||
Enabled: true,
|
||||
Model: "voyage-4-nano",
|
||||
Dimensions: 2048,
|
||||
})
|
||||
if got := requestBody["dimensions"]; got != float64(1536) {
|
||||
t.Fatalf("dimensions = %v, want fallback 1536", got)
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,9 @@ func (s *seedStubStore) ListUserContextFilesByName(_ context.Context, _ uuid.UUI
|
||||
func (s *seedStubStore) DeleteUserContextFile(_ context.Context, _ uuid.UUID, _, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (s *seedStubStore) MigrateUserDataOnMerge(_ context.Context, _ []string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remaining interface methods — not exercised.
|
||||
func (s *seedStubStore) Create(_ context.Context, _ *store.AgentData) error { return nil }
|
||||
@@ -111,10 +114,6 @@ func (s *seedStubStore) EnsureUserProfile(_ context.Context, _ uuid.UUID, _ stri
|
||||
func (s *seedStubStore) PropagateContextFile(_ context.Context, _ uuid.UUID, _ string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (s *seedStubStore) MigrateUserDataOnMerge(_ context.Context, _ []string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- Tests ----
|
||||
|
||||
// TestSeedUserFiles_PredefinedAgent_UsesAgentLevelUserMD is the primary regression test.
|
||||
|
||||
@@ -83,6 +83,9 @@ func (s *createCaptureStore) ListUserContextFilesByName(_ context.Context, _ uui
|
||||
func (s *createCaptureStore) DeleteUserContextFile(_ context.Context, _ uuid.UUID, _, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (s *createCaptureStore) MigrateUserDataOnMerge(_ context.Context, _ []string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (s *createCaptureStore) GetUserOverride(_ context.Context, _ uuid.UUID, _ string) (*store.UserAgentOverrideData, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -104,10 +107,6 @@ func (s *createCaptureStore) EnsureUserProfile(_ context.Context, _ uuid.UUID, _
|
||||
func (s *createCaptureStore) PropagateContextFile(_ context.Context, _ uuid.UUID, _ string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (s *createCaptureStore) MigrateUserDataOnMerge(_ context.Context, _ []string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
// minimalConfig returns a config sufficient for handleCreate (provider + model defaults only).
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
// Provider-level embedding settings are used by the memory system, whose
|
||||
// PostgreSQL schema currently stores fixed vector(1536) embeddings.
|
||||
func validateProviderEmbeddingSettings(p *store.LLMProviderData) error {
|
||||
es := store.ParseEmbeddingSettings(p.Settings)
|
||||
if es == nil || !es.Enabled {
|
||||
return nil
|
||||
}
|
||||
if es.Dimensions < 0 {
|
||||
return fmt.Errorf("embedding.dimensions must be a positive integer or omitted")
|
||||
}
|
||||
if es.Dimensions > 0 && es.Dimensions != store.RequiredMemoryEmbeddingDimensions {
|
||||
return fmt.Errorf(
|
||||
"embedding.dimensions must be %d or omitted because GoClaw memory stores vector(%d)",
|
||||
store.RequiredMemoryEmbeddingDimensions,
|
||||
store.RequiredMemoryEmbeddingDimensions,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
func embeddingProvider(dims int) *store.LLMProviderData {
|
||||
settings := map[string]any{
|
||||
"embedding": map[string]any{
|
||||
"enabled": true,
|
||||
"model": "test-model",
|
||||
"dimensions": dims,
|
||||
},
|
||||
}
|
||||
raw, _ := json.Marshal(settings)
|
||||
return &store.LLMProviderData{Settings: raw}
|
||||
}
|
||||
|
||||
func TestValidateProviderEmbeddingSettings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p *store.LLMProviderData
|
||||
wantErr bool
|
||||
}{
|
||||
{"nil settings", &store.LLMProviderData{}, false},
|
||||
{"embedding disabled", &store.LLMProviderData{
|
||||
Settings: json.RawMessage(`{"embedding":{"enabled":false}}`),
|
||||
}, false},
|
||||
{"dimensions omitted (0)", &store.LLMProviderData{
|
||||
Settings: json.RawMessage(`{"embedding":{"enabled":true,"model":"m"}}`),
|
||||
}, false},
|
||||
{"dimensions 1536", embeddingProvider(1536), false},
|
||||
{"dimensions 2048", embeddingProvider(2048), true},
|
||||
{"dimensions 1024", embeddingProvider(1024), true},
|
||||
{"dimensions negative", embeddingProvider(-1), true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateProviderEmbeddingSettings(tt.p)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("validateProviderEmbeddingSettings() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ type ProvidersHandler struct {
|
||||
cliMu sync.Mutex // serializes Claude CLI provider create to prevent duplicates
|
||||
msgBus *bus.MessageBus
|
||||
sysConfigStore store.SystemConfigStore
|
||||
tracingStore store.TracingStore // optional: for provider-scoped pool activity
|
||||
tracingStore store.TracingStore // optional: for provider-scoped pool activity
|
||||
agents store.AgentCRUDStore // optional: for provider pool activity agent lookup
|
||||
}
|
||||
|
||||
@@ -311,6 +311,10 @@ func (h *ProvidersHandler) handleCreateProvider(w http.ResponseWriter, r *http.R
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := validateProviderEmbeddingSettings(&p); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateProviderURL(p.APIBase); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
@@ -448,6 +452,10 @@ func (h *ProvidersHandler) handleUpdateProvider(w http.ResponseWriter, r *http.R
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := validateProviderEmbeddingSettings(&candidate); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
// Track old name before update for registry cleanup
|
||||
var oldName string
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/crypto"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
@@ -50,3 +56,134 @@ func TestProvidersHandlerRegisterInMemoryAppliesCodexPoolDefaults(t *testing.T)
|
||||
t.Fatalf("ExtraProviderNames = %#v, want [\"codex-work\"]", defaults.ExtraProviderNames)
|
||||
}
|
||||
}
|
||||
|
||||
func setupProvidersAdminToken(t *testing.T) string {
|
||||
t.Helper()
|
||||
token := "system-admin-key"
|
||||
setupTestCache(t, map[string]*store.APIKeyData{
|
||||
crypto.HashAPIKey(token): {
|
||||
ID: uuid.New(),
|
||||
Scopes: []string{"operator.admin"},
|
||||
},
|
||||
})
|
||||
return token
|
||||
}
|
||||
|
||||
func TestProvidersHandlerCreateRejectsIncompatibleEmbeddingDimensions(t *testing.T) {
|
||||
token := setupProvidersAdminToken(t)
|
||||
providerStore := newMockProviderStore()
|
||||
handler := NewProvidersHandler(providerStore, newMockSecretsStore(), nil, "")
|
||||
mux := http.NewServeMux()
|
||||
handler.RegisterRoutes(mux)
|
||||
|
||||
body := `{
|
||||
"name": "voyage",
|
||||
"provider_type": "openai_compat",
|
||||
"api_base": "https://api.voyageai.com/v1",
|
||||
"enabled": true,
|
||||
"settings": {
|
||||
"embedding": {
|
||||
"enabled": true,
|
||||
"model": "voyage-4-nano",
|
||||
"dimensions": 2048
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/providers", bytes.NewBufferString(body))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status code = %d, want %d", w.Code, http.StatusBadRequest)
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "1536") {
|
||||
t.Fatalf("response body = %q, want mention of 1536", w.Body.String())
|
||||
}
|
||||
if len(providerStore.providers) != 0 {
|
||||
t.Fatalf("provider store mutated on invalid create: %#v", providerStore.providers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersHandlerCreateAllows1536EmbeddingDimensions(t *testing.T) {
|
||||
token := setupProvidersAdminToken(t)
|
||||
providerStore := newMockProviderStore()
|
||||
handler := NewProvidersHandler(providerStore, newMockSecretsStore(), nil, "")
|
||||
mux := http.NewServeMux()
|
||||
handler.RegisterRoutes(mux)
|
||||
|
||||
body := `{
|
||||
"name": "gemini-emb",
|
||||
"provider_type": "gemini_native",
|
||||
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||
"api_key": "token",
|
||||
"enabled": true,
|
||||
"settings": {
|
||||
"embedding": {
|
||||
"enabled": true,
|
||||
"model": "gemini-embedding-001",
|
||||
"dimensions": 1536
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/providers", bytes.NewBufferString(body))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("status code = %d, want %d, body=%s", w.Code, http.StatusCreated, w.Body.String())
|
||||
}
|
||||
if len(providerStore.providers) != 1 {
|
||||
t.Fatalf("provider count = %d, want 1", len(providerStore.providers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersHandlerUpdateRejectsIncompatibleEmbeddingDimensions(t *testing.T) {
|
||||
token := setupProvidersAdminToken(t)
|
||||
providerStore := newMockProviderStore()
|
||||
provider := &store.LLMProviderData{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
Name: "voyage",
|
||||
ProviderType: store.ProviderOpenAICompat,
|
||||
APIBase: "https://api.voyageai.com/v1",
|
||||
Enabled: true,
|
||||
Settings: json.RawMessage(`{"embedding":{"enabled":true,"model":"voyage-4-nano","dimensions":1536}}`),
|
||||
}
|
||||
if err := providerStore.CreateProvider(context.Background(), provider); err != nil {
|
||||
t.Fatalf("CreateProvider() error = %v", err)
|
||||
}
|
||||
|
||||
handler := NewProvidersHandler(providerStore, newMockSecretsStore(), nil, "")
|
||||
mux := http.NewServeMux()
|
||||
handler.RegisterRoutes(mux)
|
||||
|
||||
body := `{
|
||||
"settings": {
|
||||
"embedding": {
|
||||
"enabled": true,
|
||||
"model": "voyage-4-nano",
|
||||
"dimensions": 2048
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/v1/providers/"+provider.ID.String(), bytes.NewBufferString(body))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status code = %d, want %d", w.Code, http.StatusBadRequest)
|
||||
}
|
||||
current, err := providerStore.GetProvider(context.Background(), provider.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetProvider() error = %v", err)
|
||||
}
|
||||
es := store.ParseEmbeddingSettings(current.Settings)
|
||||
if es == nil || es.Dimensions != 1536 {
|
||||
t.Fatalf("embedding dimensions = %+v, want 1536 preserved", es)
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,10 @@ type LLMProviderData struct {
|
||||
Settings json.RawMessage `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
// RequiredMemoryEmbeddingDimensions is the fixed vector size used by the pgvector memory schema.
|
||||
// All memory embeddings must match this dimensionality until the schema supports variable sizes.
|
||||
const RequiredMemoryEmbeddingDimensions = 1536
|
||||
|
||||
// EmbeddingSettings holds embedding-specific configuration stored in provider settings JSONB.
|
||||
type EmbeddingSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
@@ -43,6 +43,9 @@ func (s *stubAgentStore) DeleteUserContextFile(_ context.Context, _ uuid.UUID, _
|
||||
func (s *stubAgentStore) ListUserContextFilesByName(_ context.Context, _ uuid.UUID, _ string) ([]store.UserContextFileData, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubAgentStore) MigrateUserDataOnMerge(_ context.Context, _ []string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remaining interface methods — not exercised in these tests.
|
||||
func (s *stubAgentStore) Create(_ context.Context, _ *store.AgentData) error { return nil }
|
||||
@@ -93,10 +96,6 @@ func (s *stubAgentStore) EnsureUserProfile(_ context.Context, _ uuid.UUID, _ str
|
||||
func (s *stubAgentStore) PropagateContextFile(_ context.Context, _ uuid.UUID, _ string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (s *stubAgentStore) MigrateUserDataOnMerge(_ context.Context, _ []string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- Tests ----
|
||||
|
||||
// TestInterceptor_CacheHit verifies that a second read does NOT call GetAgentContextFiles again.
|
||||
|
||||
@@ -237,7 +237,8 @@
|
||||
"enableDesc": "Allow this provider to generate text embeddings for memory, skills, and knowledge graph",
|
||||
"model": "Embedding Model",
|
||||
"dimensions": "Output Dimensions",
|
||||
"dimensionsHint": "Truncate output to N dimensions (e.g. 1536). Required for models with native dims > 1536 like gemini-embedding-001 (3072d).",
|
||||
"dimensionsHint": "GoClaw memory currently stores embeddings in vector(1536). Leave this empty when the model already outputs 1536, or set it to exactly 1536 when the provider supports truncation from larger native dimensions.",
|
||||
"dimensionsInvalid": "Only 1536 is currently supported by GoClaw memory.",
|
||||
"apiBase": "Embedding API Base",
|
||||
"apiBasePlaceholder": "(same as provider)",
|
||||
"apiBaseHint": "Override if embedding endpoint differs from the provider's main API base",
|
||||
|
||||
@@ -269,7 +269,8 @@
|
||||
"enableDesc": "Cho phép provider này tạo embedding cho bộ nhớ, kỹ năng và đồ thị tri thức",
|
||||
"model": "Model Embedding",
|
||||
"dimensions": "Số chiều đầu ra",
|
||||
"dimensionsHint": "Cắt ngắn đầu ra về N chiều (VD: 1536). Bắt buộc với model có chiều gốc > 1536 như gemini-embedding-001 (3072d).",
|
||||
"dimensionsHint": "Bộ nhớ GoClaw hiện lưu embedding với vector(1536). Để trống nếu model đã xuất 1536 chiều, hoặc đặt đúng 1536 nếu provider hỗ trợ cắt từ chiều gốc lớn hơn.",
|
||||
"dimensionsInvalid": "Hiện tại bộ nhớ GoClaw chỉ hỗ trợ đúng 1536 chiều.",
|
||||
"apiBase": "API Base Embedding",
|
||||
"apiBasePlaceholder": "(giống provider)",
|
||||
"apiBaseHint": "Ghi đè nếu endpoint embedding khác với API base chính của provider",
|
||||
|
||||
@@ -287,7 +287,8 @@
|
||||
"enableDesc": "允许此Provider为记忆、技能和知识图谱生成文本嵌入",
|
||||
"model": "Embedding模型",
|
||||
"dimensions": "输出维度",
|
||||
"dimensionsHint": "将输出截断为N维(如1536)。对于原生维度 > 1536的模型(如gemini-embedding-001 3072d)必须设置。",
|
||||
"dimensionsHint": "GoClaw memory 当前以 vector(1536) 存储 embedding。若模型本身输出 1536 维则留空;若 provider 支持从更高原生维度截断,则只能设置为 1536。",
|
||||
"dimensionsInvalid": "GoClaw memory 当前只支持精确的 1536 维。",
|
||||
"apiBase": "Embedding API地址",
|
||||
"apiBasePlaceholder": "(与Provider相同)",
|
||||
"apiBaseHint": "如果Embedding端点与Provider的主API地址不同,可在此覆盖",
|
||||
|
||||
@@ -96,7 +96,6 @@ function providerFormSignature(input: {
|
||||
embEnabled: boolean;
|
||||
embModel: string;
|
||||
embApiBase: string;
|
||||
embDimensions: string;
|
||||
routing: ChatGPTOAuthRoutingConfig;
|
||||
isOAuth: boolean;
|
||||
}): string {
|
||||
@@ -107,7 +106,6 @@ function providerFormSignature(input: {
|
||||
embEnabled: input.embEnabled,
|
||||
embModel: input.embModel,
|
||||
embApiBase: input.embApiBase,
|
||||
embDimensions: input.embDimensions,
|
||||
routing: input.isOAuth ? routingSignature(input.routing) : "",
|
||||
});
|
||||
}
|
||||
@@ -178,7 +176,6 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
const [embEnabled, setEmbEnabled] = useState(initEmb?.enabled ?? false);
|
||||
const [embModel, setEmbModel] = useState(initEmb?.model ?? "");
|
||||
const [embApiBase, setEmbApiBase] = useState(initEmb?.api_base ?? "");
|
||||
const [embDimensions, setEmbDimensions] = useState(initEmb?.dimensions ? String(initEmb.dimensions) : "");
|
||||
const syncedProviderIDRef = useRef(provider.id);
|
||||
|
||||
const savedFormSignature = useMemo(
|
||||
@@ -192,14 +189,13 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
embEnabled: initEmb?.enabled ?? false,
|
||||
embModel: initEmb?.model ?? "",
|
||||
embApiBase: initEmb?.api_base ?? "",
|
||||
embDimensions: initEmb?.dimensions ? String(initEmb.dimensions) : "",
|
||||
routing: {
|
||||
strategy: initialRouting?.strategy ?? "primary_first",
|
||||
extra_provider_names: initialRouting?.extraProviderNames ?? [],
|
||||
},
|
||||
isOAuth,
|
||||
}),
|
||||
[initEmb?.api_base, initEmb?.dimensions, initEmb?.enabled, initEmb?.model, initialRouting?.extraProviderNames, initialRouting?.strategy, isOAuth, provider.api_key, provider.display_name, provider.enabled, showApiKey],
|
||||
[initEmb?.api_base, initEmb?.enabled, initEmb?.model, initialRouting?.extraProviderNames, initialRouting?.strategy, isOAuth, provider.api_key, provider.display_name, provider.enabled, showApiKey],
|
||||
);
|
||||
const savedFormSignatureRef = useRef(savedFormSignature);
|
||||
const draftFormSignature = useMemo(
|
||||
@@ -213,11 +209,10 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
embEnabled,
|
||||
embModel,
|
||||
embApiBase,
|
||||
embDimensions,
|
||||
routing: poolRouting,
|
||||
isOAuth,
|
||||
}),
|
||||
[apiKey, displayName, embApiBase, embDimensions, embEnabled, embModel, enabled, isOAuth, poolRouting, provider.api_key, showApiKey],
|
||||
[apiKey, displayName, embApiBase, embEnabled, embModel, enabled, isOAuth, poolRouting, provider.api_key, showApiKey],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -228,7 +223,6 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
setEmbEnabled(es?.enabled ?? false);
|
||||
setEmbModel(es?.model ?? "");
|
||||
setEmbApiBase(es?.api_base ?? "");
|
||||
setEmbDimensions(es?.dimensions ? String(es.dimensions) : "");
|
||||
setPoolRouting({
|
||||
strategy: routing?.strategy ?? "primary_first",
|
||||
extra_provider_names: routing?.extraProviderNames ?? [],
|
||||
@@ -322,7 +316,7 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
} = useProviderCodexPoolActivity(provider.id, 8, isPoolOwner);
|
||||
|
||||
const { verifyEmbedding, embVerifying, embResult, resetEmb } = useProviderVerify();
|
||||
useEffect(() => { resetEmb(); }, [embModel, embDimensions, resetEmb]);
|
||||
useEffect(() => { resetEmb(); }, [embModel, resetEmb]);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -332,8 +326,6 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
const nextDisplayName = displayName.trim();
|
||||
const nextEmbModel = embModel.trim();
|
||||
const nextEmbAPIBase = embApiBase.trim();
|
||||
const parsedDims = embDimensions ? parseInt(embDimensions, 10) : 0;
|
||||
const nextEmbDimensions = parsedDims > 0 ? String(parsedDims) : "";
|
||||
const submittedAPIKey = showApiKey && apiKey && apiKey !== "***" ? apiKey : "";
|
||||
const data: ProviderInput = {
|
||||
name: provider.name,
|
||||
@@ -354,7 +346,6 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
enabled: true,
|
||||
model: nextEmbModel || undefined,
|
||||
api_base: nextEmbAPIBase || undefined,
|
||||
dimensions: parsedDims > 0 ? parsedDims : undefined,
|
||||
}
|
||||
: { enabled: false },
|
||||
};
|
||||
@@ -371,7 +362,6 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
setDisplayName(nextDisplayName);
|
||||
setEmbModel(nextEmbModel);
|
||||
setEmbApiBase(nextEmbAPIBase);
|
||||
setEmbDimensions(nextEmbDimensions);
|
||||
if (submittedAPIKey) {
|
||||
setApiKey("***");
|
||||
}
|
||||
@@ -388,8 +378,7 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
};
|
||||
|
||||
const handleVerifyEmbedding = () => {
|
||||
const parsedDims = embDimensions ? parseInt(embDimensions, 10) : 0;
|
||||
verifyEmbedding(provider.id, embModel.trim() || undefined, parsedDims > 0 ? parsedDims : undefined);
|
||||
verifyEmbedding(provider.id, embModel.trim() || undefined, undefined);
|
||||
};
|
||||
|
||||
const displayNameDirty = displayName !== (provider.display_name || "");
|
||||
@@ -400,8 +389,7 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
const embeddingDirty =
|
||||
embEnabled !== (initEmb?.enabled ?? false)
|
||||
|| embModel !== (initEmb?.model ?? "")
|
||||
|| embApiBase !== (initEmb?.api_base ?? "")
|
||||
|| embDimensions !== (initEmb?.dimensions ? String(initEmb.dimensions) : "");
|
||||
|| embApiBase !== (initEmb?.api_base ?? "");
|
||||
const routingDirty = isOAuth && routingSignature(poolRouting) !== routingSignature({
|
||||
strategy: initialRouting?.strategy ?? "primary_first",
|
||||
extra_provider_names: initialRouting?.extraProviderNames ?? [],
|
||||
@@ -534,16 +522,8 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="embDimensions">{t("embedding.dimensions")}</Label>
|
||||
<Input
|
||||
id="embDimensions"
|
||||
type="number"
|
||||
value={embDimensions}
|
||||
onChange={(e) => setEmbDimensions(e.target.value)}
|
||||
placeholder="1536"
|
||||
min={1}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
<Label>{t("embedding.dimensions")}</Label>
|
||||
<p className="text-sm text-muted-foreground">1536</p>
|
||||
<p className="text-xs text-muted-foreground">{t("embedding.dimensionsHint")}</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user