Fix: align go provider apis with python apis (#16867)

### Summary

As title.

---------

Co-authored-by: Jin Hai <haijin.chn@gmail.com>
Co-authored-by: Wang Qi <wangq8@outlook.com>
This commit is contained in:
Lynn
2026-07-14 21:06:11 +08:00
committed by GitHub
parent 55d5879622
commit bd7a3bb878
18 changed files with 790 additions and 577 deletions

View File

@@ -157,6 +157,8 @@ RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
fi || \
{ echo "Failed to install ODBC driver"; exit 1; }
# Add dependencies of selenium
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/chrome-linux64-121-0-6167-85,target=/chrome-linux64.zip \
unzip /chrome-linux64.zip && \

View File

@@ -641,9 +641,7 @@ async def update_provider_instance(tenant_id: str = None, provider_id_or_name: s
verify = data.get("verify", True)
try:
success, msg = await provider_api_service.update_provider_instance(
tenant_id, provider_id_or_name, instance_id_or_name, instance_name, api_key, base_url, region, model_info, verify
)
success, msg = await provider_api_service.update_provider_instance(tenant_id, provider_id_or_name, instance_id_or_name, instance_name, api_key, base_url, region, model_info, verify)
if success:
return get_result(message=msg)
else:
@@ -961,13 +959,12 @@ async def delete_models_from_instance(tenant_id: str, provider_id_or_name: str,
return get_error_data_result(message="Internal server error")
@manager.route("/providers/<provider_id_or_name>/instances/<instance_id_or_name>/models/<path:model_name>", methods=["PATCH"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
async def alter_model(tenant_id: str = None, provider_id_or_name: str = None, instance_id_or_name: str = None, model_name: str = None):
"""
Enable or disable a model, or update max_tokens
Alter a model's status, max_tokens, model_type, or extra fields.
---
tags:
- Providers
@@ -996,35 +993,40 @@ async def alter_model(tenant_id: str = None, provider_id_or_name: str = None, in
description: Bearer token for authentication.
- in: body
name: body
description: Model status update.
description: Model update parameters.
required: true
schema:
type: object
required:
- status
properties:
status:
type: string
enum: ["active", "inactive"]
description: Model status.
max_tokens:
type: integer
description: Maximum number of tokens.
model_type:
type: string
description: Model type (e.g. "chat", "embedding").
extra:
type: object
description: Additional model configuration.
responses:
200:
description: Model status updated.
description: Model updated.
schema:
type: object
"""
data = await request.get_json()
if not data or ("status" not in data and "max_tokens" not in data):
return get_error_argument_result(message="status or max_tokens required.")
if not data or not any(k in data for k in ("status", "max_tokens", "model_type", "extra")):
return get_error_argument_result(message="At least one of status, max_tokens, model_type, or extra is required.")
update_dict = {k: data[k] for k in ["status", "max_tokens", "model_type", "extra"] if k in data}
if update_dict.get("status") and update_dict["status"] not in ("active", "inactive"):
return get_error_argument_result(message="status must be 'active' or 'inactive'")
try:
success, msg = provider_api_service.update_model(
tenant_id, provider_id_or_name, instance_id_or_name, model_name, update_dict
)
success, msg = provider_api_service.update_model(tenant_id, provider_id_or_name, instance_id_or_name, model_name, update_dict)
if success:
return get_result(message=msg)
else:

View File

@@ -28,7 +28,7 @@ _LEGACY_MODEL_TYPE_ALIASES = {
}
def calculate_model_type(model_type_name_list: List[str]|str) -> int:
def calculate_model_type(model_type_name_list: List[str] | str) -> int:
model_type = 0
if isinstance(model_type_name_list, str):
model_type_name_list = [model_type_name_list]

View File

@@ -94,7 +94,7 @@ class LLMType(StrEnum):
class ModelTypeBinary(Enum):
CHAT = 0b0000001 # 1 << 0 = 1
CHAT = 0b0000001 # 1 << 0 = 1
EMBEDDING = 0b0000010 # 1 << 1 = 2
ASR = 0b0000100 # 1 << 2 = 4
VISION = 0b0001000 # 1 << 3 = 8

View File

@@ -199,46 +199,6 @@ func TestSplitCompositeLLMID(t *testing.T) {
}
}
func TestCategorize_ResolvesTenantLLMCredentials(t *testing.T) {
db := setupComponentTestDB(t)
pushComponentDB(t, db)
apiKey := "tenant-llm-key"
apiBase := "https://tenant-llm.example"
modelName := "Qwen/Qwen3-8B"
if err := db.Create(&entity.TenantLLM{
TenantID: "tenant-1",
LLMFactory: "SILICONFLOW",
LLMName: &modelName,
APIKey: &apiKey,
APIBase: &apiBase,
Status: "1",
}).Error; err != nil {
t.Fatalf("create tenant_llm: %v", err)
}
stub := &stubInvoker{resp: &ChatInvokeResponse{Content: "support", Model: "stub"}}
withStubInvoker(t, stub)
c := NewCategorizeComponent(CategorizeParam{
ModelID: "Qwen/Qwen3-8B@default@SILICONFLOW",
Categories: []string{"sales", "support"},
DefaultCategory: "support",
})
_, err := c.Invoke(stateWithTenant("tenant-1"), map[string]any{})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
if stub.captured == nil {
t.Fatal("invoker not called")
}
if stub.captured.APIKey != apiKey {
t.Fatalf("APIKey=%q, want %q", stub.captured.APIKey, apiKey)
}
if stub.captured.BaseURL != apiBase {
t.Fatalf("BaseURL=%q, want %q", stub.captured.BaseURL, apiBase)
}
}
func TestCategorize_ResolvesTenantModelInstanceCredentials(t *testing.T) {
db := setupComponentTestDB(t)
pushComponentDB(t, db)
@@ -387,7 +347,6 @@ func setupComponentTestDB(t *testing.T) *gorm.DB {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(
&entity.TenantLLM{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},

View File

@@ -733,6 +733,12 @@ int32_t RAGAnalyzer::Load() {
int32_t freq = std::stoi(results[1]);
freq = int32_t(std::log(float(freq) / DENOMINATOR) + 0.5);
int32_t pos_idx = pos_table_->GetPOSIndex(results[2]);
// If the POS tag is not in the POS table (e.g. "eng" for
// English words), default to index 0. Using -1 would cause
// Encode() to produce a negative int32_t which the darts
// trie rejects.
if (pos_idx < 0)
pos_idx = 0;
int value = Encode(freq, pos_idx);
trie_->Add(results[0], value);
std::string rkey = RKey(results[0]);

View File

@@ -169,6 +169,7 @@ type Model struct {
MaxDimension *int `json:"max_dimension"` // used by embedding models
Dimensions []int `json:"dimensions"`
Alias []string `json:"alias"`
Rank *int `json:"rank"`
ModelTypeMap map[string]bool
}
@@ -488,7 +489,7 @@ func (pm *ProviderManager) ListModels(providerName string) ([]map[string]interfa
modelData := map[string]interface{}{
"name": model.Name,
"max_tokens": model.MaxTokens,
"model_type": model.ModelTypes,
"model_types": model.ModelTypes,
"max_dimension": model.MaxDimension,
"dimensions": model.Dimensions,
}
@@ -513,7 +514,7 @@ func (pm *ProviderManager) GetModelByName(providerName, modelName string) (*Mode
if provider == nil {
return nil, fmt.Errorf("provider '%s' not found", providerName)
}
model := pm.findModel(provider, modelName)
model := pm.FindModel(provider, modelName)
if model == nil {
return nil, fmt.Errorf("model '%s' not found", modelName)
}
@@ -525,7 +526,7 @@ func (pm *ProviderManager) GetModelUrl(providerName, modelName, modelType string
if provider == nil {
return nil, nil, fmt.Errorf("provider '%s' not found", providerName)
}
model := pm.findModel(provider, modelName)
model := pm.FindModel(provider, modelName)
if model == nil {
return nil, nil, fmt.Errorf("model '%s' not found", modelName)
}
@@ -568,7 +569,7 @@ func (pm *ProviderManager) SearchModelInfo(providerName, modelName string, filte
return resp
}
model := pm.findModel(provider, modelName)
model := pm.FindModel(provider, modelName)
if model == nil {
resp.Code = 404
resp.Message = fmt.Sprintf("Model '%s' not found in provider '%s'", modelName, providerName)
@@ -794,7 +795,7 @@ func (pm *ProviderManager) FindProvider(name string) *Provider {
}
// Helper: Find model by name
func (pm *ProviderManager) findModel(provider *Provider, modelName string) *Model {
func (pm *ProviderManager) FindModel(provider *Provider, modelName string) *Model {
for i := range provider.Models {
if strings.EqualFold(provider.Models[i].Name, modelName) {
return provider.Models[i]

View File

@@ -244,16 +244,15 @@ func (h *ProviderHandler) ShowProviderInstance(c *gin.Context) {
return
}
instanceName := c.Param("instance_name")
if instanceName == "" {
instanceIDOrName := c.Param("instance_name")
if instanceIDOrName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
userID := c.GetString("user_id")
// Get tenant ID from user
instance, errorCode, err := h.modelProviderService.ShowProviderInstance(providerName, instanceName, userID)
instance, errorCode, err := h.modelProviderService.ShowProviderInstance(providerName, instanceIDOrName, userID)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
@@ -516,12 +515,15 @@ func (h *ProviderHandler) ListInstanceModels(c *gin.Context) {
common.SuccessWithData(c, modelInstances, "success")
}
type EnableOrDisableModelRequest struct {
ModelID string `json:"model_id"`
Status string `json:"status"`
type AlterModelRequest struct {
ModelID string `json:"model_id"`
Status string `json:"status"`
MaxTokens int `json:"max_tokens"`
ModelType interface{} `json:"model_type"`
Extra interface{} `json:"extra"`
}
func (h *ProviderHandler) EnableOrDisableModel(c *gin.Context) {
func (h *ProviderHandler) AlterModel(c *gin.Context) {
providerName := c.Param("provider_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
@@ -534,29 +536,43 @@ func (h *ProviderHandler) EnableOrDisableModel(c *gin.Context) {
return
}
var req EnableOrDisableModelRequest
var req AlterModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
println("JSON bind error: %v (type: %T)", err, err)
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
userID := c.GetString("user_id")
modelID := strings.TrimSpace(req.ModelID)
modelName := strings.TrimPrefix(c.Param("model_name"), "/")
modelName = strings.TrimSpace(modelName)
modelID := strings.TrimSpace(req.ModelID)
if modelName == "" && modelID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "model_name or model_id is required")
return
}
status := strings.TrimSpace(req.Status)
if status != "active" && status != "inactive" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "Status must be active or inactive")
if status != "" && status != "active" && status != "inactive" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "status must be 'active' or 'inactive'")
return
}
code, err := h.modelProviderService.UpdateModelStatus(providerName, instanceName, modelName, userID, modelID, status)
updateDict := make(map[string]interface{})
if status != "" {
updateDict["status"] = status
}
if req.MaxTokens > 0 {
updateDict["max_tokens"] = req.MaxTokens
}
if req.ModelType != nil {
updateDict["model_type"] = req.ModelType
}
if req.Extra != nil {
updateDict["extra"] = req.Extra
}
code, err := h.modelProviderService.AlterModel(providerName, instanceName, modelName, userID, modelID, updateDict)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
@@ -585,30 +601,6 @@ func prepareProviderInstance(providerName, instanceName, reqProviderName, reqIns
return nil
}
func prepareAddModelRequest(req *service.AddModelRequest, providerName, instanceName string) error {
if err := prepareProviderInstance(providerName, instanceName, req.ProviderName, req.InstanceName); err != nil {
return err
}
if len(req.Models) == 0 {
return errors.New("Models are required")
}
for _, model := range req.Models {
if model.ModelName == "" {
return errors.New("Model name is required")
}
if len(model.ModelTypes) == 0 {
return errors.New("Model type is required")
}
}
req.ProviderName = providerName
req.InstanceName = instanceName
return nil
}
func (h *ProviderHandler) AddModel(c *gin.Context) {
var req service.AddModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
@@ -617,8 +609,21 @@ func (h *ProviderHandler) AddModel(c *gin.Context) {
return
}
if err := prepareAddModelRequest(&req, c.Param("provider_name"), c.Param("instance_name")); err != nil {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeDataError, nil, err.Error())
req.ProviderName = c.Param("provider_name")
req.InstanceName = c.Param("instance_name")
if req.ProviderName == "" || req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "provider_name and instance_name are required")
return
}
if req.ModelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "model_name is required")
return
}
if len(req.ModelTypes) == 0 {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "model_type is required")
return
}
@@ -634,8 +639,7 @@ func (h *ProviderHandler) AddModel(c *gin.Context) {
}
type DropInstanceModelRequest struct {
ModelIDs []string `json:"model_ids"`
Models []string `json:"models"`
ModelNames []string `json:"model_name"`
}
func (h *ProviderHandler) DropInstanceModels(c *gin.Context) {
@@ -655,14 +659,14 @@ func (h *ProviderHandler) DropInstanceModels(c *gin.Context) {
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
if len(req.ModelIDs) == 0 && len(req.Models) == 0 {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "model_ids or models is required")
if len(req.ModelNames) == 0 {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "model_name is required")
return
}
userID := c.GetString("user_id")
code, err := h.modelProviderService.DropInstanceModels(providerName, instanceName, userID, req.ModelIDs, req.Models)
code, err := h.modelProviderService.DropInstanceModels(providerName, instanceName, userID, req.ModelNames)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return

View File

@@ -84,7 +84,7 @@ func decodeProviderHandlerResponse(t *testing.T, recorder *httptest.ResponseReco
return body
}
func TestProviderHandlerEnableOrDisableModelRejectsMissingModelSelector(t *testing.T) {
func TestProviderHandlerAlterModelRejectsMissingModelSelector(t *testing.T) {
ctx, recorder := newProviderHandlerRequest(
t,
map[string]interface{}{"status": "active"},
@@ -92,7 +92,7 @@ func TestProviderHandlerEnableOrDisableModelRejectsMissingModelSelector(t *testi
gin.Param{Key: "instance_name", Value: "default"},
)
NewProviderHandler(nil, service.NewModelProviderService()).EnableOrDisableModel(ctx)
NewProviderHandler(nil, service.NewModelProviderService()).AlterModel(ctx)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
@@ -103,7 +103,7 @@ func TestProviderHandlerEnableOrDisableModelRejectsMissingModelSelector(t *testi
}
}
func TestProviderHandlerEnableOrDisableModelRejectsInvalidStatus(t *testing.T) {
func TestProviderHandlerAlterModelRejectsInvalidStatus(t *testing.T) {
ctx, recorder := newProviderHandlerRequest(
t,
map[string]interface{}{"status": "disabled"},
@@ -112,7 +112,7 @@ func TestProviderHandlerEnableOrDisableModelRejectsInvalidStatus(t *testing.T) {
gin.Param{Key: "model_name", Value: "gpt-test"},
)
NewProviderHandler(nil, service.NewModelProviderService()).EnableOrDisableModel(ctx)
NewProviderHandler(nil, service.NewModelProviderService()).AlterModel(ctx)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
@@ -123,7 +123,7 @@ func TestProviderHandlerEnableOrDisableModelRejectsInvalidStatus(t *testing.T) {
}
}
func TestProviderHandlerEnableOrDisableModelUpdatesStatus(t *testing.T) {
func TestProviderHandlerAlterModelUpdatesStatus(t *testing.T) {
db := setupProviderHandlerTestDB(t)
useProviderHandlerTestDB(t, db)
seedProviderHandlerModel(t, db)
@@ -136,7 +136,7 @@ func TestProviderHandlerEnableOrDisableModelUpdatesStatus(t *testing.T) {
gin.Param{Key: "model_name", Value: "gpt-test"},
)
NewProviderHandler(nil, service.NewModelProviderService()).EnableOrDisableModel(ctx)
NewProviderHandler(nil, service.NewModelProviderService()).AlterModel(ctx)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String())

View File

@@ -539,7 +539,7 @@ func (r *Router) Setup(engine *gin.Engine) {
provider.PUT("/:provider_name/instances/:instance_name", r.providerHandler.AlterProviderInstance)
provider.DELETE("/:provider_name/instances", r.providerHandler.DropProviderInstance)
provider.GET("/:provider_name/instances/:instance_name/models", r.providerHandler.ListInstanceModels)
provider.PATCH("/:provider_name/instances/:instance_name/models/*model_name", r.providerHandler.EnableOrDisableModel)
provider.PATCH("/:provider_name/instances/:instance_name/models/*model_name", r.providerHandler.AlterModel)
provider.POST("/:provider_name/instances/:instance_name/models", r.providerHandler.AddModel)
provider.DELETE("/:provider_name/instances/:instance_name/models", r.providerHandler.DropInstanceModels)
v1.POST("/chat/to_model", r.providerHandler.ChatToModel)

View File

@@ -158,6 +158,11 @@ func TestRunAgent_RealCanvas_BeginMessage(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -240,6 +245,11 @@ func TestRunAgent_RealCanvas_WaitForUserResume(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -367,6 +377,11 @@ func TestRunAgent_RealCanvas_WaitForUserResume_EventSemantics(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -466,6 +481,11 @@ func TestRunAgent_RealCanvas_GroupedParallelOuterFollower(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -594,6 +614,11 @@ func TestRunAgent_AllFixture_LoopInterruptResume(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -698,6 +723,11 @@ func TestRunAgent_AllFixture_LoopInterruptResume_MultiTurn(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -786,6 +816,11 @@ func TestRunAgent_AllFixture_IterationFormatsItems(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -882,6 +917,11 @@ func TestRunAgent_AllFixture_VarAssigner(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -942,6 +982,11 @@ func TestRunAgent_AllFixture_DataOps(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -1018,6 +1063,11 @@ func TestRunAgent_RealCanvas_CompileFails(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -1090,6 +1140,11 @@ func TestRunAgent_AllFixture_CategorizeResume(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -1217,6 +1272,11 @@ func TestRunAgent_RealCanvas_InvokeFails(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
@@ -1319,6 +1379,11 @@ func TestRunAgent_RunTracker_AttachCheckpoint_CallSequence(t *testing.T) {
&entity.UserTenant{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.APIToken{},
&entity.API4Conversation{},
&entity.TenantModelProvider{},
&entity.TenantModelInstance{},
&entity.TenantModel{},
); err != nil {
t.Fatalf("migrate: %v", err)
}

View File

@@ -220,7 +220,9 @@ func TestGetVersion_WrongCanvas(t *testing.T) {
t.Helper()
if err := testDB.AutoMigrate(
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.UserTenant{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
@@ -248,7 +250,9 @@ func TestGetVersion_NotFound(t *testing.T) {
t.Helper()
if err := testDB.AutoMigrate(
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.UserTenant{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}

File diff suppressed because it is too large Load Diff

View File

@@ -155,14 +155,14 @@ func seedModelProviderServiceScope(t *testing.T, db *gorm.DB) {
}
}
func TestModelProviderServiceUpdateModelStatusByID(t *testing.T) {
func TestModelProviderServiceAlterModelStatusByID(t *testing.T) {
db := setupModelProviderServiceTestDB(t)
useModelProviderServiceTestDB(t, db)
seedModelProviderServiceScope(t, db)
code, err := NewModelProviderService().UpdateModelStatus("OpenAI", "default", "", "user-1", "model-1", "inactive")
code, err := NewModelProviderService().AlterModel("OpenAI", "default", "", "user-1", "model-1", map[string]interface{}{"status": "inactive"})
if err != nil {
t.Fatalf("UpdateModelStatus() error = %v", err)
t.Fatalf("AlterModel() error = %v", err)
}
if code != common.CodeSuccess {
t.Fatalf("code = %v, want %v", code, common.CodeSuccess)
@@ -197,23 +197,23 @@ func TestModelProviderServiceGetModelConfigByID(t *testing.T) {
}
}
func TestModelProviderServiceUpdateModelStatusRejectsInvalidStatus(t *testing.T) {
code, err := NewModelProviderService().UpdateModelStatus("OpenAI", "default", "", "user-1", "model-1", "disabled")
func TestModelProviderServiceAlterModelRejectsInvalidStatus(t *testing.T) {
code, err := NewModelProviderService().AlterModel("OpenAI", "default", "", "user-1", "model-1", map[string]interface{}{"status": "disabled"})
if err == nil {
t.Fatalf("UpdateModelStatus() error = nil, want invalid status error")
t.Fatalf("AlterModel() error = nil, want invalid status error")
}
if code != common.CodeBadRequest {
t.Fatalf("code = %v, want %v", code, common.CodeBadRequest)
}
if !strings.Contains(err.Error(), "status must be active or inactive") {
if !strings.Contains(err.Error(), "status must be") {
t.Fatalf("error = %v, want status validation message", err)
}
}
func TestModelProviderServiceUpdateModelStatusRejectsMissingModelSelector(t *testing.T) {
code, err := NewModelProviderService().UpdateModelStatus("OpenAI", "default", "", "user-1", "", "active")
func TestModelProviderServiceAlterModelRejectsMissingModelSelector(t *testing.T) {
code, err := NewModelProviderService().AlterModel("OpenAI", "default", "", "user-1", "", map[string]interface{}{"status": "active"})
if err == nil {
t.Fatalf("UpdateModelStatus() error = nil, want missing model selector error")
t.Fatalf("AlterModel() error = nil, want missing model selector error")
}
if code != common.CodeBadRequest {
t.Fatalf("code = %v, want %v", code, common.CodeBadRequest)
@@ -223,7 +223,7 @@ func TestModelProviderServiceUpdateModelStatusRejectsMissingModelSelector(t *tes
}
}
func TestModelProviderServiceUpdateModelStatusRejectsWrongScopedModelID(t *testing.T) {
func TestModelProviderServiceAlterModelRejectsWrongScopedModelID(t *testing.T) {
db := setupModelProviderServiceTestDB(t)
useModelProviderServiceTestDB(t, db)
seedModelProviderServiceScope(t, db)
@@ -231,9 +231,9 @@ func TestModelProviderServiceUpdateModelStatusRejectsWrongScopedModelID(t *testi
t.Fatalf("failed to seed second instance: %v", err)
}
code, err := NewModelProviderService().UpdateModelStatus("OpenAI", "other", "", "user-1", "model-1", "inactive")
code, err := NewModelProviderService().AlterModel("OpenAI", "other", "", "user-1", "model-1", map[string]interface{}{"status": "inactive"})
if err == nil {
t.Fatalf("UpdateModelStatus() error = nil, want not found error")
t.Fatalf("AlterModel() error = nil, want not found error")
}
if code != common.CodeNotFound {
t.Fatalf("code = %v, want %v", code, common.CodeNotFound)

View File

@@ -358,6 +358,7 @@ func (s *OpenAIChatService) OpenAIChatCompletions(c *gin.Context, userID, chatID
if result.Final {
finalContent := strings.TrimSpace(result.Answer)
fullContent = finalContent
finalReference = []FormattedChunk{}
if ref, ok := result.Reference["chunks"]; ok {
if chunks, ok := ref.([]map[string]interface{}); ok {
finalReference = formatChunks(chunks)
@@ -397,6 +398,7 @@ func (s *OpenAIChatService) OpenAIChatCompletions(c *gin.Context, userID, chatID
}
if finalReference == nil && openaiReq.NeedReference {
finalReference = []FormattedChunk{}
if ref, ok := lastResult.Reference["chunks"]; ok {
if chunks, ok := ref.([]map[string]interface{}); ok {
finalReference = formatChunks(chunks)
@@ -441,6 +443,7 @@ func (s *OpenAIChatService) OpenAIChatCompletions(c *gin.Context, userID, chatID
TotalTokens: promptTokens + completionTokens,
}
if openaiReq.NeedReference {
resp.Reference = []FormattedChunk{}
if ref, ok := finalResult.Reference["chunks"]; ok {
if chunks, ok := ref.([]map[string]interface{}); ok {
resp.Reference = formatChunks(chunks)

View File

@@ -489,71 +489,103 @@ func (s *TenantService) GetDefaultModelName(tenantID string, modelType entity.Mo
}
func (s *TenantService) GetModelInfo(tenantID string, defaultModel string, modelType string) (*string, *string, *string, bool, error) {
// normally the model string is: modelName@instanceName@providerName, sometimes it's just modelName@providerName
// for the 1st case, parse defaultChatModel into three parts
defaultChatModelParts := strings.Split(defaultModel, "@")
var providerName *string
var instanceName *string
var modelName *string
if len(defaultChatModelParts) >= 3 {
providerName = &defaultChatModelParts[len(defaultChatModelParts)-1]
instanceName = &defaultChatModelParts[len(defaultChatModelParts)-2]
joinedModelName := strings.Join(defaultChatModelParts[:len(defaultChatModelParts)-2], "@")
modelName = &joinedModelName
} else if len(defaultChatModelParts) == 2 {
providerName = &defaultChatModelParts[1]
instanceName = new(string)
*instanceName = "default"
modelName = &defaultChatModelParts[0]
} else {
return nil, nil, nil, false, fmt.Errorf("invalid model string: %s", defaultModel)
// Mirror Python's _get_model_info: right-anchored rsplit so that model
// names containing '@' (e.g. LM Studio IDs like
// "text-embedding-nomic-embed-text-v1.5@q8_0") remain intact.
// The composite key is: modelName@instanceName@providerName or
// modelName@providerName.
parts := rsplitN(defaultModel, "@", 2)
var modelName, instanceName, providerName string
switch len(parts) {
case 3:
modelName, instanceName, providerName = parts[0], parts[1], parts[2]
case 2:
modelName, providerName = parts[0], parts[1]
instanceName = "default"
default:
modelName = parts[0]
providerName = ""
instanceName = "default"
}
if modelType == "ocr" {
if *providerName == "infiniflow" && *instanceName == "default" && *modelName == "deepdoc" {
return providerName, instanceName, modelName, true, nil
}
// Special case: OCR with infiniflow@default@deepdoc is always enabled.
if modelType == "ocr" && providerName == "infiniflow" && instanceName == "default" && modelName == "deepdoc" {
return &providerName, &instanceName, &modelName, true, nil
}
// Check if the provider and instance exists
modelProvider, err := s.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, *providerName)
// Special case: TEI Builtin embedding model.
composeProfiles := common.GetEnv(common.EnvComposeProfiles)
teiModel := common.GetEnv(common.EnvTEIModel)
if modelType == "embedding" && strings.Contains(composeProfiles, "tei-") && teiModel != "" &&
modelName == teiModel && (providerName == "" || providerName == "Builtin") {
return &providerName, &instanceName, &modelName, true, nil
}
// Check if the provider exists for the tenant.
modelProvider, err := s.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
if err != nil {
return nil, nil, nil, false, err
}
modelInstance, err := s.modelInstanceDAO.GetByProviderIDAndInstanceName(modelProvider.ID, *instanceName)
// Check if the instance exists.
modelInstance, err := s.modelInstanceDAO.GetByProviderIDAndInstanceName(modelProvider.ID, instanceName)
if err != nil {
return nil, nil, nil, false, err
}
modelSchema, err := dao.GetModelProviderManager().GetModelByName(*providerName, *modelName)
// Validate that the factory model supports this model type.
modelSchema, err := dao.GetModelProviderManager().GetModelByName(providerName, modelName)
if err != nil {
return nil, nil, nil, false, err
}
factoryModelType := factoryModelTypeName(modelType)
if !modelSchema.ModelTypeMap[factoryModelType] {
return nil, nil, nil, false, fmt.Errorf("model %s isn't a %s model", *modelName, modelType)
return nil, nil, nil, false, fmt.Errorf("model %s isn't a %s model", modelName, modelType)
}
var modelEntity *entity.TenantModel
modelEntity, err = s.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(modelProvider.ID, modelInstance.ID, *modelName)
// Check if the model exists and is active.
modelEntity, err := s.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(modelProvider.ID, modelInstance.ID, modelName)
if err != nil {
errString := err.Error()
if !strings.Contains(errString, "record not found") {
if !dao.IsNotFoundErr(err) {
return nil, nil, nil, false, err
}
}
if modelEntity == nil {
return nil, nil, nil, false, fmt.Errorf("model %s isn't available", *modelName)
return nil, nil, nil, false, fmt.Errorf("model %s isn't available", modelName)
}
if modelEntity.Status != "active" {
return nil, nil, nil, false, fmt.Errorf("model %s isn't available", *modelName)
return nil, nil, nil, false, fmt.Errorf("model %s isn't available", modelName)
}
return providerName, instanceName, modelName, true, nil
return &providerName, &instanceName, &modelName, true, nil
}
// rsplitN splits s by sep from the right, limiting to n+1 parts (mirrors
// Python's str.rsplit(sep, maxsplit)).
func rsplitN(s, sep string, n int) []string {
if n <= 0 {
return []string{s}
}
result := make([]string, 0, n+1)
remaining := s
for i := 0; i < n; i++ {
idx := strings.LastIndex(remaining, sep)
if idx < 0 {
result = append(result, remaining)
// Reverse the collected parts.
for left, right := 0, len(result)-1; left < right; left, right = left+1, right-1 {
result[left], result[right] = result[right], result[left]
}
return result
}
result = append(result, remaining[idx+len(sep):])
remaining = remaining[:idx]
}
result = append(result, remaining)
for left, right := 0, len(result)-1; left < right; left, right = left+1, right-1 {
result[left], result[right] = result[right], result[left]
}
return result
}
func (s *TenantService) ListTenantDefaultModels(userID string) ([]ModelItem, error) {

View File

@@ -241,4 +241,5 @@ def test_openai_compatible_reference_metadata_fields_filter_accepts_array(rest_c
choice_msg = payload["choices"][0]["message"]
skip_if_go_proxy_upstream_error(choice_msg)
assert "reference" in choice_msg, payload
print(payload)
assert isinstance(choice_msg["reference"], list), payload

View File

@@ -27,14 +27,14 @@ import pytest
pytestmark = pytest.mark.p2
class _StubModelTypeBinary(IntEnum):
CHAT = 1
EMBEDDING = 2
ASR = 4
VISION = 8
RERANK = 16
TTS = 32
OCR = 64
class _StubModelTypeBinary(IntEnum):
CHAT = 1
EMBEDDING = 2
ASR = 4
VISION = 8
RERANK = 16
TTS = 32
OCR = 64
def _stub(monkeypatch, name, **attrs):