From b0cac0ac9dc88dddfde183f62a7d940af07dc9cd Mon Sep 17 00:00:00 2001 From: Lynn Date: Mon, 13 Jul 2026 11:20:02 +0800 Subject: [PATCH] Fix: Align part of Go provider APIs with Python APIs (#16823) --- .../google_scholar_component_test.go | 6 +- internal/cli/cli.go | 2 +- internal/dao/tenant_model.go | 25 ++ internal/dao/tenant_model_instance.go | 12 + internal/entity/types.go | 8 + internal/handler/providers.go | 39 +- internal/service/model_service.go | 416 +++++++++++++++++- web/src/locales/en.ts | 3 +- .../agent/form/compilation-form/index.tsx | 4 +- .../pages/next-chats/chat/llm-select-form.tsx | 4 +- 10 files changed, 498 insertions(+), 21 deletions(-) diff --git a/internal/agent/component/google_scholar_component_test.go b/internal/agent/component/google_scholar_component_test.go index 197af7ba4b..fbb91df323 100644 --- a/internal/agent/component/google_scholar_component_test.go +++ b/internal/agent/component/google_scholar_component_test.go @@ -81,9 +81,9 @@ func TestGoogleScholar_InvokeMergesNodeParams(t *testing.T) { fake := &fakeGoogleScholarInvoker{} c := newGoogleScholarComponentWithInvoker(fake, map[string]any{ - "top_n": float64(20), - "sort_by": "date", - "patents": false, + "top_n": float64(20), + "sort_by": "date", + "patents": false, }) _, err := c.Invoke(context.Background(), map[string]any{ diff --git a/internal/cli/cli.go b/internal/cli/cli.go index b7aefedd87..6dddba6283 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -834,7 +834,7 @@ Commands (User Mode): USE MODEL 'provider/instance/model'; - Set current model for chat CHAT 'message'; - Chat using current model CHAT 'provider/instance/model' 'message'; - Chat with specified model - OPENAI_CHAT 'chat_id' 'message' [options] ; - OpenAI-compatible chat + OPENAI_CHAT 'chat_id' 'message' [options] ; - OpenAI-compatible chat (run openai_chat -h for detailed options) CHAT COMPLETIONS 'question' [options] ; - Chat completions via /api/v1/chat/completions (run chat completions -h for detailed options) diff --git a/internal/dao/tenant_model.go b/internal/dao/tenant_model.go index 2fb235ef97..ae2085e8e2 100644 --- a/internal/dao/tenant_model.go +++ b/internal/dao/tenant_model.go @@ -123,6 +123,31 @@ func (dao *TenantModelDAO) GetModelsByInstanceID(instanceID string) ([]*entity.T return models, nil } +// DeleteByIDs deletes all models whose id is in the given list. +// Mirrors Python's TenantModelService.delete_by_ids. +func (dao *TenantModelDAO) DeleteByIDs(ids []string) (int64, error) { + if len(ids) == 0 { + return 0, nil + } + result := DB.Unscoped().Where("id IN ?", ids).Delete(&entity.TenantModel{}) + return result.RowsAffected, result.Error +} + +// UpdateByID updates a tenant model's model_type and extra by primary key. +// Mirrors Python's TenantModelService.update_model. +func (dao *TenantModelDAO) UpdateByID(id string, updates map[string]interface{}) error { + return DB.Model(&entity.TenantModel{}).Where("id = ?", id).Updates(updates).Error +} + +// DeleteByInstanceIDs deletes all models whose instance_id is in the given list. +func (dao *TenantModelDAO) DeleteByInstanceIDs(instanceIDs []string) (int64, error) { + if len(instanceIDs) == 0 { + return 0, nil + } + result := DB.Unscoped().Where("instance_id IN ?", instanceIDs).Delete(&entity.TenantModel{}) + return result.RowsAffected, result.Error +} + // GetModelsByProviderIDsAndInstanceIDs returns TenantModel rows whose // provider_id is in providerIDs and instance_id is in instanceIDs. // Mirrors Python's diff --git a/internal/dao/tenant_model_instance.go b/internal/dao/tenant_model_instance.go index 2be481dede..a48e539e55 100644 --- a/internal/dao/tenant_model_instance.go +++ b/internal/dao/tenant_model_instance.go @@ -110,3 +110,15 @@ func (dao *TenantModelInstanceDAO) DeleteByProviderIDAndInstanceName(providerID, result := DB.Unscoped().Where("provider_id = ? and instance_name = ?", providerID, instanceName).Delete(&entity.TenantModelInstance{}) return result.RowsAffected, result.Error } + +// UpdateByID updates a tenant model instance by primary key. +// Mirrors Python's TenantModelInstanceService.update_by_id. +func (dao *TenantModelInstanceDAO) UpdateByID(id string, updates map[string]interface{}) error { + return DB.Model(&entity.TenantModelInstance{}).Where("id = ?", id).Updates(updates).Error +} + +// DeleteByProviderID deletes all instances for the given provider. +func (dao *TenantModelInstanceDAO) DeleteByProviderID(providerID string) (int64, error) { + result := DB.Unscoped().Where("provider_id = ?", providerID).Delete(&entity.TenantModelInstance{}) + return result.RowsAffected, result.Error +} diff --git a/internal/entity/types.go b/internal/entity/types.go index db32883b9f..da267f39b0 100644 --- a/internal/entity/types.go +++ b/internal/entity/types.go @@ -141,3 +141,11 @@ func ModelTypeFromStrings(types []string) ModelType { } return result } + +// ModelVerifyStatus mirrors Python's ModelVerifyStatusEnum +// (common/constants.py). +const ( + ModelVerifySuccess = "success" + ModelVerifyFail = "fail" + ModelVerifyUnknown = "unknown" +) diff --git a/internal/handler/providers.go b/internal/handler/providers.go index e674b8b6d8..29092d87ec 100644 --- a/internal/handler/providers.go +++ b/internal/handler/providers.go @@ -175,10 +175,11 @@ func (h *ProviderHandler) ShowModel(c *gin.Context) { } type CreateProviderInstanceRequest struct { - InstanceName string `json:"instance_name" binding:"required"` - APIKey string `json:"api_key"` - BaseURL string `json:"base_url"` - Region string `json:"region"` + InstanceName string `json:"instance_name" binding:"required"` + APIKey string `json:"api_key"` + BaseURL string `json:"base_url"` + Region string `json:"region"` + ModelInfo []service.CreateInstanceModelInfo `json:"model_info"` } func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) { @@ -196,7 +197,20 @@ func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) { userID := c.GetString("user_id") - _, err := h.modelProviderService.CreateProviderInstance(providerName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, userID) + // If the request body only contains "instance_name", create a name-only + // instance without API key validation or model creation. + // Mirrors Python's provider_api.py:349 — set(data.keys()) == {"instance_name"}. + if req.APIKey == "" && req.BaseURL == "" && req.Region == "" && len(req.ModelInfo) == 0 { + code, err := h.modelProviderService.CreateNameOnlyProviderInstance(providerName, req.InstanceName, userID) + if err != nil { + common.ErrorWithCode(c, code, err.Error()) + return + } + common.SuccessWithMessage(c, "success") + return + } + + _, err := h.modelProviderService.CreateProviderInstance(providerName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, userID, req.ModelInfo) if err != nil { common.ErrorWithCode(c, common.CodeServerError, err.Error()) return @@ -388,8 +402,12 @@ func (h *ProviderHandler) ShowTask(c *gin.Context) { } type AlterProviderInstanceRequest struct { - InstanceName string `json:"instance_name"` - APIKey string `json:"api_key"` + InstanceName string `json:"instance_name"` + APIKey string `json:"api_key"` + BaseURL string `json:"base_url"` + Region string `json:"region"` + ModelInfo []service.CreateInstanceModelInfo `json:"model_info"` + Verify *bool `json:"verify"` } func (h *ProviderHandler) AlterProviderInstance(c *gin.Context) { @@ -417,7 +435,12 @@ func (h *ProviderHandler) AlterProviderInstance(c *gin.Context) { return } - code, err := h.modelProviderService.AlterProviderInstance(userID, providerName, instanceName, req.InstanceName, req.APIKey) + verify := true + if req.Verify != nil { + verify = *req.Verify + } + + code, err := h.modelProviderService.AlterProviderInstance(userID, providerName, instanceName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, req.ModelInfo, verify) if err != nil { common.ErrorWithCode(c, code, err.Error()) return diff --git a/internal/service/model_service.go b/internal/service/model_service.go index 2b235fcbe7..a2d18e16fb 100644 --- a/internal/service/model_service.go +++ b/internal/service/model_service.go @@ -228,12 +228,35 @@ func (m *ModelProviderService) ListProvidersOfTenant(userID string) ([]map[strin return nil, common.CodeServerError, err } provider["name"] = providerName + + // Set has_instance flag. Mirrors Python's: + // provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, name) + // has_instance = bool(provider_obj and TenantModelInstanceService.get_all_by_provider_id(provider_obj.id)) + provider["has_instance"] = m.providerHasInstance(tenantID, providerName) + result = append(result, provider) } return result, common.CodeSuccess, nil } +// providerHasInstance checks whether the given tenant's provider has any +// configured instances. Mirrors the Python pattern: +// +// provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, name) +// has_instance = bool(provider_obj and TenantModelInstanceService.get_all_by_provider_id(provider_obj.id)) +func (m *ModelProviderService) providerHasInstance(tenantID, providerName string) bool { + providerObj, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName) + if err != nil { + return false + } + instances, err := m.modelInstanceDAO.GetAllInstancesByProviderID(providerObj.ID) + if err != nil { + return false + } + return len(instances) > 0 +} + // isExcludedTenantProvider returns true for system-pool names that the Python // implementation (api/apps/services/provider_api_service.py:108) intentionally // filters out when listing a tenant's providers. @@ -255,6 +278,30 @@ func (m *ModelProviderService) DeleteModelProvider(userID, providerName string) } tenantID := tenants[0].TenantID + // Find the provider first. + provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName) + if err != nil { + return common.CodeNotFound, fmt.Errorf("provider %s not found", providerName) + } + + // Delete all models and instances under this provider. + instances, err := m.modelInstanceDAO.GetAllInstancesByProviderID(provider.ID) + if err != nil { + return common.CodeServerError, err + } + if len(instances) > 0 { + instanceIDs := make([]string, len(instances)) + for i, inst := range instances { + instanceIDs[i] = inst.ID + } + if _, err := m.modelDAO.DeleteByInstanceIDs(instanceIDs); err != nil { + return common.CodeServerError, err + } + if _, err := m.modelInstanceDAO.DeleteByProviderID(provider.ID); err != nil { + return common.CodeServerError, err + } + } + _, err = m.modelProviderDAO.DeleteByTenantIDAndProviderName(tenantID, providerName) if err != nil { return common.CodeServerError, err @@ -339,7 +386,16 @@ func (m *ModelProviderService) ListSupportedModels(providerName, instanceName, u return result, nil } -func (m *ModelProviderService) CreateProviderInstance(providerName, instanceName, apiKey, baseURL, region, userID string) (common.ErrorCode, error) { +// CreateInstanceModelInfo mirrors Python's model_info dict in +// create_provider_instance. See api/apps/services/provider_api_service.py:434. +type CreateInstanceModelInfo struct { + ModelName string `json:"model_name"` + ModelTypes []string `json:"model_type"` + MaxTokens int `json:"max_tokens"` + Extra map[string]interface{} `json:"extra"` +} + +func (m *ModelProviderService) CreateProviderInstance(providerName, instanceName, apiKey, baseURL, region, userID string, modelInfo []CreateInstanceModelInfo) (common.ErrorCode, error) { providerName = strings.TrimSpace(providerName) providerName = strings.ToLower(providerName) @@ -361,19 +417,28 @@ func (m *ModelProviderService) CreateProviderInstance(providerName, instanceName return common.CodeServerError, err } + // Normalize api_key: VLLM with empty api_key defaults to "x". + // Mirrors Python's _normalize_provider_api_key. + if strings.EqualFold(providerName, "vllm") && apiKey == "" { + apiKey = "x" + } + + // Verify the API key against the provider. + // Mirrors Python's verify_api_key (provider_api_service.py:596). + modelVerifyResult := m.verifyProviderAPIKey(providerName, apiKey, region, baseURL, modelInfo) + instanceID := utility.GenerateToken() extra := make(map[string]string) extra["region"] = region extra["base_url"] = baseURL - // convert extra to string extraByte, err := json.Marshal(extra) if err != nil { return common.CodeServerError, errors.New("fail to marshal extra") } extraStr := string(extraByte) - tenantModelProvider := &entity.TenantModelInstance{ + tenantModelInstance := &entity.TenantModelInstance{ ID: instanceID, InstanceName: instanceName, ProviderID: provider.ID, @@ -381,14 +446,187 @@ func (m *ModelProviderService) CreateProviderInstance(providerName, instanceName Status: "active", Extra: extraStr, } - err = m.modelInstanceDAO.Create(tenantModelProvider) - + err = m.modelInstanceDAO.Create(tenantModelInstance) if err != nil { return common.CodeServerError, fmt.Errorf("fail to create model instance: %s", err.Error()) } + + // Add models with verify result in extra. + if len(modelInfo) > 0 { + for _, model := range modelInfo { + if model.Extra == nil { + model.Extra = make(map[string]interface{}) + } + verifyStatus := modelVerifyResult[model.ModelName] + if verifyStatus == "" { + verifyStatus = entity.ModelVerifyUnknown + } + model.Extra["verify"] = verifyStatus + if err := m.addModelToInstance(tenantID, providerName, instanceName, model); err != nil { + return common.CodeServerError, err + } + } + } + return common.CodeSuccess, nil } +// CreateNameOnlyProviderInstance creates a provider instance with only a name, +// skipping API key validation and model creation. Mirrors Python's +// create_name_only_provider_instance in provider_api_service.py:536. +func (m *ModelProviderService) CreateNameOnlyProviderInstance(providerName, instanceName, userID string) (common.ErrorCode, error) { + providerName = strings.TrimSpace(providerName) + providerName = strings.ToLower(providerName) + + if instanceName == "default" { + return common.CodeBadRequest, errors.New("instance name cannot be 'default'") + } + + tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner") + if err != nil { + return common.CodeServerError, err + } + if len(tenants) == 0 { + return common.CodeNotFound, errors.New("user has no tenants") + } + tenantID := tenants[0].TenantID + + provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName) + if err != nil { + return common.CodeNotFound, fmt.Errorf("provider '%s' does not exist", providerName) + } + + instanceID := utility.GenerateToken() + + tenantModelInstance := &entity.TenantModelInstance{ + ID: instanceID, + InstanceName: instanceName, + ProviderID: provider.ID, + APIKey: "", + Status: "active", + Extra: "{}", + } + err = m.modelInstanceDAO.Create(tenantModelInstance) + if err != nil { + return common.CodeServerError, fmt.Errorf("fail to create model instance: %s", err.Error()) + } + + return common.CodeSuccess, nil +} + +// verifyProviderAPIKey verifies the API key against the provider by calling +// the driver's CheckConnection. It returns a map from model name to verify +// status (success/fail/unknown). Mirrors Python's verify_api_key in +// provider_api_service.py:596. +func (m *ModelProviderService) verifyProviderAPIKey(providerName, apiKey, region, baseURL string, modelInfo []CreateInstanceModelInfo) map[string]string { + result := make(map[string]string) + + providerInfo := dao.GetModelProviderManager().FindProvider(providerName) + if providerInfo == nil { + // Provider not in system pool — mark all models as unknown. + for _, model := range modelInfo { + result[model.ModelName] = entity.ModelVerifyUnknown + } + return result + } + + apiKey = strings.TrimSpace(apiKey) + region = strings.TrimSpace(region) + baseURL = strings.TrimSpace(baseURL) + if region == "" { + region = "default" + } + + driver := providerInfo.ModelDriver + if strings.EqualFold(providerInfo.Class, "local") { + var err error + driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL) + if err != nil { + for _, model := range modelInfo { + result[model.ModelName] = entity.ModelVerifyFail + } + return result + } + } + + apiConfig := &modelModule.APIConfig{ + ApiKey: &apiKey, + Region: ®ion, + BaseURL: &baseURL, + } + + verifyErr := driver.CheckConnection(apiConfig) + verifyStatus := entity.ModelVerifySuccess + if verifyErr != nil { + verifyStatus = entity.ModelVerifyFail + } + + for _, model := range modelInfo { + result[model.ModelName] = verifyStatus + } + return result +} + +// addModelToInstance creates a single model under the given provider instance. +// Mirrors Python's add_model_to_instance in provider_api_service.py:969. +func (m *ModelProviderService) addModelToInstance(tenantID, providerName, instanceName string, model CreateInstanceModelInfo) error { + provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName) + if err != nil { + return fmt.Errorf("no provider found for provider '%s'", providerName) + } + + instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceName) + if err != nil { + return fmt.Errorf("no instance found for provider '%s' and instance '%s'", providerName, instanceName) + } + + // Check for duplicate model. + _, err = m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(provider.ID, instance.ID, model.ModelName) + if err == nil { + return fmt.Errorf("model '%s' already exists for provider '%s' and instance '%s'", model.ModelName, providerName, instanceName) + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + // Compute model type bitmask. + combinedType := entity.ModelType(0) + for _, t := range model.ModelTypes { + combinedType |= entity.ModelTypeFromString(t) + } + + // Build extra fields. Mirrors Python's extra_fields in add_model_to_instance. + extraFields := map[string]interface{}{ + "max_tokens": model.MaxTokens, + } + if model.Extra != nil { + for k, v := range model.Extra { + extraFields[k] = v + } + } + extraBytes, err := json.Marshal(extraFields) + if err != nil { + return fmt.Errorf("fail to marshal extra: %s", err.Error()) + } + + modelID := utility.GenerateToken() + tenantModel := &entity.TenantModel{ + ID: modelID, + ModelName: model.ModelName, + ModelType: int(combinedType), + ProviderID: provider.ID, + InstanceID: instance.ID, + Status: "active", + Extra: string(extraBytes), + } + + if err := m.modelDAO.Create(tenantModel); err != nil { + return fmt.Errorf("fail to create model '%s': %s", model.ModelName, err.Error()) + } + + return nil +} + func (m *ModelProviderService) ListProviderInstances(providerName, userID string) ([]map[string]interface{}, common.ErrorCode, error) { providerName = strings.TrimSpace(providerName) providerName = strings.ToLower(providerName) @@ -999,7 +1237,173 @@ func (m *ModelProviderService) resolveModelListTenant(userID, ownerTenantID stri return tenant, common.CodeSuccess, nil } -func (m *ModelProviderService) AlterProviderInstance(userID, providerName, instanceName, newInstanceName, apiKey string) (common.ErrorCode, error) { +func (m *ModelProviderService) AlterProviderInstance(userID, providerName, instanceIDOrName, newInstanceName, apiKey, baseURL, region string, modelInfo []CreateInstanceModelInfo, verify bool) (common.ErrorCode, error) { + providerName = strings.TrimSpace(providerName) + providerName = strings.ToLower(providerName) + + tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner") + if err != nil { + return common.CodeServerError, err + } + if len(tenants) == 0 { + return common.CodeNotFound, errors.New("user has no tenants") + } + tenantID := tenants[0].TenantID + + provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName) + if err != nil { + return common.CodeNotFound, fmt.Errorf("provider '%s' does not exist", providerName) + } + + // Find the instance — try by ID first, then by name. + instance, err := m.modelInstanceDAO.GetByID(instanceIDOrName) + if err != nil || instance.ProviderID != provider.ID { + instance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceIDOrName) + } + if err != nil { + return common.CodeNotFound, fmt.Errorf("no instance found for provider '%s' and instance '%s'", providerName, instanceIDOrName) + } + + // Normalize api_key: VLLM with empty api_key defaults to "x". + if strings.EqualFold(providerName, "vllm") && apiKey == "" { + apiKey = "x" + } + + // Verify API key if requested. + modelVerifyResult := make(map[string]string) + if verify { + modelVerifyResult = m.verifyProviderAPIKey(providerName, apiKey, region, baseURL, modelInfo) + } + + // Update instance record. + instanceUpdates := map[string]interface{}{ + "api_key": apiKey, + } + if newInstanceName != "" && newInstanceName != instance.InstanceName { + instanceUpdates["instance_name"] = newInstanceName + } + + extraFields := make(map[string]interface{}) + if baseURL != "" { + extraFields["base_url"] = baseURL + } + if region != "" { + extraFields["region"] = region + } + // Preserve existing extra fields not overwritten. + existingExtra := make(map[string]interface{}) + if instance.Extra != "" { + if err := json.Unmarshal([]byte(instance.Extra), &existingExtra); err != nil { + return common.CodeServerError, err + } + } + for k, v := range extraFields { + existingExtra[k] = v + } + extraBytes, err := json.Marshal(existingExtra) + if err != nil { + return common.CodeServerError, err + } + instanceUpdates["extra"] = string(extraBytes) + if err := m.modelInstanceDAO.UpdateByID(instance.ID, instanceUpdates); err != nil { + return common.CodeServerError, fmt.Errorf("fail to update instance: %s", err.Error()) + } + + // Use the (possibly updated) instance_name for model operations. + effectiveInstanceName := instance.InstanceName + if newInstanceName != "" { + effectiveInstanceName = newInstanceName + } + + // Upsert models: add new ones, update existing ones, remove ones no longer selected. + existingModels, err := m.modelDAO.GetModelsByInstanceID(instance.ID) + if err != nil { + return common.CodeServerError, err + } + existingModelMap := make(map[string]*entity.TenantModel) + for _, mdl := range existingModels { + existingModelMap[mdl.ModelName] = mdl + } + + // Delete models that are no longer in the submitted model_info. + submittedModelNames := make(map[string]bool) + if modelInfo != nil { + for _, mdl := range modelInfo { + if mdl.ModelName != "" { + submittedModelNames[mdl.ModelName] = true + } + } + } + var idsToRemove []string + for name, mdl := range existingModelMap { + if !submittedModelNames[name] { + idsToRemove = append(idsToRemove, mdl.ID) + } + } + if len(idsToRemove) > 0 { + if _, err := m.modelDAO.DeleteByIDs(idsToRemove); err != nil { + return common.CodeServerError, err + } + } + + // Add or update models from model_info. + if modelInfo != nil { + for _, mdl := range modelInfo { + if mdl.ModelName == "" { + continue + } + // Attach verify status. + if verify { + verifyStatus := modelVerifyResult[mdl.ModelName] + if verifyStatus == "" { + verifyStatus = entity.ModelVerifyUnknown + } + if mdl.Extra == nil { + mdl.Extra = make(map[string]interface{}) + } + mdl.Extra["verify"] = verifyStatus + } + + if existingMdl, exists := existingModelMap[mdl.ModelName]; exists { + // Update existing model. + updates := make(map[string]interface{}) + if len(mdl.ModelTypes) > 0 { + combinedType := entity.ModelType(0) + for _, t := range mdl.ModelTypes { + combinedType |= entity.ModelTypeFromString(t) + } + if int(combinedType) != existingMdl.ModelType { + updates["model_type"] = int(combinedType) + } + } + mergedExtra := make(map[string]interface{}) + if existingMdl.Extra != "" { + json.Unmarshal([]byte(existingMdl.Extra), &mergedExtra) + } + if mdl.Extra != nil { + for k, v := range mdl.Extra { + mergedExtra[k] = v + } + } + if mdl.MaxTokens > 0 { + mergedExtra["max_tokens"] = mdl.MaxTokens + } + extraBytes, _ := json.Marshal(mergedExtra) + updates["extra"] = string(extraBytes) + if len(updates) > 0 { + if err := m.modelDAO.UpdateByID(existingMdl.ID, updates); err != nil { + return common.CodeServerError, err + } + } + } else { + // Add new model. + if err := m.addModelToInstance(tenantID, providerName, effectiveInstanceName, mdl); err != nil { + return common.CodeServerError, err + } + } + } + } + return common.CodeSuccess, nil } diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index ab7ebffd66..c377907ee4 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -2401,7 +2401,8 @@ Best for: Documents with flowing, contextually connected content — such as boo bGPTDescription: 'Search scientific papers via BGPT and return structured evidence from full-text studies: methods, sample sizes, limitations, conflicts of interest, data availability, blind spots, and falsification prompts. Optional API key after the free tier.', bgptApiKey: 'API key', - bgptApiKeyTip: 'Optional. Leave blank for the free tier (first 50 results).', + bgptApiKeyTip: + 'Optional. Leave blank for the free tier (first 50 results).', bgptDaysBack: 'Days back', bgptDaysBackTip: 'Optional recency filter (e.g. 365 for the last year).', email: 'Email', diff --git a/web/src/pages/agent/form/compilation-form/index.tsx b/web/src/pages/agent/form/compilation-form/index.tsx index f1b043c4b2..5c08a5b6f0 100644 --- a/web/src/pages/agent/form/compilation-form/index.tsx +++ b/web/src/pages/agent/form/compilation-form/index.tsx @@ -38,7 +38,9 @@ const CompilationForm = ({ node }: INextOperatorForm) => { return (
- + diff --git a/web/src/pages/next-chats/chat/llm-select-form.tsx b/web/src/pages/next-chats/chat/llm-select-form.tsx index 072122170a..f7b54a12c0 100644 --- a/web/src/pages/next-chats/chat/llm-select-form.tsx +++ b/web/src/pages/next-chats/chat/llm-select-form.tsx @@ -29,7 +29,9 @@ export function LLMSelectForm() { return ( - +
); }