//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

package service

import (
	"context"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"ragflow/internal/common"
	"ragflow/internal/dao"
	"ragflow/internal/entity"
	modelModule "ragflow/internal/entity/models"
	"ragflow/internal/utility"
	"sort"
	"strings"

	"go.uber.org/zap"
	"gorm.io/gorm"
)

// parseModelName parses a composite model name in format "model@instance@provider" or "model@provider"
// Returns modelName, instanceName, providerName separately.
//
// The composite key is right-anchored: providerName is always the *last*
// '@'-separated field, instanceName is the second-to-last (when present),
// and everything to the left is the bare model name. Some model names
// legitimately contain '@' characters themselves (e.g. LM Studio embedding
// model IDs such as `text-embedding-nomic-embed-text-v1.5@q8_0`), which
// produces composite keys like
// `text-embedding-nomic-embed-text-v1.5@q8_0@lmstudio@LM-Studio`. When the
// split yields more than 3 fields we rejoin the leading fields back into the
// modelName so any embedded '@' characters are preserved verbatim.
func parseModelName(compositeName string) (modelName, instanceName, providerName string, err error) {
	parts := strings.Split(compositeName, "@")
	switch len(parts) {
	case 3:
		// Format: model@instance@provider
		return parts[0], parts[1], parts[2], nil
	case 2:
		// Format: model@provider -> instance defaults to "default"
		return parts[0], "default", parts[1], nil
	case 1:
		return parts[0], "", "", fmt.Errorf("provider name missing in model name: %s", compositeName)
	}
	// len(parts) > 3: any '@' characters embedded in the leftmost modelName
	// component must be preserved in that component instead of being dropped
	// or assigned to the instance/provider fields.
	n := len(parts)
	return strings.Join(parts[:n-2], "@"), parts[n-2], parts[n-1], nil
}

// splitRightAnchoredModelName is a bare-name-tolerant variant of
// parseModelName used by the Builtin / TEI short-circuit branches in
// GetModelConfigFromProviderInstance.
//
// Those branches must accept a bare model name (no provider suffix) where
// parseModelName would return an error, while still preserving any '@'
// characters embedded in the modelName portion of a multi-segment key.
// Returns the modelName, instanceName ("default" for the 2-segment form),
// and providerName ("" for the 1-segment form).
func splitRightAnchoredModelName(compositeName string) (modelName, instanceName, providerName string) {
	parts := strings.Split(compositeName, "@")
	switch len(parts) {
	case 3:
		return parts[0], parts[1], parts[2]
	case 2:
		// The 2-segment form "model@X" is ambiguous: X could be a provider
		// suffix (only "Builtin" is recognised by the TEI / Builtin
		// short-circuits that consume this helper) or part of the model
		// name itself (e.g. a quantization tag like "q8_0" in
		// "text-embedding-nomic-embed-text-v1.5@q8_0"). Treat the last
		// token as a provider only when it actually is one; otherwise
		// the whole string is the bare model name and the caller falls
		// through to its non-short-circuit path. The TEI short-circuit's
		// `modelName == teiModel` exact-match fast path already covers
		// the bare-default case where the embedded '@' happens to match
		// the TEI model identifier verbatim.
		if parts[1] == "Builtin" {
			return parts[0], "default", parts[1]
		}
		return compositeName, "", ""
	case 1:
		return parts[0], "", ""
	}
	n := len(parts)
	return strings.Join(parts[:n-2], "@"), parts[n-2], parts[n-1]
}

func newModelDriverForBaseURL(driver modelModule.ModelDriver, providerName, region, baseURL string) (modelModule.ModelDriver, error) {
	if driver == nil {
		return nil, fmt.Errorf("provider %s driver not found", providerName)
	}

	if strings.TrimSpace(baseURL) == "" {
		return driver, nil
	}

	baseURLByRegion := map[string]string{
		region: baseURL,
	}
	if region == "" {
		baseURLByRegion["default"] = baseURL
	}

	newDriver := driver.NewInstance(baseURLByRegion)
	if newDriver == nil {
		return nil, fmt.Errorf("provider %s does not support custom base_url", providerName)
	}

	return newDriver, nil
}

func NewModelProviderService() *ModelProviderService {
	return &ModelProviderService{
		modelProviderDAO:     dao.NewTenantModelProviderDAO(),
		modelInstanceDAO:     dao.NewTenantModelInstanceDAO(),
		modelDAO:             dao.NewTenantModelDAO(),
		modelGroupDAO:        dao.NewTenantModelGroupDAO(),
		modelGroupMappingDAO: dao.NewTenantModelGroupMappingDAO(),
		tenantDAO:            dao.NewTenantDAO(),
		userTenantDAO:        dao.NewUserTenantDAO(),
	}
}

type ModelProviderService struct {
	modelProviderDAO     *dao.TenantModelProviderDAO
	modelInstanceDAO     *dao.TenantModelInstanceDAO
	modelDAO             *dao.TenantModelDAO
	modelGroupDAO        *dao.TenantModelGroupDAO
	modelGroupMappingDAO *dao.TenantModelGroupMappingDAO
	tenantDAO            *dao.TenantDAO
	userTenantDAO        *dao.UserTenantDAO
}

// CheckConnectionModelInfo CheckConnectionRequest carries the credentials and optional instance selector
// for checking provider connectivity without creating a new model instance.
type CheckConnectionModelInfo struct {
	ModelName  string                 `json:"model_name"`
	ModelTypes []string               `json:"model_type"`
	MaxTokens  int                    `json:"max_tokens"`
	Extra      map[string]interface{} `json:"extra"`
}

type CheckConnectionRequest struct {
	APIKey     string                     `json:"api_key"`
	Region     string                     `json:"region"`
	BaseURL    string                     `json:"base_url"`
	InstanceID string                     `json:"instance_id"`
	ModelInfo  []CheckConnectionModelInfo `json:"model_info"`
}

func (m *ModelProviderService) AddModelProvider(ctx context.Context, providerName, userID string) (common.ErrorCode, error) {
	providerName = strings.TrimSpace(providerName)

	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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

	existing, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return common.CodeServerError, err
	}
	if existing != nil {
		return common.CodeSuccess, nil
	}

	providerID := utility.GenerateToken()

	tenantModelProvider := &entity.TenantModelProvider{
		ID:           providerID,
		ProviderName: providerName,
		TenantID:     tenantID,
	}
	err = m.modelProviderDAO.Create(ctx, dao.DB, tenantModelProvider)
	if err != nil {
		return common.CodeServerError, fmt.Errorf("fail to create model provider: %s", err.Error())
	}
	return common.CodeSuccess, nil
}

func (m *ModelProviderService) ListProvidersOfTenant(ctx context.Context, userID string) ([]map[string]interface{}, common.ErrorCode, error) {

	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, common.CodeServerError, err
	}

	if len(tenants) == 0 {
		return nil, common.CodeNotFound, errors.New("user has no tenants")
	}

	tenantID := tenants[0].TenantID

	providerNames, err := m.modelProviderDAO.ListByID(ctx, dao.DB, tenantID)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	var result []map[string]interface{}
	for _, providerName := range providerNames {
		// Mirror Python's list_providers tenant branch: silently skip system-excluded
		// factory names (e.g. "Builtin", "Youdao", "FastEmbed", "BAAI",
		// "siliconflow_intl") and any stale entries whose factory is no longer in
		// the system pool. See api/apps/services/provider_api_service.py:108.
		if isExcludedTenantProvider(providerName) {
			continue
		}
		provider, err := dao.GetModelProviderManager().GetProviderByName(providerName)
		if err != nil {
			// Treat "provider not found in system pool" as a stale tenant entry
			// rather than a 500. Mirrors Python's factory_info_mapping.get(name)
			// truthy gate in api/apps/services/provider_api_service.py:108.
			if strings.Contains(err.Error(), "not found") {
				continue
			}
			return nil, common.CodeServerError, err
		}
		// provider["name"] is the catalog name from GetProviderByName (e.g.,
		// "SILICONFLOW"), which matches Python's factory_info["name"]. Do NOT
		// override it with providerName (the DB value), which may differ in
		// case from historical ToLower writes.

		// 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(ctx, 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(ctx context.Context, tenantID, providerName string) bool {
	providerObj, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return false
	}
	instances, err := m.modelInstanceDAO.GetAllInstancesByProviderID(ctx, dao.DB, 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.
func isExcludedTenantProvider(name string) bool {
	switch name {
	case "Youdao", "FastEmbed", "BAAI", "Builtin", "siliconflow_intl":
		return true
	}
	return false
}

func (m *ModelProviderService) DeleteModelProvider(ctx context.Context, userID, providerName string) (common.ErrorCode, error) {
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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

	// Find the provider first.
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, 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(ctx, dao.DB, 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(ctx, dao.DB, instanceIDs); err != nil {
			return common.CodeServerError, err
		}
		if _, err = m.modelInstanceDAO.DeleteByProviderID(ctx, dao.DB, provider.ID); err != nil {
			return common.CodeServerError, err
		}
	}

	_, err = m.modelProviderDAO.DeleteByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return common.CodeServerError, err
	}

	return common.CodeSuccess, nil
}

func (m *ModelProviderService) ListSupportedModels(ctx context.Context, providerName, instanceName, userID string) ([]map[string]interface{}, error) {
	providerName = strings.TrimSpace(providerName)

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, errors.New("fail to get tenant")
	}

	if len(tenants) == 0 {
		return nil, errors.New("user has no tenants")
	}

	tenantID := tenants[0].TenantID

	// Check if provider exists
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return nil, err
	}

	instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
	if err != nil {
		return nil, err
	}

	providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
	if providerInfo == nil {
		return nil, fmt.Errorf("provider %s not found", providerName)
	}

	var extra map[string]string
	err = json.Unmarshal([]byte(instance.Extra), &extra)
	if err != nil {
		return nil, err
	}

	apiConfig := &modelModule.APIConfig{
		ApiKey: nil,
		Region: nil,
	}

	region := extra["region"]
	apiConfig.Region = &region
	apiConfig.ApiKey = &instance.APIKey

	driver := providerInfo.ModelDriver

	// For local deployed models
	if baseURL, ok := extra["base_url"]; ok && baseURL != "" {
		driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
		if err != nil {
			return nil, err
		}
	}

	modelList, err := driver.ListModels(ctx, apiConfig)
	if err != nil {
		return nil, err
	}
	if strings.EqualFold(provider.ProviderName, "NVIDIA") {
		if err = m.reconcileNvidiaInstanceModels(ctx, dao.DB, provider, instance, modelList); err != nil {
			return nil, err
		}
	}

	var result []map[string]interface{}
	for _, model := range modelList {
		result = append(result, map[string]interface{}{
			"name":          model.Name,
			"max_dimension": model.MaxDimension,
			"dimensions":    model.Dimensions,
			"max_tokens":    model.MaxTokens,
			"model_types":   model.ModelTypes,
			"thinking":      model.Thinking,
		})
	}
	return result, nil
}

func (m *ModelProviderService) reconcileNvidiaInstanceModels(
	ctx context.Context,
	db *gorm.DB,
	provider *entity.TenantModelProvider,
	instance *entity.TenantModelInstance,
	remoteModels []modelModule.ListModelResponse,
) error {
	if provider == nil || instance == nil || provider.ID == "" || instance.ID == "" || instance.ProviderID != provider.ID {
		return errors.New("invalid NVIDIA provider instance scope")
	}

	normalized := make([]modelModule.ListModelResponse, 0, len(remoteModels))
	seen := make(map[string]struct{}, len(remoteModels))
	for _, remote := range remoteModels {
		remote.Name = strings.TrimSpace(remote.Name)
		if remote.Name == "" {
			continue
		}
		if _, ok := seen[remote.Name]; ok {
			continue
		}
		seen[remote.Name] = struct{}{}
		if len(remote.ModelTypes) == 0 {
			remote.ModelTypes = modelModule.InferModelTypes(remote.Name)
		}
		normalized = append(normalized, remote)
	}
	if len(normalized) == 0 {
		return errors.New("NVIDIA model discovery returned no usable models")
	}

	return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		existingModels, err := m.modelDAO.GetModelsByInstanceID(ctx, tx, instance.ID)
		if err != nil {
			return err
		}
		existingByName := make(map[string]*entity.TenantModel, len(existingModels))
		for _, existing := range existingModels {
			existingByName[existing.ModelName] = existing
		}

		for _, remote := range normalized {
			maxTokens := 8192
			if remote.MaxTokens != nil && *remote.MaxTokens > 0 {
				maxTokens = *remote.MaxTokens
			}
			modelType := int(entity.ModelTypeFromStrings(remote.ModelTypes))

			if existing, ok := existingByName[remote.Name]; ok {
				extra := make(map[string]interface{})
				if existing.Extra != "" {
					if err := json.Unmarshal([]byte(existing.Extra), &extra); err != nil {
						return fmt.Errorf("decode metadata for NVIDIA model %q: %w", remote.Name, err)
					}
				}
				setDiscoveredModelMetadata(extra, remote, maxTokens)
				extraBytes, err := json.Marshal(extra)
				if err != nil {
					return fmt.Errorf("encode metadata for NVIDIA model %q: %w", remote.Name, err)
				}
				if err = m.modelDAO.UpdateByID(ctx, tx, existing.ID, map[string]interface{}{
					"model_type": modelType,
					"extra":      string(extraBytes),
				}); err != nil {
					return err
				}
				delete(existingByName, remote.Name)
				continue
			}

			extra := map[string]interface{}{"verify": entity.ModelVerifyUnknown}
			setDiscoveredModelMetadata(extra, remote, maxTokens)
			extraBytes, err := json.Marshal(extra)
			if err != nil {
				return fmt.Errorf("encode metadata for NVIDIA model %q: %w", remote.Name, err)
			}
			if err = m.modelDAO.Create(ctx, tx, &entity.TenantModel{
				ID:         utility.GenerateToken(),
				ModelName:  remote.Name,
				ModelType:  modelType,
				ProviderID: provider.ID,
				InstanceID: instance.ID,
				Status:     "active",
				Extra:      string(extraBytes),
			}); err != nil {
				return err
			}
		}

		staleIDs := make([]string, 0, len(existingByName))
		for _, stale := range existingByName {
			staleIDs = append(staleIDs, stale.ID)
		}
		if len(staleIDs) > 0 {
			if _, err := m.modelDAO.DeleteByIDs(ctx, tx, staleIDs); err != nil {
				return err
			}
		}
		return nil
	})
}

func setDiscoveredModelMetadata(extra map[string]interface{}, model modelModule.ListModelResponse, maxTokens int) {
	extra["max_tokens"] = maxTokens
	if model.MaxDimension != nil {
		extra["max_dimension"] = *model.MaxDimension
	}
	if len(model.Dimensions) > 0 {
		extra["dimensions"] = model.Dimensions
	}
	if model.Thinking != nil {
		extra["thinking"] = model.Thinking.DefaultValue
		extra["clear_thinking"] = model.Thinking.ClearThinking
	}
}

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) getProviderByIDOrName(ctx context.Context, tenantID, providerIDOrName string) (*entity.TenantModelProvider, error) {
	provider, err := m.modelProviderDAO.GetByID(ctx, dao.DB, providerIDOrName)
	if err == nil && provider.TenantID == tenantID {
		return provider, nil
	}
	return m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, strings.TrimSpace(providerIDOrName))
}

func (m *ModelProviderService) CreateProviderInstance(ctx context.Context, providerIDOrName, instanceName, apiKey, baseURL, region, userID string, modelInfo []CreateInstanceModelInfo) (common.ErrorCode, error) {
	providerIDOrName = strings.TrimSpace(providerIDOrName)

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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.getProviderByIDOrName(ctx, tenantID, providerIDOrName)
	if err != nil {
		return common.CodeNotFound, fmt.Errorf("provider '%s' does not exist", providerIDOrName)
	}
	providerName := provider.ProviderName

	// 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(ctx, providerName, apiKey, region, baseURL, modelInfo)

	instanceID := utility.GenerateToken()

	extra := make(map[string]string)
	extra["region"] = region
	extra["base_url"] = baseURL
	extraByte, err := json.Marshal(extra)
	if err != nil {
		return common.CodeServerError, errors.New("fail to marshal extra")
	}
	extraStr := string(extraByte)

	tenantModelInstance := &entity.TenantModelInstance{
		ID:           instanceID,
		InstanceName: instanceName,
		ProviderID:   provider.ID,
		APIKey:       apiKey,
		Status:       "active",
		Extra:        extraStr,
	}
	err = m.modelInstanceDAO.Create(ctx, dao.DB, 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(ctx, tenantID, providerName, instanceName, model); err != nil {
				return common.CodeServerError, err
			}
		}
	} else {
		// model_info not provided — add all factory default models.
		// Mirrors Python's create_provider_instance
		// (api/apps/services/provider_api_service.py:506-531).
		targetFactoryName := providerName
		if region == "intl" && strings.EqualFold(providerName, "siliconflow") {
			targetFactoryName = "siliconflow_intl"
		}
		factoryProvider := dao.GetModelProviderManager().FindProvider(targetFactoryName)
		if factoryProvider != nil {
			for _, llm := range factoryProvider.Models {
				verifyStatus := modelVerifyResult[llm.Name]
				if verifyStatus == "" {
					verifyStatus = entity.ModelVerifyUnknown
				}
				extraMap := map[string]interface{}{
					"verify": verifyStatus,
				}
				if llm.Tools != nil {
					extraMap["is_tools"] = llm.Tools.Support
				}
				if llm.Thinking != nil {
					extraMap["thinking"] = llm.Thinking.DefaultValue
				}
				if err = m.addModelToInstance(ctx, tenantID, providerName, instanceName, CreateInstanceModelInfo{
					ModelName:  llm.Name,
					ModelTypes: llm.ModelTypes,
					MaxTokens: func() int {
						if llm.MaxTokens != nil {
							return *llm.MaxTokens
						}
						return 8192
					}(),
					Extra: extraMap,
				}); 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.
func (m *ModelProviderService) CreateNameOnlyProviderInstance(ctx context.Context, providerIDOrName, instanceName, userID string) (common.ErrorCode, error) {
	providerIDOrName = strings.TrimSpace(providerIDOrName)

	if instanceName == "default" {
		return common.CodeBadRequest, errors.New("instance name cannot be 'default'")
	}

	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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.getProviderByIDOrName(ctx, tenantID, providerIDOrName)
	if err != nil {
		return common.CodeNotFound, fmt.Errorf("provider '%s' does not exist", providerIDOrName)
	}

	instanceID := utility.GenerateToken()

	tenantModelInstance := &entity.TenantModelInstance{
		ID:           instanceID,
		InstanceName: instanceName,
		ProviderID:   provider.ID,
		APIKey:       "",
		Status:       "active",
		Extra:        "{}",
	}
	err = m.modelInstanceDAO.Create(ctx, dao.DB, 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).
func (m *ModelProviderService) verifyProviderAPIKey(ctx context.Context, 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:  &region,
		BaseURL: &baseURL,
	}

	verifyErr := driver.CheckConnection(ctx, 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.
func (m *ModelProviderService) addModelToInstance(ctx context.Context, tenantID, providerName, instanceName string, model CreateInstanceModelInfo) error {
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return fmt.Errorf("no provider found for provider '%s'", providerName)
	}

	instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, 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(ctx, dao.DB, 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(ctx, dao.DB, tenantModel); err != nil {
		return fmt.Errorf("fail to create model '%s': %s", model.ModelName, err.Error())
	}

	return nil
}

func (m *ModelProviderService) ListProviderInstances(ctx context.Context, providerIDOrName, userID string) ([]map[string]interface{}, common.ErrorCode, error) {
	providerIDOrName = strings.TrimSpace(providerIDOrName)

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, common.CodeServerError, err
	}

	if len(tenants) == 0 {
		return nil, common.CodeNotFound, errors.New("user has no tenants")
	}

	tenantID := tenants[0].TenantID

	// Check if provider exists — try by ID first, then by name.
	provider, err := m.getProviderByIDOrName(ctx, tenantID, providerIDOrName)
	if err != nil {
		return nil, common.CodeDataError, fmt.Errorf("no provider found for provider '%s'", providerIDOrName)
	}

	// Check if provider exists
	instances, err := m.modelInstanceDAO.GetAllInstancesByProviderID(ctx, dao.DB, provider.ID)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	// Always emit a non-nil slice so the JSON encoder serializes [] rather
	// than null when the tenant has no instances on this provider. The
	// front-end calls .map() / .forEach() on the response array (see
	// web/src/pages/user-setting/setting-model/...) and would otherwise
	// crash on a freshly created tenant.
	result := make([]map[string]interface{}, 0, len(instances))
	for _, instance := range instances {
		// Parse extra to extract region
		var extraFields map[string]string
		if instance.Extra != "" {
			if err = json.Unmarshal([]byte(instance.Extra), &extraFields); err != nil {
				return nil, common.CodeServerError, err
			}
		}
		if extraFields == nil {
			extraFields = make(map[string]string)
		}

		// Emit snake_case keys to match the Python
		// list_provider_instances response shape
		// (provider_api_service.py:563).
		result = append(result, map[string]interface{}{
			"id":            instance.ID,
			"instance_name": instance.InstanceName,
			"provider_id":   instance.ProviderID,
			"region":        extraFields["region"],
			"status":        instance.Status,
		})
	}

	return result, common.CodeSuccess, nil
}

func (m *ModelProviderService) ShowProviderInstance(ctx context.Context, providerName, instanceIDOrName, userID string) (map[string]interface{}, common.ErrorCode, error) {
	providerName = strings.TrimSpace(providerName)
	providerName = strings.ToLower(providerName)

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, common.CodeServerError, err
	}

	if len(tenants) == 0 {
		return nil, common.CodeNotFound, errors.New("user has no tenants")
	}

	tenantID := tenants[0].TenantID

	// Check if provider exists. Mirrors Python's:
	//   provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_id(tenant_id, provider_id_or_name)
	//   if not provider_obj:
	//       provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, provider_id_or_name)
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return nil, common.CodeDataError, fmt.Errorf("no provider found for provider '%s'", providerName)
	}

	// Find the instance — try by ID first, then by name.
	// Mirrors Python's:
	//   _, instance_obj = TenantModelInstanceService.get_by_id(instance_id_or_name)
	//   if instance_obj and instance_obj.provider_id != provider_id: instance_obj = None
	//   if not instance_obj:
	//       instance_obj = TenantModelInstanceService.get_by_provider_id_and_instance_name(provider_id, instance_id_or_name)
	var instance *entity.TenantModelInstance
	instance, err = m.modelInstanceDAO.GetByID(ctx, dao.DB, instanceIDOrName)
	if err != nil || instance.ProviderID != provider.ID {
		instance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceIDOrName)
	}
	if err != nil {
		return nil, common.CodeDataError, fmt.Errorf("no instance found for provider '%s' and instance '%s'", providerName, instanceIDOrName)
	}

	// Parse extra fields. Mirrors Python's:
	//   extra_fields = json.loads(instance_obj.extra) if instance_obj.extra else {}
	var extraFields map[string]string
	if instance.Extra != "" {
		if err := json.Unmarshal([]byte(instance.Extra), &extraFields); err != nil {
			return nil, common.CodeServerError, err
		}
	}
	if extraFields == nil {
		extraFields = make(map[string]string)
	}

	result := map[string]interface{}{
		"id":            instance.ID,
		"instance_name": instance.InstanceName,
		"provider_id":   provider.ID,
		"region":        extraFields["region"],
		"base_url":      extraFields["base_url"],
		"api_key":       instance.APIKey,
		"status":        instance.Status,
	}

	return result, common.CodeSuccess, nil
}

func (m *ModelProviderService) ShowInstanceBalance(ctx context.Context, providerName, instanceName, userID string) (map[string]interface{}, common.ErrorCode, error) {

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, common.CodeServerError, err
	}

	if len(tenants) == 0 {
		return nil, common.CodeNotFound, errors.New("user has no tenants")
	}

	tenantID := tenants[0].TenantID

	// Check if provider exists
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
	if providerInfo == nil {
		return nil, common.CodeServerError, fmt.Errorf("provider %s not found", providerName)
	}

	var extra map[string]string
	err = json.Unmarshal([]byte(instance.Extra), &extra)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	apiConfig := &modelModule.APIConfig{
		ApiKey: nil,
		Region: nil,
	}

	region := extra["region"]
	baseURL := extra["base_url"]
	apiConfig.Region = &region
	apiConfig.ApiKey = &instance.APIKey
	apiConfig.BaseURL = &baseURL

	var result map[string]interface{}
	result, err = providerInfo.ModelDriver.Balance(ctx, apiConfig)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	return result, common.CodeSuccess, nil
}

func (m *ModelProviderService) CheckConnection(ctx context.Context, providerName, apiKey, region, baseURL, instanceID, userID string, modelInfo []CheckConnectionModelInfo) (common.ErrorCode, error) {
	providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
	if providerInfo == nil {
		return common.CodeServerError, fmt.Errorf("provider %s not found", providerName)
	}

	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") {
		if baseURL == "" {
			return common.CodeDataError, fmt.Errorf("base_url is required for local provider %s", providerName)
		}

		var err error
		driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
		if err != nil {
			return common.CodeServerError, err
		}
	}

	apiConfig := &modelModule.APIConfig{
		ApiKey:  &apiKey,
		Region:  &region,
		BaseURL: &baseURL,
	}

	// Mirror Python verify_api_key: verify each model by making a real
	// lightweight API request.  Returns per-model verify results.
	modelVerifyResult, verifyErr := verifyProviderModel(ctx, driver, providerInfo.Models, apiConfig, modelInfo)

	// When instanceID is provided (frontend passes it), persist the verify
	// results to the database — mirrors Python's per-model update_model calls
	// inside the /connection/verify REST endpoint.
	if instanceID != "" && len(modelVerifyResult) > 0 {
		if dbErr := m.updateModelVerifyResults(ctx, userID, providerName, instanceID, modelVerifyResult); dbErr != nil {
			common.Logger.Error("failed to persist model verify results", zap.Error(dbErr))
		}
	}

	if verifyErr != nil {
		return common.CodeServerError, verifyErr
	}

	return common.CodeSuccess, nil
}

// updateModelVerifyResults persists the per-model verification status to the
// tenant_model table.  It mirrors the Python update_model() called from the
// /api/v1/providers/<name>/connection/verify endpoint when instance_id is
// present in the request body.
func (m *ModelProviderService) updateModelVerifyResults(ctx context.Context, userID, providerName, instanceID string, modelVerifyResult map[string]string) error {
	// Resolve tenant from user.
	userTenants, err := m.userTenantDAO.GetByUserID(ctx, dao.DB, userID)
	if err != nil || len(userTenants) == 0 {
		return fmt.Errorf("no tenant found for user %s", userID)
	}
	tenantID := userTenants[0].TenantID

	// Resolve provider DB record from tenant + provider name.
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return fmt.Errorf("provider %s not found for tenant %s: %w", providerName, tenantID, err)
	}

	for modelName, verifyStatus := range modelVerifyResult {
		var modelObj *entity.TenantModel
		modelObj, err = m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(ctx, dao.DB, provider.ID, instanceID, modelName)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				// No existing row — nothing to update (default is active).
				continue
			}
			return fmt.Errorf("failed to look up %s for verify update: %w", modelName, err)
		}

		extra := make(map[string]interface{})
		if modelObj.Extra != "" {
			_ = json.Unmarshal([]byte(modelObj.Extra), &extra)
		}
		extra["verify"] = verifyStatus
		var extraJSON []byte
		extraJSON, err = json.Marshal(extra)
		if err != nil {
			return fmt.Errorf("failed to marshal extra for %s: %w", modelName, err)
		}

		if err = m.modelDAO.UpdateByID(ctx, dao.DB, modelObj.ID, map[string]interface{}{
			"extra": string(extraJSON),
		}); err != nil {
			return fmt.Errorf("failed to update verify status for %s: %w", modelName, err)
		}
	}

	return nil
}

// verifyProviderModel mirrors Python verify_api_key's model-level verification.
// It tries each model and returns a map of modelName → verify status
// ("success"/"fail") so the caller can persist the results to the database.
// A nil error means at least one model passed verification.
//
// When modelInfo is provided (non-empty), it is used directly as the source of
// models to verify — each entry carries model_name and model_type ([]string).
// When modelInfo is empty, the function falls back to the provider's static
// model catalog (providerModels).
func verifyProviderModel(ctx context.Context, driver modelModule.ModelDriver, providerModels []*modelModule.Model, apiConfig *modelModule.APIConfig, modelInfo []CheckConnectionModelInfo) (map[string]string, error) {
	modelVerifyResult := make(map[string]string)

	// Determine which models to verify: prefer the caller-supplied modelInfo
	// list; fall back to the full provider model catalog.  Mirrors Python’s
	// verify_api_key ll. 656–671.
	var modelsToVerify []*modelModule.Model
	if len(modelInfo) > 0 {
		for _, mi := range modelInfo {
			modelName := strings.TrimSpace(mi.ModelName)
			if modelName == "" {
				continue
			}
			modelTypes := mi.ModelTypes
			if len(modelTypes) == 0 {
				// When the caller didn't supply model types, infer them from the
				// model name using the same hint heuristics as Python's
				// OpenAIAPICompatible._infer_model_types.
				modelTypes = modelModule.InferModelTypes(modelName)
			}
			modelsToVerify = append(modelsToVerify, &modelModule.Model{
				Name:       modelName,
				ModelTypes: modelTypes,
			})
		}
	} else {
		modelsToVerify = providerModels
	}

	if len(modelsToVerify) == 0 {
		// Third fallback: try to fetch remote models via the provider's API,
		// mirroring Python's get_model_list() fallback in verify_api_key.
		remoteModels, listErr := driver.ListModels(ctx, apiConfig)
		if listErr == nil {
			for _, rm := range remoteModels {
				modelName := strings.TrimSpace(rm.Name)
				if modelName == "" {
					continue
				}
				modelTypes := rm.ModelTypes
				if len(modelTypes) == 0 {
					modelTypes = modelModule.InferModelTypes(modelName)
				}
				modelsToVerify = append(modelsToVerify, &modelModule.Model{
					Name:       modelName,
					ModelTypes: modelTypes,
				})
			}
		} else {
			zap.L().Warn("failed to list remote models from provider",
				zap.Error(listErr),
			)
		}

		if len(modelsToVerify) == 0 {
			return modelVerifyResult, fmt.Errorf("no models found for provider")
		}
	}

	var errs []error
	errSet := make(map[string]bool)
	passedTypes := make(map[string]bool)

	for _, model := range modelsToVerify {
		modelName := model.Name
		anyPassed := false

		for _, modelType := range model.ModelTypes {
			mtLower := strings.ToLower(modelType)

			// If a model type we've already verified successfully, skip.
			if passedTypes[mtLower] {
				continue
			}

			var err error

			switch mtLower {
			case "chat", "vision":
				msg := []modelModule.Message{{Role: "user", Content: "Hi"}}
				_, err = driver.ChatWithMessages(ctx, modelName, msg, apiConfig, nil, nil)
			case "embedding":
				_, err = driver.Embed(ctx, &modelName, []string{"test"}, apiConfig, nil, nil)
			case "rerank":
				_, err = driver.Rerank(ctx, &modelName, "test", []string{"test"}, apiConfig, &modelModule.RerankConfig{}, nil)
			case "tts":
				content := "hello"
				_, err = driver.AudioSpeech(ctx, &modelName, &content, apiConfig, nil, nil)
			case "asr":
				err = verifyASRModel(ctx, driver, modelName, apiConfig)
			case "ocr":
				err = verifyOCRModel(ctx, driver, modelName, apiConfig)
			case "doc_parse":
				err = driver.CheckConnection(ctx, apiConfig)
			default:
				continue
			}

			if err == nil {
				passedTypes[mtLower] = true
				anyPassed = true
				break
			}

			apiErr := extractAPIErrorMessage(err)
			if !errSet[apiErr.Error()] {
				errSet[apiErr.Error()] = true
				errs = append(errs, apiErr)
			}
		}

		if anyPassed {
			modelVerifyResult[modelName] = entity.ModelVerifySuccess
		} else {
			modelVerifyResult[modelName] = entity.ModelVerifyFail
		}
	}

	if len(passedTypes) == 0 {
		return modelVerifyResult, fmt.Errorf("all model verification attempts failed: %w", errors.Join(errs...))
	}

	return modelVerifyResult, nil
}

// extractAPIErrorMessage tries to parse the `message` field from a JSON error
// body embedded in a Go error string.  If the body is valid JSON with a
// non-empty "message" key, the returned error contains only that message;
// otherwise the original error is returned unchanged.
func extractAPIErrorMessage(err error) error {
	msg := err.Error()
	// Look for the last '{'...'}' substring — that is typically the JSON body
	// appended by API drivers like "API request failed with status 400: {...}".
	start := strings.LastIndexByte(msg, '{')
	if start < 0 {
		return err
	}
	end := strings.LastIndexByte(msg, '}')
	if end <= start {
		return err
	}
	jsonStr := msg[start : end+1]

	var body struct {
		Message string `json:"message"`
	}
	if json.Unmarshal([]byte(jsonStr), &body) != nil || body.Message == "" {
		return err
	}
	return fmt.Errorf("%s", body.Message)
}

// generateTestWAV creates a minimal silent WAV (16-bit mono PCM, 0.5 second,
// 16000 Hz sample rate) as a byte slice. Mirrors Python
// sequence2txt_model.py's _generate_test_wav: pure stdlib, no dependencies.
func generateTestWAV() []byte {
	const (
		sampleRate      = 16000
		durationSeconds = 0.5
		numChannels     = 1
		bitsPerSample   = 16
	)
	numSamples := int(sampleRate * durationSeconds)
	dataSize := numSamples * numChannels * (bitsPerSample / 8)

	var buf []byte

	// RIFF header
	buf = append(buf, []byte("RIFF")...)
	buf = binary.LittleEndian.AppendUint32(buf, uint32(36+dataSize))
	buf = append(buf, []byte("WAVE")...)

	// fmt sub-chunk
	buf = append(buf, []byte("fmt ")...)
	buf = binary.LittleEndian.AppendUint32(buf, 16)                  // sub-chunk size
	buf = binary.LittleEndian.AppendUint16(buf, 1)                   // PCM
	buf = binary.LittleEndian.AppendUint16(buf, uint16(numChannels)) // mono
	buf = binary.LittleEndian.AppendUint32(buf, uint32(sampleRate))
	buf = binary.LittleEndian.AppendUint32(buf, uint32(sampleRate*numChannels*bitsPerSample/8)) // byte rate
	buf = binary.LittleEndian.AppendUint16(buf, uint16(numChannels*bitsPerSample/8))            // block align
	buf = binary.LittleEndian.AppendUint16(buf, uint16(bitsPerSample))

	// data sub-chunk
	buf = append(buf, []byte("data")...)
	buf = binary.LittleEndian.AppendUint32(buf, uint32(dataSize))
	buf = append(buf, make([]byte, dataSize)...) // silence

	return buf
}

// verifyASRModel mirrors Python sequence2txt_model.py's check_available:
// generates a minimal test WAV, writes it to a temp file, calls
// TranscribeAudio, and checks the result for errors.
func verifyASRModel(ctx context.Context, driver modelModule.ModelDriver, modelName string, apiConfig *modelModule.APIConfig) error {
	wavData := generateTestWAV()

	tmpFile, err := os.CreateTemp("", "ragflow-asr-verify-*.wav")
	if err != nil {
		return fmt.Errorf("failed to create temp WAV for ASR verification: %w", err)
	}
	tmpPath := tmpFile.Name()
	defer os.Remove(tmpPath)

	if _, err = tmpFile.Write(wavData); err != nil {
		tmpFile.Close()
		return fmt.Errorf("failed to write test WAV: %w", err)
	}
	tmpFile.Close()

	resp, err := driver.TranscribeAudio(ctx, &modelName, &tmpPath, apiConfig, nil, nil)
	if err != nil {
		return err
	}
	if resp == nil || resp.Text == "" {
		return fmt.Errorf("ASR model %s returned no transcription", modelName)
	}
	return nil
}

// verifyOCRModel mirrors Python OCRModel.check_available by sending a
// minimal PNG (1×1 pixel white) through the OCR pipeline.  Most OCR
// providers return an error or empty text for such a trivial image;
// we accept any non-error response as a successful connectivity check.
func verifyOCRModel(ctx context.Context, driver modelModule.ModelDriver, modelName string, apiConfig *modelModule.APIConfig) error {
	// Send a minimal 1×1 white PNG through the OCR pipeline to verify
	// connectivity. Most OCRModel drivers only check server reachability
	// rather than performing full document parsing.
	_, err := driver.OCRFile(ctx, &modelName, minimalPNG(), nil, apiConfig, nil, nil)
	if err != nil {
		return err
	}
	return nil
}

// minimalPNG returns a 1×1 white PNG as a byte slice for OCR verification.
func minimalPNG() []byte {
	return []byte{
		0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
		0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
		0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
		0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
		0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41,
		0x54, 0x08, 0xD7, 0x63, 0x60, 0x60, 0xF8, 0x0F,
		0x00, 0x01, 0x01, 0x00, 0x05, 0x18, 0xD8, 0x32,
		0x48, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E,
		0x44, 0xAE, 0x42, 0x60, 0x82,
	}
}

func (m *ModelProviderService) CheckInstanceConnection(ctx context.Context, providerName, instanceName, userID string) (common.ErrorCode, error) {

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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

	// Check if provider exists
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return common.CodeServerError, err
	}

	instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
	if err != nil {
		return common.CodeServerError, err
	}

	providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
	if providerInfo == nil {
		return common.CodeServerError, fmt.Errorf("provider %s not found", providerName)
	}

	var extra map[string]string
	err = json.Unmarshal([]byte(instance.Extra), &extra)
	if err != nil {
		return common.CodeServerError, err
	}

	apiConfig := &modelModule.APIConfig{
		ApiKey: nil,
		Region: nil,
	}

	region := extra["region"]
	apiConfig.Region = &region
	apiConfig.ApiKey = &instance.APIKey

	driver := providerInfo.ModelDriver
	if baseURL, ok := extra["base_url"]; ok && baseURL != "" {
		driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
		if err != nil {
			return common.CodeServerError, err
		}
	}

	err = driver.CheckConnection(ctx, apiConfig)
	if err != nil {
		return common.CodeServerError, err
	}
	return common.CodeSuccess, nil
}

func (m *ModelProviderService) ListTasks(ctx context.Context, providerName, instanceName, userID string) ([]modelModule.ListTaskStatus, common.ErrorCode, error) {

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, common.CodeServerError, err
	}

	if len(tenants) == 0 {
		return nil, common.CodeNotFound, errors.New("user has no tenants")
	}

	tenantID := tenants[0].TenantID

	// Check if provider exists
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
	if providerInfo == nil {
		return nil, common.CodeServerError, fmt.Errorf("provider %s not found", providerName)
	}

	var extra map[string]string
	err = json.Unmarshal([]byte(instance.Extra), &extra)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	apiConfig := &modelModule.APIConfig{
		ApiKey: nil,
		Region: nil,
	}

	region := extra["region"]
	apiConfig.Region = &region
	apiConfig.ApiKey = &instance.APIKey

	driver := providerInfo.ModelDriver
	if baseURL, ok := extra["base_url"]; ok && baseURL != "" {
		driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
		if err != nil {
			return nil, common.CodeServerError, err
		}
	}

	var listTaskResponse []modelModule.ListTaskStatus
	listTaskResponse, err = driver.ListTasks(ctx, apiConfig)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	return listTaskResponse, common.CodeSuccess, nil
}

func (m *ModelProviderService) ShowTask(ctx context.Context, providerName, instanceName, taskID, userID string) (*modelModule.TaskResponse, common.ErrorCode, error) {

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, common.CodeServerError, err
	}

	if len(tenants) == 0 {
		return nil, common.CodeNotFound, errors.New("user has no tenants")
	}

	tenantID := tenants[0].TenantID

	// Check if provider exists
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
	if providerInfo == nil {
		return nil, common.CodeServerError, fmt.Errorf("provider %s not found", providerName)
	}

	var extra map[string]string
	err = json.Unmarshal([]byte(instance.Extra), &extra)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	apiConfig := &modelModule.APIConfig{
		ApiKey: nil,
		Region: nil,
	}

	region := extra["region"]
	apiConfig.Region = &region
	apiConfig.ApiKey = &instance.APIKey

	driver := providerInfo.ModelDriver
	if baseURL, ok := extra["base_url"]; ok && baseURL != "" {
		driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
		if err != nil {
			return nil, common.CodeServerError, err
		}
	}

	var taskResponse *modelModule.TaskResponse
	taskResponse, err = driver.ShowTask(ctx, taskID, apiConfig)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	return taskResponse, common.CodeSuccess, nil
}

// ListTenantAddedModels returns the list of models the tenant has "added"
// across all of their provider instances. It is the Go port of Python's
// models_api_service.list_tenant_added_models
// (api/apps/services/models_api_service.py:300) and is the response
// contract for GET /api/v1/models — the endpoint that
// web/src/hooks/use-llm-request.tsx → useFetchAllAddedModels consumes.
//
// Per the Python algorithm, for each (provider × instance) we cross-reference the factory catalog (internal/entity/models/model.go
// ProviderManager.Providers) with the per-tenant overrides in
// tenant_model:
//
//	active_model_types   = tenant_model rows with status='active'
//	inactive_model_types = tenant_model rows with status='inactive'
//	factory_model_types  = provider.Models[i].ModelTypes
//	model_types = (factory ∪ active) \ inactive
//
// The Go port never WRITES to tenant_model, so in practice every model
// from the factory catalog is treated as added unless explicitly
// disabled (which today can only happen via SQL — the Go port has no
// enable/disable endpoint path that mutates tenant_model). This is
// intentional: the previous Go contract mistakenly routed /api/v1/models
// to ListTenantDefaultModels (which only enumerates the 6-7 default
// tenant fields and returned `[]` for any tenant without defaults),
// breaking the front-end's "View Models" list entirely.
func (m *ModelProviderService) ListTenantAddedModels(ctx context.Context, userID, ownerTenantID, modelTypeFilter string) ([]map[string]interface{}, common.ErrorCode, error) {
	tenant, code, err := m.resolveModelListTenant(ctx, userID, ownerTenantID)
	if err != nil {
		return nil, code, err
	}
	if tenant == nil {
		return []map[string]interface{}{}, common.CodeSuccess, nil
	}
	tenantID := tenant.ID
	tenantName := ""
	if tenant.Name != nil {
		tenantName = *tenant.Name
	}

	if modelTypeFilter != "" {
		modelTypeFilter = strings.ToLower(strings.TrimSpace(modelTypeFilter))
	}

	// Mirror Python's ensure_*_from_env calls.
	_ = m.ensureMineruFromEnv(ctx, tenantID)
	_ = m.ensurePaddleOCREnabledFromEnv(ctx, tenantID)
	_ = m.ensureOpenDataLoaderFromEnv(ctx, tenantID)

	var modelTypeFilterBin entity.ModelType
	if modelTypeFilter != "" {
		modelTypeFilterBin = entity.ModelTypeFromString(modelTypeFilter)
	}

	providers, err := m.modelProviderDAO.GetByTenantID(ctx, dao.DB, tenantID)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	if len(providers) == 0 {
		return []map[string]interface{}{}, common.CodeSuccess, nil
	}

	providerIDs := make([]string, 0, len(providers))
	providerInfoByID := make(map[string]*entity.TenantModelProvider, len(providers))
	for _, p := range providers {
		providerIDs = append(providerIDs, p.ID)
		providerInfoByID[p.ID] = p
	}

	instances, err := m.modelInstanceDAO.GetByProviderIDs(ctx, dao.DB, providerIDs)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	if len(instances) == 0 {
		return []map[string]interface{}{}, common.CodeSuccess, nil
	}

	instanceIDs := make([]string, 0, len(instances))
	instanceInfoByID := make(map[string]*entity.TenantModelInstance, len(instances))
	for _, inst := range instances {
		instanceIDs = append(instanceIDs, inst.ID)
		instanceInfoByID[inst.ID] = inst
	}

	// Fetch tenant model records and filter by model_type if needed.
	modelRecords, err := m.modelDAO.GetModelsByProviderIDsAndInstanceIDs(ctx, dao.DB, providerIDs, instanceIDs)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	// Repair records that have model_type == 0. Before the ModelTypeFromString
	// fix that added "asr" support, models whose factory catalog used the
	// canonical name "asr" (instead of the legacy "speech2text") were stored
	// with model_type = 0. Re-derive the correct bitmask from the factory
	// catalog and persist it so the model shows up in typed queries.
	providerManager := dao.GetModelProviderManager()
	for _, rec := range modelRecords {
		if rec.ModelType != 0 {
			continue
		}
		provInfo := providerInfoByID[rec.ProviderID]
		if provInfo == nil {
			continue
		}
		factoryModel, lookupErr := providerManager.GetModelByName(provInfo.ProviderName, rec.ModelName)
		if lookupErr != nil || len(factoryModel.ModelTypes) == 0 {
			continue
		}
		combinedType := entity.ModelType(0)
		for _, t := range factoryModel.ModelTypes {
			combinedType |= entity.ModelTypeFromString(t)
		}
		if combinedType != 0 {
			rec.ModelType = int(combinedType)
			_ = m.modelDAO.UpdateByID(ctx, dao.DB, rec.ID, map[string]interface{}{"model_type": int(combinedType)})
		}
	}

	var targetRecords []*entity.TenantModel
	if modelTypeFilterBin != 0 {
		for _, rec := range modelRecords {
			if entity.ModelType(rec.ModelType)&modelTypeFilterBin != 0 {
				targetRecords = append(targetRecords, rec)
			}
		}
	} else {
		targetRecords = modelRecords
	}

	// Build model rank map from factory catalog (mirrors Python's model_rank_map).
	modelRankMap := make(map[string]int) // key: "providerName@modelName"
	factoryRankMapping := make(map[string]int)
	for i := range providerManager.Providers {
		factory := &providerManager.Providers[i]
		factoryRankMapping[factory.Name] = -factory.Rank
		for _, llm := range factory.Models {
			rank := 500
			if llm.Rank != nil {
				rank = *llm.Rank
			}
			modelRankMap[factory.Name+"@"+llm.Name] = rank
		}
	}

	added := make([]map[string]interface{}, 0, len(targetRecords))
	for _, modelRecord := range targetRecords {
		provInfo, provOK := providerInfoByID[modelRecord.ProviderID]
		instInfo, instOK := instanceInfoByID[modelRecord.InstanceID]
		if !provOK || !instOK {
			continue
		}
		rank := modelRankMap[provInfo.ProviderName+"@"+modelRecord.ModelName]
		if rank == 0 {
			rank = 500
		}
		added = append(added, map[string]interface{}{
			"model_id":      modelRecord.ID,
			"tenant_id":     provInfo.TenantID,
			"tenant_name":   tenantName,
			"model_type":    entity.ModelType(modelRecord.ModelType).HumanReadable(),
			"name":          modelRecord.ModelName,
			"provider_id":   modelRecord.ProviderID,
			"provider_name": provInfo.ProviderName,
			"instance_id":   modelRecord.InstanceID,
			"instance_name": instInfo.InstanceName,
			"rank":          rank,
		})
	}

	// Add TEI Builtin embedding model if configured (mirrors Python).
	composeProfiles := common.GetEnv(common.EnvComposeProfiles)
	teiModel := common.GetEnv(common.EnvTEIModel)
	if strings.Contains(composeProfiles, "tei-") && teiModel != "" {
		if modelTypeFilter == "" || modelTypeFilter == "embedding" {
			teiAlreadyAdded := false
			for _, m := range added {
				if pn, _ := m["provider_name"].(string); pn == "Builtin" {
					if n, _ := m["name"].(string); n == teiModel {
						teiAlreadyAdded = true
						break
					}
				}
			}
			if !teiAlreadyAdded {
				rank := modelRankMap["Builtin@"+teiModel]
				if rank == 0 {
					rank = 500
				}
				added = append(added, map[string]interface{}{
					"model_id":      "",
					"tenant_id":     tenant.ID,
					"tenant_name":   tenantName,
					"model_type":    []string{"embedding"},
					"name":          teiModel,
					"provider_id":   "",
					"provider_name": "Builtin",
					"instance_id":   "",
					"instance_name": "default",
					"rank":          rank,
				})
			}
		}
	}

	// Sort by factory rank desc, provider_name, instance_name, model rank desc, name.
	sort.Slice(added, func(i, j int) bool {
		pi, _ := added[i]["provider_name"].(string)
		pj, _ := added[j]["provider_name"].(string)
		fi := factoryRankMapping[pi]
		fj := factoryRankMapping[pj]
		if fi != fj {
			return fi < fj // factory rank: smaller (more negative) = higher priority
		}
		if pi != pj {
			return pi < pj
		}
		ini, _ := added[i]["instance_name"].(string)
		inj, _ := added[j]["instance_name"].(string)
		if ini != inj {
			return ini < inj
		}
		ri, _ := added[i]["rank"].(int)
		rj, _ := added[j]["rank"].(int)
		if ri != rj {
			return ri > rj // model rank desc
		}
		ni, _ := added[i]["name"].(string)
		nj, _ := added[j]["name"].(string)
		return ni < nj
	})

	return added, common.CodeSuccess, nil
}

func (m *ModelProviderService) resolveModelListTenant(ctx context.Context, userID, ownerTenantID string) (*entity.Tenant, common.ErrorCode, error) {
	if ownerTenantID == "" {
		tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
		if err != nil {
			return nil, common.CodeServerError, err
		}
		if len(tenants) == 0 {
			return nil, common.CodeSuccess, nil
		}
		ownerTenantID = tenants[0].TenantID
	} else {
		relations, err := m.userTenantDAO.GetByUserID(ctx, dao.DB, userID)
		if err != nil {
			return nil, common.CodeServerError, err
		}
		allowed := false
		for _, rel := range relations {
			if rel.TenantID == ownerTenantID {
				allowed = true
				break
			}
		}
		if !allowed {
			return nil, common.CodeAuthenticationError, fmt.Errorf("permission denied")
		}
	}

	tenant, err := m.tenantDAO.GetByID(ctx, dao.DB, ownerTenantID)
	if err != nil {
		if dao.IsNotFoundErr(err) {
			return nil, common.CodeNotFound, fmt.Errorf("tenant %s not found", ownerTenantID)
		}
		return nil, common.CodeServerError, err
	}
	return tenant, common.CodeSuccess, nil
}

// ensureMineruFromEnv mirrors Python's ensure_mineru_from_env.
// It ensures a MinerU OCR provider instance exists when env vars are configured.
func (m *ModelProviderService) ensureMineruFromEnv(ctx context.Context, tenantID string) error {
	config := collectEnvConfig(mineruEnvKeys, mineruDefaultConfig)
	return m.ensureOCRProviderFromEnv(ctx, tenantID, "MinerU", "mineru-from-env", config)
}

// ensurePaddleOCREnabledFromEnv mirrors Python's ensure_paddleocr_from_env.
func (m *ModelProviderService) ensurePaddleOCREnabledFromEnv(ctx context.Context, tenantID string) error {
	config := collectEnvConfig(paddleOCREnvKeys, paddleOCRDefaultConfig)
	return m.ensureOCRProviderFromEnv(ctx, tenantID, "PaddleOCR", "paddleocr-from-env", config)
}

// ensureOpenDataLoaderFromEnv mirrors Python's ensure_opendataloader_from_env.
func (m *ModelProviderService) ensureOpenDataLoaderFromEnv(ctx context.Context, tenantID string) error {
	config := collectEnvConfig(openDataLoaderEnvKeys, openDataLoaderDefaultConfig)
	return m.ensureOCRProviderFromEnv(ctx, tenantID, "OpenDataLoader", "opendataloader-from-env", config)
}

// env key / default config tables for the three OCR providers.
// Mirrors common/constants.py MINERU_ENV_KEYS, PADDLEOCR_ENV_KEYS, OPENDATALOADER_ENV_KEYS.
var (
	mineruEnvKeys = []string{
		common.EnvMineruApiServer,
		"MINERU_OUTPUT_DIR",
		common.EnvMineruBackend,
		"MINERU_SERVER_URL",
		"MINERU_DELETE_OUTPUT",
	}
	mineruDefaultConfig = map[string]interface{}{
		common.EnvMineruApiServer: "",
		"MINERU_OUTPUT_DIR":       "",
		common.EnvMineruBackend:   "pipeline",
		"MINERU_SERVER_URL":       "",
		"MINERU_DELETE_OUTPUT":    1,
	}
	paddleOCREnvKeys = []string{
		common.EnvPaddleOCRBaseUrl,
		common.EnvPaddleOCRApiURL,
		common.EnvPaddleOCRAccessToken,
		common.EnvPaddleOCRAlgorithm,
	}
	paddleOCRDefaultConfig = map[string]interface{}{
		common.EnvPaddleOCRBaseUrl:     "",
		common.EnvPaddleOCRApiURL:      "",
		common.EnvPaddleOCRAccessToken: nil,
		common.EnvPaddleOCRAlgorithm:   "PaddleOCR-VL",
	}
	openDataLoaderEnvKeys = []string{
		common.EnvOpenDataLoaderApiServer,
	}
	openDataLoaderDefaultConfig = map[string]interface{}{
		common.EnvOpenDataLoaderApiServer: "",
	}
)

// collectEnvConfig collects environment variable values for the given keys
// into a map pre-populated with defaultConfig. Returns nil if none of the
// env vars are actually set, matching Python's _collect_env_config.
func collectEnvConfig(envKeys []string, defaultConfig map[string]interface{}) map[string]interface{} {
	config := make(map[string]interface{}, len(defaultConfig))
	for k, v := range defaultConfig {
		config[k] = v
	}
	found := false
	for _, key := range envKeys {
		value := os.Getenv(key)
		if value != "" {
			found = true
			config[key] = value
		}
	}
	if !found {
		return nil
	}
	return config
}

// ensureOCRProviderFromEnv mirrors Python's _ensure_ocr_provider_from_env.
// It finds or creates a provider, instance, and model for the given OCR provider.
func (m *ModelProviderService) ensureOCRProviderFromEnv(ctx context.Context, tenantID, providerName, modelName string, config map[string]interface{}) error {
	if config == nil {
		return nil
	}

	// 1. Find or create the provider.
	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		if !dao.IsNotFoundErr(err) {
			return fmt.Errorf("failed to get provider %s: %w", providerName, err)
		}
		providerID := utility.GenerateToken()
		provider = &entity.TenantModelProvider{
			ID:           providerID,
			TenantID:     tenantID,
			ProviderName: providerName,
		}
		if err = m.modelProviderDAO.Create(ctx, dao.DB, provider); err != nil {
			return fmt.Errorf("failed to create provider %s: %w", providerName, err)
		}
	}

	// 2. Find or create the instance (api_key stores the JSON config for dedup).
	apiKeyBytes, err := json.Marshal(config)
	if err != nil {
		return fmt.Errorf("failed to marshal config for %s: %w", providerName, err)
	}
	apiKey := string(apiKeyBytes)

	instance, err := m.modelInstanceDAO.GetInstanceByApiKey(ctx, dao.DB, apiKey, provider.ID)
	if err != nil {
		if !dao.IsNotFoundErr(err) {
			return fmt.Errorf("failed to get instance for %s: %w", providerName, err)
		}
		instanceID := utility.GenerateToken()
		instance = &entity.TenantModelInstance{
			ID:           instanceID,
			ProviderID:   provider.ID,
			InstanceName: modelName,
			APIKey:       apiKey,
			Extra:        "{}",
		}
		if err = m.modelInstanceDAO.Create(ctx, dao.DB, instance); err != nil {
			return fmt.Errorf("failed to create instance for %s: %w", providerName, err)
		}
	}

	// 3. Find or create the model.
	_, err = m.modelDAO.GetByProviderIDAndInstanceIDAndModelTypeAndModelName(
		ctx,
		dao.DB,
		provider.ID,
		instance.ID,
		int(entity.ModelTypeOCR),
		modelName,
	)
	if err != nil {
		if !dao.IsNotFoundErr(err) {
			return fmt.Errorf("failed to get model for %s: %w", providerName, err)
		}
		var extraBytes []byte
		extraBytes, err = json.Marshal(map[string]int{"max_tokens": 0})
		if err != nil {
			return fmt.Errorf("failed to marshal extra for %s model: %w", providerName, err)
		}
		modelID := utility.GenerateToken()
		tenantModel := &entity.TenantModel{
			ID:         modelID,
			ModelName:  modelName,
			ProviderID: provider.ID,
			InstanceID: instance.ID,
			ModelType:  int(entity.ModelTypeOCR),
			Status:     "active",
			Extra:      string(extraBytes),
		}
		if err = m.modelDAO.Create(ctx, dao.DB, tenantModel); err != nil {
			return fmt.Errorf("failed to create model for %s: %w", providerName, err)
		}
	}

	return nil
}

func (m *ModelProviderService) AlterProviderInstance(ctx context.Context, userID, providerIDOrName, instanceIDOrName, newInstanceName, apiKey, baseURL, region string, modelInfo []CreateInstanceModelInfo, verify bool) (common.ErrorCode, error) {
	providerIDOrName = strings.TrimSpace(providerIDOrName)

	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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.getProviderByIDOrName(ctx, tenantID, providerIDOrName)
	if err != nil {
		return common.CodeNotFound, fmt.Errorf("provider '%s' does not exist", providerIDOrName)
	}
	providerName := provider.ProviderName

	// Find the instance — try by ID first, then by name.
	instance, err := m.modelInstanceDAO.GetByID(ctx, dao.DB, instanceIDOrName)
	if err != nil || instance.ProviderID != provider.ID {
		instance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, 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(ctx, 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(ctx, dao.DB, 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(ctx, dao.DB, 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(ctx, dao.DB, 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(ctx, dao.DB, existingMdl.ID, updates); err != nil {
						return common.CodeServerError, err
					}
				}
			} else {
				// Add new model.
				if err = m.addModelToInstance(ctx, tenantID, providerName, effectiveInstanceName, mdl); err != nil {
					return common.CodeServerError, err
				}
			}
		}
	}

	return common.CodeSuccess, nil
}

func (m *ModelProviderService) DropProviderInstances(ctx context.Context, providerIDOrName, userID string, instanceIDOrNames []string) (common.ErrorCode, error) {
	if len(instanceIDOrNames) == 0 {
		return common.CodeBadRequest, errors.New("instances is required")
	}

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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

	// Find provider by ID or name
	provider, err := m.getProviderByIDOrName(ctx, tenantID, providerIDOrName)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return common.CodeNotFound, fmt.Errorf("no provider found for provider %q", providerIDOrName)
		}
		return common.CodeServerError, err
	}

	// First pass: resolve all instance IDs and collect not-found instances.
	// Mirrors Python's drop_provider_instances which returns error immediately
	// if any instance does not exist, without performing partial deletion.
	notExistInstances := make([]string, 0)
	instanceIDs := make([]string, 0, len(instanceIDOrNames))
	for _, idOrName := range instanceIDOrNames {
		var instance *entity.TenantModelInstance
		// Try by ID first, then by name — same as Python.
		if idOrName != "" {
			instance, err = m.modelInstanceDAO.GetByID(ctx, dao.DB, idOrName)
			if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
				return common.CodeServerError, err
			}
			if instance != nil && instance.ProviderID != provider.ID {
				instance = nil
			}
		}
		if instance == nil {
			instance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, idOrName)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					notExistInstances = append(notExistInstances, idOrName)
					continue
				}
				return common.CodeServerError, err
			}
		}
		instanceIDs = append(instanceIDs, instance.ID)
	}

	if len(notExistInstances) > 0 {
		return common.CodeNotFound, fmt.Errorf("no instance found for provider %q and instances %q", providerIDOrName, notExistInstances)
	}

	// Second pass: delete models and instances by IDs.
	// Mirrors Python's: delete_models_by_instance_ids(instance_ids)
	//                   TenantModelInstanceService.delete_by_ids(instance_ids)
	if _, err = m.modelDAO.DeleteByInstanceIDs(ctx, dao.DB, instanceIDs); err != nil {
		return common.CodeServerError, err
	}
	if _, err = m.modelInstanceDAO.DeleteByIDs(ctx, dao.DB, instanceIDs); err != nil {
		return common.CodeServerError, err
	}

	return common.CodeSuccess, nil
}

func (m *ModelProviderService) DropInstanceModels(ctx context.Context, providerIDOrName, instanceIDOrName, userID string, modelNames []string) (common.ErrorCode, error) {
	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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

	// Get provider by ID or name (matches Python's get_by_tenant_id_and_provider_id → get_by_tenant_id_and_provider_name fallback).
	provider, err := m.getProviderByIDOrName(ctx, tenantID, providerIDOrName)
	if err != nil {
		return common.CodeDataError, fmt.Errorf("no provider found for provider '%s'", providerIDOrName)
	}

	// Get instance by ID or name (matches Python's get_by_id → get_by_provider_id_and_instance_name fallback).
	instance, err := m.modelInstanceDAO.GetByID(ctx, dao.DB, instanceIDOrName)
	if err != nil || instance.ProviderID != provider.ID {
		instance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceIDOrName)
	}
	if err != nil {
		return common.CodeDataError, fmt.Errorf("no instance found for provider '%s' and instance '%s'", providerIDOrName, instanceIDOrName)
	}

	// Fetch all models under this instance and validate all requested names exist.
	modelObjs, err := m.modelDAO.GetModelsByInstanceID(ctx, dao.DB, instance.ID)
	if err != nil {
		return common.CodeServerError, err
	}

	existingNames := make(map[string]struct{}, len(modelObjs))
	for _, obj := range modelObjs {
		existingNames[obj.ModelName] = struct{}{}
	}

	notExist := make([]string, 0)
	for _, name := range modelNames {
		if _, ok := existingNames[name]; !ok {
			notExist = append(notExist, name)
		}
	}
	if len(notExist) > 0 {
		return common.CodeNotFound, fmt.Errorf("models %v not found for provider '%s' and instance '%s'", notExist, providerIDOrName, instanceIDOrName)
	}

	// Collect IDs of only the requested models to delete.
	requestedNames := make(map[string]struct{}, len(modelNames))
	for _, name := range modelNames {
		requestedNames[name] = struct{}{}
	}
	idsToDelete := make([]string, 0, len(modelNames))
	for _, obj := range modelObjs {
		if _, ok := requestedNames[obj.ModelName]; ok {
			idsToDelete = append(idsToDelete, obj.ID)
		}
	}

	if _, err = m.modelDAO.DeleteByIDs(ctx, dao.DB, idsToDelete); err != nil {
		return common.CodeServerError, err
	}

	return common.CodeSuccess, nil
}

func (m *ModelProviderService) ListInstanceModels(ctx context.Context, providerName, instanceName, userID string) ([]map[string]interface{}, error) {
	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, err
	}

	if len(tenants) == 0 {
		return nil, errors.New("user has no tenants")
	}

	tenantID := tenants[0].TenantID

	// Find provider by ID or name
	provider, err := m.getProviderByIDOrName(ctx, tenantID, providerName)
	if err != nil {
		return nil, err
	}

	// Find instance by ID first, then by name
	instance, err := m.modelInstanceDAO.GetByID(ctx, dao.DB, instanceName)
	if err != nil || instance.ProviderID != provider.ID {
		instance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
		if err != nil {
			return nil, err
		}
	}

	// Get all models for this instance
	modelObjs, err := m.modelDAO.GetModelsByInstanceID(ctx, dao.DB, instance.ID)
	if err != nil {
		return nil, err
	}

	modelList := make([]map[string]interface{}, 0, len(modelObjs))
	for _, model := range modelObjs {
		modelExtra := make(map[string]interface{})
		if model.Extra != "" {
			_ = json.Unmarshal([]byte(model.Extra), &modelExtra)
		}

		maxTokens := 8192
		if v, ok := modelExtra["max_tokens"]; ok {
			switch val := v.(type) {
			case float64:
				maxTokens = int(val)
			case int:
				maxTokens = val
			}
		}

		verify := "unknown"
		if v, ok := modelExtra["verify"].(string); ok {
			verify = v
		}

		features := make([]string, 0)
		if v, ok := modelExtra["is_tools"]; ok && isTruthy(v) {
			features = append(features, "is_tools")
		}
		if v, ok := modelExtra["thinking"]; ok && isTruthy(v) {
			features = append(features, "thinking")
		}

		modelList = append(modelList, map[string]interface{}{
			"name":       model.ModelName,
			"model_type": entity.ModelType(model.ModelType).HumanReadable(),
			"max_tokens": maxTokens,
			"status":     model.Status,
			"verify":     verify,
			"features":   features,
		})
	}

	// Sort by name
	sort.Slice(modelList, func(i, j int) bool {
		return modelList[i]["name"].(string) < modelList[j]["name"].(string)
	})

	return modelList, nil
}

func (m *ModelProviderService) AlterModel(ctx context.Context, providerName, instanceName, modelName, userID, modelID string, updateDict map[string]interface{}) (common.ErrorCode, error) {
	modelName = strings.TrimSpace(modelName)
	modelID = strings.TrimSpace(modelID)
	if modelName == "" && modelID == "" {
		return common.CodeBadRequest, errors.New("model name or model ID is required")
	}

	// Validate status early, before any DB lookup.
	if status, ok := updateDict["status"].(string); ok && status != "" {
		if status != "active" && status != "inactive" {
			return common.CodeBadRequest, errors.New("status must be 'active' or 'inactive'")
		}
	}

	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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

	// Check if provider exists
	provider, err := m.getProviderByIDOrName(ctx, tenantID, providerName)
	if err != nil {
		return common.CodeServerError, err
	}

	instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
	if err != nil {
		return common.CodeServerError, err
	}

	var model *entity.TenantModel
	if modelName != "" {
		model, err = m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(ctx, dao.DB, provider.ID, instance.ID, modelName)
		if err != nil {
			if !errors.Is(err, gorm.ErrRecordNotFound) {
				return common.CodeServerError, err
			}
			return common.CodeNotFound, fmt.Errorf("model %q not found for provider %q and instance %q", modelName, providerName, instanceName)
		}
		if modelID != "" && model.ID != modelID {
			return common.CodeBadRequest, errors.New("model ID does not match model name")
		}
	} else {
		model, err = m.modelDAO.GetByID(ctx, dao.DB, modelID)
		if err != nil {
			if !errors.Is(err, gorm.ErrRecordNotFound) {
				return common.CodeServerError, err
			}
			return common.CodeNotFound, fmt.Errorf("model with ID %q not found", modelID)
		}
		if model.ProviderID != provider.ID || model.InstanceID != instance.ID {
			return common.CodeNotFound, fmt.Errorf("model with ID %q does not belong to provider %q and instance %q", modelID, providerName, instanceName)
		}
	}

	toUpdate := make(map[string]interface{})

	// Handle status (validation already done above, here we only apply the change)
	if status, ok := updateDict["status"].(string); ok && status != "" {
		if status != model.Status {
			toUpdate["status"] = status
		}
	}

	// Handle model_type
	if modelTypeVal, ok := updateDict["model_type"]; ok && modelTypeVal != nil {
		targetModelType := resolveModelType(modelTypeVal)
		if targetModelType != model.ModelType {
			toUpdate["model_type"] = targetModelType
		}
	}

	// Handle extra (max_tokens, extra)
	newExtra := make(map[string]interface{})
	if extraVal, ok := updateDict["extra"]; ok && extraVal != nil {
		if extraMap, ok := extraVal.(map[string]interface{}); ok {
			for k, v := range extraMap {
				newExtra[k] = v
			}
		}
	}
	if maxTokensVal, ok := updateDict["max_tokens"]; ok {
		switch v := maxTokensVal.(type) {
		case int:
			if v > 0 {
				newExtra["max_tokens"] = v
			}
		case float64:
			if v > 0 {
				newExtra["max_tokens"] = int(v)
			}
		}
	}
	if len(newExtra) > 0 {
		dbExtra := make(map[string]interface{})
		if model.Extra != "" {
			_ = json.Unmarshal([]byte(model.Extra), &dbExtra)
		}
		for k, v := range newExtra {
			dbExtra[k] = v
		}
		extraBytes, err := json.Marshal(dbExtra)
		if err != nil {
			return common.CodeServerError, err
		}
		toUpdate["extra"] = string(extraBytes)
	}

	if len(toUpdate) > 0 {
		if err = m.modelDAO.UpdateByID(ctx, dao.DB, model.ID, toUpdate); err != nil {
			return common.CodeServerError, err
		}
	}

	return common.CodeSuccess, nil
}

// resolveModelType converts a model_type value (string, []string, or float64) to an int bitmask.
func resolveModelType(val interface{}) int {
	switch v := val.(type) {
	case string:
		return int(entity.ModelTypeFromString(v))
	case []interface{}:
		types := make([]string, 0, len(v))
		for _, t := range v {
			if s, ok := t.(string); ok {
				types = append(types, s)
			}
		}
		return int(entity.ModelTypeFromStrings(types))
	case float64:
		return int(v)
	default:
		return 0
	}
}

type ModelInstanceAndProviderInfo struct {
	ProviderEntity *entity.TenantModelProvider
	ProviderInfo   *modelModule.Provider
	InstanceEntity *entity.TenantModelInstance
	ModelEntity    *entity.TenantModel
	ModelInfo      *modelModule.Model
	APIConfig      *modelModule.APIConfig
}

type tenantModelExtra struct {
	MaxTokens    *int     `json:"max_tokens"`
	ModelTypes   []string `json:"model_types"`
	MaxDimension *int     `json:"max_dimension"`
	Dimensions   []int    `json:"dimensions"`
	Thinking     *bool    `json:"thinking"`
}

func modelInfoWithTenantExtra(modelInfo *modelModule.Model, modelEntity *entity.TenantModel) (*modelModule.Model, error) {
	if modelInfo == nil || modelEntity == nil || strings.TrimSpace(modelEntity.Extra) == "" {
		return modelInfo, nil
	}

	var extra tenantModelExtra
	if err := json.Unmarshal([]byte(modelEntity.Extra), &extra); err != nil {
		return nil, err
	}

	model := *modelInfo
	model.ModelTypes = append([]string(nil), modelInfo.ModelTypes...)
	model.Dimensions = append([]int(nil), modelInfo.Dimensions...)
	model.Alias = append([]string(nil), modelInfo.Alias...)
	if modelInfo.ModelTypeMap != nil {
		model.ModelTypeMap = make(map[string]bool, len(modelInfo.ModelTypeMap))
		for modelType, enabled := range modelInfo.ModelTypeMap {
			model.ModelTypeMap[modelType] = enabled
		}
	}
	if modelInfo.Thinking != nil {
		thinking := *modelInfo.Thinking
		model.Thinking = &thinking
	}

	if extra.MaxTokens != nil && *extra.MaxTokens > 0 {
		model.MaxTokens = extra.MaxTokens
	}
	if len(extra.ModelTypes) > 0 {
		model.ModelTypes = append([]string(nil), extra.ModelTypes...)
		model.ModelTypeMap = make(map[string]bool, len(extra.ModelTypes))
		for _, modelType := range extra.ModelTypes {
			model.ModelTypeMap[modelType] = true
		}
	}
	if extra.MaxDimension != nil && *extra.MaxDimension > 0 {
		model.MaxDimension = extra.MaxDimension
	}
	if len(extra.Dimensions) > 0 {
		model.Dimensions = append([]int(nil), extra.Dimensions...)
	}
	if extra.Thinking != nil {
		if model.Thinking == nil {
			model.Thinking = &modelModule.ModelThinking{}
		}
		model.Thinking.DefaultValue = *extra.Thinking
	}

	return &model, nil
}

func maxTokensFromTenantModelExtra(modelEntity *entity.TenantModel, fallback int) (int, error) {
	if modelEntity == nil || strings.TrimSpace(modelEntity.Extra) == "" {
		return fallback, nil
	}
	var extra tenantModelExtra
	if err := json.Unmarshal([]byte(modelEntity.Extra), &extra); err != nil {
		return 0, err
	}
	if extra.MaxTokens != nil && *extra.MaxTokens > 0 {
		return *extra.MaxTokens, nil
	}
	return fallback, nil
}

func (m *ModelProviderService) getModelInstanceAndProviderByName(ctx context.Context, providerName, instanceName, modelName *string, userID string, apiConfig *modelModule.APIConfig) (*ModelInstanceAndProviderInfo, error) {
	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, err
	}

	if len(tenants) == 0 {
		return nil, err
	}

	tenantID := tenants[0].TenantID

	// Check if provider exists
	providerEntity, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, *providerName)
	if err != nil {
		return nil, err
	}

	instanceEntity, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, providerEntity.ID, *instanceName)
	if err != nil {
		return nil, err
	}

	modelEntity, err := m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(ctx, dao.DB, providerEntity.ID, instanceEntity.ID, *modelName)
	if err != nil {
		// Not found model
		modelEntity = nil
	}

	providerInfo := dao.GetModelProviderManager().FindProvider(*providerName)
	if providerInfo == nil {
		return nil, errors.New("provider not found")
	}

	modelInfo, err := dao.GetModelProviderManager().GetModelByName(*providerName, *modelName)
	if err != nil {
		return nil, errors.New(fmt.Sprintf("provider %s model %s not found", *providerName, *modelName))
	}
	modelInfo, err = modelInfoWithTenantExtra(modelInfo, modelEntity)
	if err != nil {
		return nil, err
	}

	var extra map[string]string
	err = json.Unmarshal([]byte(instanceEntity.Extra), &extra)
	if err != nil {
		return nil, err
	}

	if apiConfig == nil {
		apiConfig = &modelModule.APIConfig{}
	}

	region := extra["region"]
	baseURL := extra["base_url"]

	apiConfig.ApiKey = &instanceEntity.APIKey
	apiConfig.BaseURL = &baseURL
	apiConfig.Region = &region

	var result = &ModelInstanceAndProviderInfo{
		ProviderEntity: providerEntity,
		ProviderInfo:   providerInfo,
		InstanceEntity: instanceEntity,
		ModelEntity:    modelEntity,
		ModelInfo:      modelInfo,
		APIConfig:      apiConfig,
	}

	return result, nil
}

func (m *ModelProviderService) getModelInstanceAndProviderByID(ctx context.Context, modelID *string, userID string, apiConfig *modelModule.APIConfig) (*ModelInstanceAndProviderInfo, error) {
	// Get tenant ID from user
	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, userID, "owner")
	if err != nil {
		return nil, err
	}

	if len(tenants) == 0 {
		return nil, err
	}

	tenantID := tenants[0].TenantID

	modelEntity, err := m.modelDAO.GetByID(ctx, dao.DB, *modelID)
	if err != nil {
		return nil, err
	}

	instanceEntity, err := m.modelInstanceDAO.GetByID(ctx, dao.DB, modelEntity.InstanceID)
	if err != nil {
		return nil, err
	}

	providerEntity, err := m.modelProviderDAO.GetByID(ctx, dao.DB, instanceEntity.ProviderID)
	if err != nil {
		return nil, err
	}

	if providerEntity.TenantID != tenantID {
		return nil, errors.New("provider not found")
	}

	providerInfo := dao.GetModelProviderManager().FindProvider(providerEntity.ProviderName)
	if providerInfo == nil {
		return nil, errors.New("provider not found")
	}

	modelInfo, err := dao.GetModelProviderManager().GetModelByName(providerEntity.ProviderName, modelEntity.ModelName)
	if err != nil {
		return nil, errors.New(fmt.Sprintf("provider %s model %s not found", providerEntity.ProviderName, modelEntity.ModelName))
	}
	modelInfo, err = modelInfoWithTenantExtra(modelInfo, modelEntity)
	if err != nil {
		return nil, err
	}

	var extra map[string]string
	err = json.Unmarshal([]byte(instanceEntity.Extra), &extra)
	if err != nil {
		return nil, err
	}

	if apiConfig == nil {
		apiConfig = &modelModule.APIConfig{}
	}

	region := extra["region"]
	baseURL := extra["base_url"]

	apiConfig.ApiKey = &instanceEntity.APIKey
	apiConfig.BaseURL = &baseURL
	apiConfig.Region = &region

	var result = &ModelInstanceAndProviderInfo{
		ProviderEntity: providerEntity,
		ProviderInfo:   providerInfo,
		InstanceEntity: instanceEntity,
		ModelEntity:    modelEntity,
		ModelInfo:      modelInfo,
		APIConfig:      apiConfig,
	}

	return result, nil
}

// ChatToModelWithMessages sends messages to the model with messages array
func (m *ModelProviderService) ChatToModelWithMessages(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig, modelUsage *common.ModelUsage) (*modelModule.ChatResponse, common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.ChatConfig{}
	}
	modelConfig.ModelClass = info.ModelInfo.Class
	if modelConfig.Thinking == nil && info.ModelInfo.Thinking != nil {
		thinking := info.ModelInfo.Thinking.DefaultValue
		modelConfig.Thinking = &thinking
	}
	resolvedModelName := info.ModelInfo.Name
	if info.ModelEntity != nil && info.ModelEntity.ModelName != "" {
		resolvedModelName = info.ModelEntity.ModelName
	}
	resolvedProviderName := info.ProviderEntity.ProviderName

	var response *modelModule.ChatResponse
	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		if !info.ModelInfo.ModelTypeMap["chat"] && !info.ModelInfo.ModelTypeMap["vision"] {
			return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a chat or multimodal model", resolvedModelName, resolvedProviderName))
		}
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeChat) && !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeImage2Text) {
				return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a chat or multimodal model", resolvedModelName, resolvedProviderName))
			}

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, resolvedProviderName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return nil, common.CodeServerError, err
			}
		} else {
			return nil, common.CodeServerError, errors.New("model is inactive")
		}
	}

	modelUsage.TenantID = info.ProviderEntity.TenantID
	modelUsage.InstanceID = info.InstanceEntity.ID
	modelUsage.APIKey = info.InstanceEntity.APIKey

	response, err = modelDriver.ChatWithMessages(ctx, resolvedModelName, messages, info.APIConfig, modelConfig, modelUsage)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	if response == nil {
		return nil, common.CodeServerError, errors.New("empty chat response")
	}

	return response, common.CodeSuccess, nil
}

// ChatToModelStreamWithSender streams chat response directly via sender function ( the best performance, no channel)
func (m *ModelProviderService) ChatToModelStreamWithSender(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig, modelUsage *common.ModelUsage, sender func(*string, *string) error) (common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.ChatConfig{}
	}
	modelConfig.ModelClass = info.ModelInfo.Class
	if modelConfig.Thinking == nil && info.ModelInfo.Thinking != nil {
		thinking := info.ModelInfo.Thinking.DefaultValue
		modelConfig.Thinking = &thinking
	}
	resolvedModelName := info.ModelInfo.Name
	if info.ModelEntity != nil && info.ModelEntity.ModelName != "" {
		resolvedModelName = info.ModelEntity.ModelName
	}
	resolvedProviderName := info.ProviderEntity.ProviderName

	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeChat) && !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeImage2Text) {
				return common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a chat or multimodal model", resolvedModelName, resolvedProviderName))
			}

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, resolvedProviderName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return common.CodeServerError, err
			}
		} else {
			return common.CodeServerError, errors.New("model is inactive")
		}
	}

	modelUsage.TenantID = info.ProviderEntity.TenantID
	modelUsage.InstanceID = info.InstanceEntity.ID
	modelUsage.APIKey = info.InstanceEntity.APIKey

	err = modelDriver.ChatStreamlyWithSender(ctx, resolvedModelName, messages, info.APIConfig, modelConfig, modelUsage, sender)
	if err != nil {
		return common.CodeServerError, err
	}
	return common.CodeSuccess, nil
}

func validateEmbeddingDimension(model *modelModule.Model, requested int) error {
	if requested <= 0 || model == nil {
		return nil
	}

	if len(model.Dimensions) > 0 {
		for _, dim := range model.Dimensions {
			if dim == requested {
				return nil
			}
		}
		return fmt.Errorf(
			"dimension %d is not supported by model %s, supported dimensions: %v",
			requested,
			model.Name,
			model.Dimensions,
		)
	}
	if model.MaxDimension != nil && requested > *model.MaxDimension {
		return fmt.Errorf(
			"dimension %d is not supported by model %s, max dimension: %d",
			requested,
			model.Name,
			*model.MaxDimension,
		)
	}

	return nil
}

// EmbedText sends texts to the embedding model
func (m *ModelProviderService) EmbedText(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, texts []string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.EmbeddingConfig) ([]modelModule.EmbeddingData, common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.EmbeddingConfig{}
	}
	resolvedModelName := info.ModelInfo.Name
	if info.ModelEntity != nil && info.ModelEntity.ModelName != "" {
		resolvedModelName = info.ModelEntity.ModelName
	}
	resolvedProviderName := info.ProviderEntity.ProviderName

	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		if !info.ModelInfo.ModelTypeMap["embedding"] {
			return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an embedding model", resolvedModelName, resolvedProviderName))
		}
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeEmbedding) {
				return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an embedding model", resolvedModelName, resolvedProviderName))
			}

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, resolvedProviderName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return nil, common.CodeServerError, err
			}
		} else {
			return nil, common.CodeServerError, errors.New("model is inactive")
		}
	}

	if err = validateEmbeddingDimension(info.ModelInfo, modelConfig.Dimension); err != nil {
		return nil, common.CodeBadRequest, err
	}

	var response []modelModule.EmbeddingData
	response, err = modelDriver.Embed(ctx, &resolvedModelName, texts, info.APIConfig, modelConfig, nil)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	if response == nil || len(response) == 0 {
		return nil, common.CodeServerError, errors.New("empty embed response")
	}

	return response, common.CodeSuccess, nil
}

// RerankDocument sends texts to the embedding model
func (m *ModelProviderService) RerankDocument(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID, query string, documents []string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.RerankConfig) (*modelModule.RerankResponse, common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.RerankConfig{}
	}
	resolvedModelName := info.ModelInfo.Name
	if info.ModelEntity != nil && info.ModelEntity.ModelName != "" {
		resolvedModelName = info.ModelEntity.ModelName
	}
	resolvedProviderName := info.ProviderEntity.ProviderName

	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		if !info.ModelInfo.ModelTypeMap["rerank"] {
			return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a rerank model", resolvedModelName, resolvedProviderName))
		}
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeRerank) {
				return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a rerank model", resolvedModelName, resolvedProviderName))
			}

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, resolvedProviderName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return nil, common.CodeServerError, err
			}
		} else {
			return nil, common.CodeServerError, errors.New("model is inactive")
		}
	}

	var response *modelModule.RerankResponse
	response, err = modelDriver.Rerank(ctx, &resolvedModelName, query, documents, info.APIConfig, modelConfig, nil)
	if err != nil {
		return nil, common.CodeServerError, err
	}

	return response, common.CodeSuccess, nil
}

// TranscribeAudio transcribe audio file to text
func (m *ModelProviderService) TranscribeAudio(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, audioFile *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ASRConfig) (*modelModule.ASRResponse, common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.ASRConfig{}
	}

	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		if !info.ModelInfo.ModelTypeMap["asr"] {
			return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an ASR model", *modelName, *providerName))
		}
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeSpeech2Text) {
				return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an ASR model", *modelName, *providerName))
			}

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, *providerName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return nil, common.CodeServerError, err
			}
		} else {
			return nil, common.CodeServerError, errors.New("model is inactive")
		}
	}

	var response *modelModule.ASRResponse
	response, err = modelDriver.TranscribeAudio(ctx, modelName, audioFile, apiConfig, modelConfig, nil)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	if response == nil {
		return nil, common.CodeServerError, errors.New("empty chat response")
	}

	return response, common.CodeSuccess, nil
}

// TranscribeAudioStream transcribe audio file to text stream directly via sender function ( the best performance, no channel)
func (m *ModelProviderService) TranscribeAudioStream(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, audioFile *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ASRConfig, sender func(*string, *string) error) (common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.ASRConfig{}
	}

	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		if !info.ModelInfo.ModelTypeMap["asr"] {
			return common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an ASR model", *modelName, *providerName))
		}
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeSpeech2Text) {
				return common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an ASR model", *modelName, *providerName))
			}

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, *providerName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return common.CodeServerError, err
			}
		} else {
			return common.CodeServerError, errors.New("model is inactive")
		}
	}

	err = modelDriver.TranscribeAudioWithSender(ctx, modelName, audioFile, apiConfig, modelConfig, nil, sender)
	if err != nil {
		return common.CodeServerError, err
	}

	return common.CodeSuccess, nil
}

// AudioSpeech convert audio to speech
func (m *ModelProviderService) AudioSpeech(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, audioContent *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.TTSConfig) (*modelModule.TTSResponse, common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.TTSConfig{}
	}

	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		if !info.ModelInfo.ModelTypeMap["tts"] {
			return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a TTS model", *modelName, *providerName))
		}
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeTTS) {
				return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a TTS model", *modelName, *providerName))
			}

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, *providerName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return nil, common.CodeServerError, err
			}
		} else {
			return nil, common.CodeServerError, errors.New("model is inactive")
		}
	}

	var response *modelModule.TTSResponse
	response, err = modelDriver.AudioSpeech(ctx, modelName, audioContent, apiConfig, modelConfig, nil)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	if response == nil {
		return nil, common.CodeServerError, errors.New("empty chat response")
	}

	return response, common.CodeSuccess, nil
}

func (m *ModelProviderService) AudioSpeechStream(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, audioContent *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.TTSConfig, sender func(*string, *string) error) (common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.TTSConfig{}
	}

	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		if !info.ModelInfo.ModelTypeMap["tts"] {
			return common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a TTS model", *modelName, *providerName))
		}
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeTTS) {
				return common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a TTS model", *modelName, *providerName))
			}

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, *providerName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return common.CodeServerError, err
			}
		} else {
			return common.CodeServerError, errors.New("model is inactive")
		}
	}

	err = modelDriver.AudioSpeechWithSender(ctx, modelName, audioContent, apiConfig, modelConfig, nil, sender)
	if err != nil {
		return common.CodeServerError, err
	}

	return common.CodeSuccess, nil
}

func (m *ModelProviderService) OCRFile(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, content []byte, url *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.OCRConfig) (*modelModule.OCRFileResponse, common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.OCRConfig{}
	}

	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		if !info.ModelInfo.ModelTypeMap["ocr"] {
			return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an OCR model", *modelName, *providerName))
		}
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeOCR) {
				return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an OCR model", *modelName, *providerName))
			}

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, *providerName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return nil, common.CodeServerError, err
			}
		} else {
			return nil, common.CodeServerError, errors.New("model is inactive")
		}
	}

	var response *modelModule.OCRFileResponse
	response, err = modelDriver.OCRFile(ctx, modelName, content, url, apiConfig, modelConfig, nil)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	if response == nil {
		return nil, common.CodeServerError, errors.New("empty chat response")
	}

	return response, common.CodeSuccess, nil
}

func (m *ModelProviderService) ParseFile(ctx context.Context, providerName, instanceName, modelName, modelID *string, userID string, content []byte, url *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ParseFileConfig) (*modelModule.ParseFileResponse, common.ErrorCode, error) {

	var err error
	var info *ModelInstanceAndProviderInfo

	if modelID != nil {
		info, err = m.getModelInstanceAndProviderByID(ctx, modelID, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	} else {
		info, err = m.getModelInstanceAndProviderByName(ctx, providerName, instanceName, modelName, userID, apiConfig)
		if err != nil || info == nil {
			return nil, common.CodeNotFound, err
		}
	}

	if modelConfig == nil {
		modelConfig = &modelModule.ParseFileConfig{}
	}

	var modelDriver modelModule.ModelDriver

	if info.ModelEntity == nil {
		if !info.ModelInfo.ModelTypeMap["doc_parse"] {
			return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a ParseFile model", *modelName, *providerName))
		}
		modelDriver = info.ProviderInfo.ModelDriver
	} else {
		// model entity exists
		if info.ModelEntity.Status == "active" {
			// Note: ParseFile model type is not in the ModelType enum; skip entity-type check
			// and rely on the factory ModelTypeMap check in the nil-entity branch.

			modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, *providerName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
			if err != nil {
				return nil, common.CodeServerError, err
			}
		} else {
			return nil, common.CodeServerError, errors.New("model is inactive")
		}
	}

	var response *modelModule.ParseFileResponse
	response, err = modelDriver.ParseFile(ctx, modelName, content, url, apiConfig, modelConfig, nil)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	if response == nil {
		return nil, common.CodeServerError, errors.New("empty chat response")
	}

	return response, common.CodeSuccess, nil
}

// GetEmbeddingModel returns an EmbeddingModel wrapper for the given tenant
func (m *ModelProviderService) GetEmbeddingModel(ctx context.Context, tenantID, compositeModelName string) (*modelModule.EmbeddingModel, error) {
	driver, modelName, apiConfig, maxTokens, err := m.ResolveModelConfig(ctx, tenantID, entity.ModelTypeEmbedding, compositeModelName)
	if err != nil {
		return nil, err
	}
	return modelModule.NewEmbeddingModel(driver, &modelName, apiConfig, maxTokens), nil
}

// GetChatModel  returns a ChatModel wrapper for the given tenant
func (m *ModelProviderService) GetChatModel(ctx context.Context, tenantID, compositeModelName string) (*modelModule.ChatModel, error) {
	driver, modelName, apiConfig, _, err := m.ResolveModelConfig(ctx, tenantID, entity.ModelTypeChat, compositeModelName)
	if err != nil {
		return nil, err
	}
	return modelModule.NewChatModel(driver, &modelName, apiConfig), nil
}

// GetRerankModel returns a RerankModel wrapper for the given tenant
func (m *ModelProviderService) GetRerankModel(ctx context.Context, tenantID, compositeModelName string) (*modelModule.RerankModel, error) {
	driver, modelName, apiConfig, _, err := m.ResolveModelConfig(ctx, tenantID, entity.ModelTypeRerank, compositeModelName)
	if err != nil {
		return nil, err
	}
	return modelModule.NewRerankModel(driver, &modelName, apiConfig), nil
}

type AddModelRequest struct {
	ProviderName string                 `json:"provider_name"`
	InstanceName string                 `json:"instance_name"`
	ModelName    string                 `json:"model_name"`
	ModelTypes   []string               `json:"model_type"`
	MaxTokens    int                    `json:"max_tokens"`
	Extra        map[string]interface{} `json:"extra"`
}

func (m *ModelProviderService) GetTenantDefaultModelByType(ctx context.Context, tenantID string, modelType entity.ModelType) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) {
	if modelType == entity.ModelTypeOCR {
		return nil, "", nil, 0, fmt.Errorf("OCR model name is required")
	}

	tenant, err := m.tenantDAO.GetByID(ctx, dao.DB, tenantID)
	if err != nil {
		return nil, "", nil, 0, fmt.Errorf("failed to get tenant: %s type %s: %w", tenantID, modelType, err)
	}
	modelName, modelID := defaultModelRefs(tenant, modelType)
	if modelID != "" {
		driver, resolvedName, apiConfig, maxTokens, idErr := m.GetModelConfigByID(ctx, tenantID, modelType, modelID)
		if idErr == nil {
			return driver, resolvedName, apiConfig, maxTokens, nil
		}
		common.Warn("GetTenantDefaultModelByType: model_id lookup failed, falling back to model name",
			zap.String("tenantID", tenantID),
			zap.String("modelID", modelID),
			zap.Error(idErr))
	}
	if modelName == "" {
		return nil, "", nil, 0, fmt.Errorf("no default %s model is set", modelType)
	}

	return m.ResolveModelConfig(ctx, tenantID, modelType, modelName)
}

// GetModelConfigByID returns model driver and API config for a tenant_model row by its ID.
func (m *ModelProviderService) GetModelConfigByID(ctx context.Context, userID string, modelType entity.ModelType, modelID string) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) {
	common.Debug("GetModelConfigByID",
		zap.String("userID", userID),
		zap.String("modelType", modelType.String()),
		zap.String("modelID", modelID))

	modelEntity, err := m.modelDAO.GetByID(ctx, dao.DB, modelID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, "", nil, 0, fmt.Errorf("tenant model id=%s not found", modelID)
		}
		return nil, "", nil, 0, err
	}
	if modelEntity.Status != "active" {
		return nil, "", nil, 0, fmt.Errorf("tenant model id=%s is disabled", modelID)
	}
	if !entity.ModelType(modelEntity.ModelType).Has(modelType) {
		return nil, "", nil, 0, fmt.Errorf("tenant model id=%s cannot be used as %s model", modelID, modelType.String())
	}

	providerEntity, err := m.modelProviderDAO.GetByID(ctx, dao.DB, modelEntity.ProviderID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, "", nil, 0, fmt.Errorf("provider id=%s not found for model id=%s", modelEntity.ProviderID, modelID)
		}
		return nil, "", nil, 0, err
	}
	if providerEntity == nil {
		return nil, "", nil, 0, fmt.Errorf("provider id=%s not found for model id=%s", modelEntity.ProviderID, modelID)
	}

	if providerEntity.TenantID != userID {
		userTenants, terr := NewUserTenantService().GetUserTenantRelationByUserIDWithContext(ctx, userID)
		if terr != nil {
			return nil, "", nil, 0, terr
		}
		allowed := false
		for _, rel := range userTenants {
			if rel != nil && rel.TenantID == providerEntity.TenantID {
				allowed = true
				break
			}
		}
		if !allowed {
			return nil, "", nil, 0, fmt.Errorf("tenant %s has no access to provider owned by tenant %s", userID, providerEntity.TenantID)
		}
	}

	instanceEntity, err := m.modelInstanceDAO.GetByID(ctx, dao.DB, modelEntity.InstanceID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, "", nil, 0, fmt.Errorf("instance id=%s not found for model id=%s", modelEntity.InstanceID, modelID)
		}
		return nil, "", nil, 0, err
	}

	apiKey := instanceEntity.APIKey
	var extra map[string]string
	if err := json.Unmarshal([]byte(instanceEntity.Extra), &extra); err != nil {
		return nil, "", nil, 0, err
	}
	region := extra["region"]
	baseURL := extra["base_url"]

	providerInfo := dao.GetModelProviderManager().FindProvider(providerEntity.ProviderName)
	if providerInfo == nil {
		return nil, "", nil, 0, fmt.Errorf("provider %q driver not found", providerEntity.ProviderName)
	}
	modelDriver, err := newModelDriverForBaseURL(providerInfo.ModelDriver, providerEntity.ProviderName, region, baseURL)
	if err != nil {
		return nil, "", nil, 0, err
	}

	maxTokens := 0
	if mi, _ := dao.GetModelProviderManager().GetModelByName(providerEntity.ProviderName, modelEntity.ModelName); mi != nil {
		if mi.MaxTokens != nil {
			maxTokens = *mi.MaxTokens
		}
	}
	maxTokens, err = maxTokensFromTenantModelExtra(modelEntity, maxTokens)
	if err != nil {
		return nil, "", nil, 0, err
	}

	apiConfig := &modelModule.APIConfig{ApiKey: &apiKey, Region: &region, BaseURL: &baseURL}
	return modelDriver, modelEntity.ModelName, apiConfig, maxTokens, nil
}

func defaultModelRefs(tenant *entity.Tenant, modelType entity.ModelType) (string, string) {
	if tenant == nil {
		return "", ""
	}
	switch modelType {
	case entity.ModelTypeChat:
		return tenant.LLMID, ptrStringValue(tenant.TenantLLMID)
	case entity.ModelTypeEmbedding:
		return tenant.EmbdID, ptrStringValue(tenant.TenantEmbdID)
	case entity.ModelTypeRerank:
		return tenant.RerankID, ptrStringValue(tenant.TenantRerankID)
	case entity.ModelTypeSpeech2Text:
		return tenant.ASRID, ptrStringValue(tenant.TenantASRID)
	case entity.ModelTypeImage2Text:
		return tenant.Img2TxtID, ptrStringValue(tenant.TenantImg2TxtID)
	case entity.ModelTypeTTS:
		return *tenant.TTSID, ptrStringValue(tenant.TenantTTSID)
	case entity.ModelTypeOCR:
		return *tenant.OCRID, ptrStringValue(tenant.TenantOCRID)
	default:
		return "", ""
	}
}

func (m *ModelProviderService) ResolveModelConfig(ctx context.Context, tenantID string, modelType entity.ModelType, modelRef string) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) {
	if strings.TrimSpace(modelRef) == "" {
		return nil, "", nil, 0, fmt.Errorf("model ref is required")
	}
	if _, err := m.modelDAO.GetByID(ctx, dao.DB, modelRef); err == nil {
		return m.GetModelConfigByID(ctx, tenantID, modelType, modelRef)
	} else if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, "", nil, 0, err
	}
	return m.GetModelConfigFromProviderInstance(ctx, tenantID, modelType, modelRef)
}

func (m *ModelProviderService) ResolveModelID(ctx context.Context, tenantID string, modelType entity.ModelType, modelName string) (string, error) {
	if modelObj, err := m.modelDAO.GetByID(ctx, dao.DB, modelName); err == nil {
		if modelObj.Status != "active" {
			return "", fmt.Errorf("tenant model id=%s is disabled", modelName)
		}
		if !entity.ModelType(modelObj.ModelType).Has(modelType) {
			return "", fmt.Errorf("tenant model id=%s cannot be used as %s model", modelName, modelType.String())
		}
		if _, _, _, _, err = m.GetModelConfigByID(ctx, tenantID, modelType, modelName); err != nil {
			return "", err
		}
		return modelObj.ID, nil
	} else if !errors.Is(err, gorm.ErrRecordNotFound) {
		return "", err
	}

	pureModelName, instanceName, providerName, err := parseModelName(modelName)
	if err != nil {
		return "", err
	}

	// Builtin provider: Builtin models (e.g. local TEI embeddings) have no
	// tenant_model_instance records, so there is no tenant-scoped model ID to
	// resolve.  Downstream resolution (ResolveModelConfig / getModelConfig)
	// falls through to parseModelName → Builtin routing, which already works.
	if providerName == "Builtin" && modelType == entity.ModelTypeEmbedding {
		if builtinDriver := modelModule.GetBuiltinEmbeddingModel(pureModelName); builtinDriver == nil {
			return "", fmt.Errorf("builtin embedding model %q not found", pureModelName)
		}
		return "", nil
	}

	provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if err != nil {
		return "", fmt.Errorf("provider %q lookup failed: %w", providerName, err)
	}
	if provider == nil {
		return "", fmt.Errorf("provider %q not found for model %q", providerName, modelName)
	}

	instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
	if err != nil {
		return "", fmt.Errorf("instance %q lookup failed: %w", instanceName, err)
	}
	if instance == nil {
		return "", fmt.Errorf("instance %q not found for model %q", instanceName, modelName)
	}

	modelObj, err := m.modelDAO.GetByProviderIDAndInstanceIDAndModelTypeAndModelName(ctx, dao.DB, provider.ID, instance.ID, int(modelType), pureModelName)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return "", fmt.Errorf("model %q not found for model type %s", modelName, modelType.String())
		}
		return "", err
	}
	if modelObj.Status != "active" {
		return "", fmt.Errorf("model %q is disabled", modelName)
	}
	return modelObj.ID, nil
}

func (m *ModelProviderService) ResolveModelType(ctx context.Context, tenantID, modelRef string) ([]entity.ModelType, error) {
	modelObj, err := m.modelDAO.GetByID(ctx, dao.DB, modelRef)
	if err == nil {
		if modelObj.Status != "active" {
			return nil, fmt.Errorf("tenant model id=%s is disabled", modelRef)
		}
		return []entity.ModelType{entity.ModelType(modelObj.ModelType)}, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	return m.GetModelTypeByName(ctx, tenantID, modelRef)
}

// GetModelTypeByName returns the list of model types the given model is enrolled as.
func (m *ModelProviderService) GetModelTypeByName(ctx context.Context, tenantID, modelName string) ([]entity.ModelType, error) {
	common.Debug("GetModelTypeByName",
		zap.String("tenantID", tenantID),
		zap.String("modelName", modelName))

	pureModelName, instanceName, providerName, err := parseModelName(modelName)
	if err != nil {
		return nil, err
	}

	// Direct provider lookup
	provider, provErr := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if provErr != nil {
		return nil, fmt.Errorf("provider %q lookup failed: %w", providerName, provErr)
	}
	if provider == nil {
		return nil, fmt.Errorf("provider %q not found for model %q", providerName, modelName)
	}

	// Direct instance lookup
	instance, instErr := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
	if instErr != nil {
		return nil, fmt.Errorf("instance %q lookup failed: %w", instanceName, instErr)
	}
	if instance == nil {
		return nil, fmt.Errorf("instance %q not found for model %q", instanceName, modelName)
	}

	// Direct model lookup
	modelObjs, modelErr := m.modelDAO.GetModelsByProviderIDAndInstanceIDAndModelName(ctx, dao.DB, provider.ID, instance.ID, pureModelName)
	if modelErr == nil && len(modelObjs) > 0 {
		types := make([]entity.ModelType, 0, len(modelObjs))
		for _, obj := range modelObjs {
			types = append(types, entity.ModelType(obj.ModelType))
		}
		return types, nil
	}

	// Fallback: factory LLM catalog
	var extra map[string]string
	_ = json.Unmarshal([]byte(instance.Extra), &extra)
	region := extra["region"]

	targetFactoryName := providerName
	if region == "intl" && strings.EqualFold(providerName, "siliconflow") {
		targetFactoryName = "siliconflow_intl"
	}
	// Use the case-insensitive FindProvider / findModel helpers. The Go's
	// conf/models/*.json files now use the same names as the Python's
	// conf/llm_factories.json (e.g. "SILICONFLOW"); but a strict
	// `==` here would still fail for any case mismatch. The rest of the Go codebase
	// uses these helpers too.
	targetProvider := dao.GetModelProviderManager().FindProvider(targetFactoryName)
	if targetProvider == nil {
		return nil, fmt.Errorf("model provider config not found: %s", providerName)
	}
	for i := range targetProvider.Models {
		if strings.EqualFold(targetProvider.Models[i].Name, pureModelName) {
			if len(targetProvider.Models[i].ModelTypes) == 0 {
				return nil, fmt.Errorf("model %q has no model_types in factory catalog", pureModelName)
			}
			return []entity.ModelType{entity.ModelTypeFromString(targetProvider.Models[i].ModelTypes[0])}, nil
		}
	}
	return nil, fmt.Errorf("model %q not found for model %q", pureModelName, modelName)
}

type AddCustomModelRequest struct {
	ProviderName string   `json:"provider_name"`
	InstanceName string   `json:"instance_name"`
	ModelName    string   `json:"model_name"`
	ModelTypes   []string `json:"model_types"`
	MaxTokens    int      `json:"max_tokens"`
	Thinking     *bool    `json:"thinking"`
}

type ModelRequest struct {
	ModelName    string   `json:"model_name"`
	ModelTypes   []string `json:"model_types"`
	MaxTokens    int      `json:"max_tokens"`
	MaxDimension int      `json:"max_dimension"`
	Dimensions   []int    `json:"dimensions"`
	Thinking     *bool    `json:"thinking"`
}

func (m *ModelProviderService) AddModel(ctx context.Context, request *AddModelRequest, userID string) (common.ErrorCode, error) {
	if request == nil {
		return common.CodeBadRequest, errors.New("request is required")
	}

	modelName := strings.TrimSpace(request.ModelName)
	if modelName == "" {
		return common.CodeBadRequest, errors.New("model_name is required")
	}

	if len(request.ModelTypes) == 0 {
		return common.CodeBadRequest, errors.New("model_type is required")
	}

	tenants, err := m.userTenantDAO.GetByUserIDAndRole(ctx, dao.DB, 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

	// Get provider by ID or name (matches Python's get_by_tenant_id_and_provider_id → get_by_tenant_id_and_provider_name fallback).
	provider, err := m.getProviderByIDOrName(ctx, tenantID, request.ProviderName)
	if err != nil {
		return common.CodeDataError, fmt.Errorf("no provider found for provider '%s'", request.ProviderName)
	}

	// Get instance by ID or name (matches Python's get_by_id → get_by_provider_id_and_instance_name fallback).
	instance, err := m.modelInstanceDAO.GetByID(ctx, dao.DB, request.InstanceName)
	if err != nil || instance.ProviderID != provider.ID {
		instance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, request.InstanceName)
	}
	if err != nil {
		return common.CodeDataError, fmt.Errorf("no instance found for provider '%s' and instance '%s'", request.ProviderName, request.InstanceName)
	}

	// Check for duplicate model.
	_, err = m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(ctx, dao.DB, provider.ID, instance.ID, modelName)
	if err == nil {
		return common.CodeConflict, fmt.Errorf("model '%s' already exists for provider '%s' and instance '%s'", modelName, request.ProviderName, request.InstanceName)
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return common.CodeServerError, err
	}

	// Compute model type bitmask.
	combinedType := entity.ModelType(0)
	for _, rawType := range request.ModelTypes {
		mt := strings.TrimSpace(rawType)
		if mt == "" {
			continue
		}
		t := entity.ModelTypeFromString(mt)
		if t == 0 {
			return common.CodeBadRequest, fmt.Errorf("invalid model type: %s", mt)
		}
		combinedType |= t
	}

	maxTokens := request.MaxTokens
	if maxTokens <= 0 {
		maxTokens = 8192
	}

	// Build extra fields. Mirrors Python's add_model_to_instance.
	extraFields := map[string]interface{}{
		"max_tokens": maxTokens,
	}

	// Look up factory info to populate is_tools and thinking (matches Python).
	targetProvider := dao.GetModelProviderManager().FindProvider(provider.ProviderName)
	if targetProvider != nil {
		targetModel := dao.GetModelProviderManager().FindModel(targetProvider, modelName)
		if targetModel != nil {
			if targetModel.Tools != nil && targetModel.Tools.Support {
				extraFields["is_tools"] = true
			}
			if targetModel.Thinking != nil {
				extraFields["thinking"] = true
			}
		}
	}

	if request.Extra != nil {
		for k, v := range request.Extra {
			extraFields[k] = v
		}
	}

	extraBytes, err := json.Marshal(extraFields)
	if err != nil {
		return common.CodeServerError, errors.New("fail to marshal extra")
	}

	modelID := utility.GenerateToken()
	tenantModel := &entity.TenantModel{
		ID:         modelID,
		ModelName:  modelName,
		ModelType:  int(combinedType),
		ProviderID: provider.ID,
		InstanceID: instance.ID,
		Status:     "active",
		Extra:      string(extraBytes),
	}

	if err = m.modelDAO.Create(ctx, dao.DB, tenantModel); err != nil {
		return common.CodeServerError, fmt.Errorf("fail to create model '%s': %s", modelName, err.Error())
	}

	return common.CodeSuccess, nil
}

// GetModelConfigFromProviderInstance get model config from provider instance
// modelName@instance@provider or
// "model@provider" — the provider is required and is looked up directly via
// tenant_model_provider. For 2-part names the instance defaults to "default".
// If the model is enrolled in tenant_model, that row is used (and INACTIVE rows
// raise). Otherwise, the factory's LLM catalog is consulted, with
// region=intl + siliconflow redirected to the siliconflow_intl factory.
func (m *ModelProviderService) GetModelConfigFromProviderInstance(ctx context.Context, tenantID string, modelType entity.ModelType, modelName string) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) {
	common.Debug("GetModelConfigFromProviderInstance",
		zap.String("tenantID", tenantID),
		zap.String("modelName", modelName),
		zap.String("modelType", modelType.String()))

	// TEI builtin embedding short-circuit
	if modelType == entity.ModelTypeEmbedding && strings.Contains(common.GetEnv(common.EnvComposeProfiles), "tei-") {
		teiModel := common.GetEnv(common.EnvTEIModel)
		teiBaseURL := common.GetEnv(common.EnvTEIBaseURL)

		// First try exact match: handles bare model IDs like "model@q8_0"
		// where '@' is part of the model name itself.
		if modelName == teiModel {
			builtinDriver := modelModule.GetBuiltinEmbeddingModel(modelName)
			if builtinDriver == nil {
				return nil, "", nil, 0, fmt.Errorf("builtin (TEI) embedding model %q not found", modelName)
			}
			apiConfig := &modelModule.APIConfig{ApiKey: nil, Region: nil, BaseURL: &teiBaseURL}
			return builtinDriver, modelName, apiConfig, 0, nil
		}

		// Then try right-anchored parsing for explicit "model@Builtin" or
		// "model@instance@Builtin" composite keys.
		teiPure, _, teiProvider := splitRightAnchoredModelName(modelName)
		if teiPure == teiModel && (teiProvider == "Builtin" || teiProvider == "") {
			builtinDriver := modelModule.GetBuiltinEmbeddingModel(teiPure)
			if builtinDriver == nil {
				return nil, "", nil, 0, fmt.Errorf("builtin (TEI) embedding model %q not found", teiPure)
			}
			apiConfig := &modelModule.APIConfig{ApiKey: nil, Region: nil, BaseURL: &teiBaseURL}
			return builtinDriver, teiPure, apiConfig, 0, nil
		}
	}

	// Generic Builtin provider short-circuit. The tenant_model_provider /
	// tenant_model_instance tables don't have a row for "Builtin" because it's
	// a local service, not a tenant-enrolled provider — so the direct lookups
	// below would raise. Mirrors the private getModelConfig's Builtin branch.
	//
	// Gated on ModelTypeEmbedding because the Builtin driver here is the TEI
	// embedding endpoint; the underlying BuiltinModel's Chat/Rerank/AudioSpeech
	// /OCR methods all return hard "not supported" errors. A chat/rerank/etc.
	// request that names a Builtin provider must fall through to the standard
	// branch, which surfaces an accurate "provider not found" instead of
	// handing back an embedding-only driver.
	if modelType == entity.ModelTypeEmbedding {
		// The Builtin provider is a local service, not a tenant-enrolled
		// provider. Extract the model name by looking for the @Builtin
		// suffix explicitly so model names with embedded '@' characters
		// (e.g. `model@q8_0@Builtin` or bare `model@q8_0` without any
		// provider suffix) are handled correctly.
		var pureModelName string
		switch {
		case strings.HasSuffix(modelName, "@default@Builtin"):
			pureModelName = modelName[:len(modelName)-len("@default@Builtin")]
		case strings.HasSuffix(modelName, "@Builtin"):
			pureModelName = modelName[:len(modelName)-len("@Builtin")]
		default:
			pureModelName = ""
		}
		if pureModelName != "" {
			builtinDriver := modelModule.GetBuiltinEmbeddingModel(pureModelName)
			if builtinDriver == nil {
				return nil, "", nil, 0, fmt.Errorf("builtin embedding model %q not found", pureModelName)
			}
			apiKey := ""
			region := ""
			apiConfig := &modelModule.APIConfig{ApiKey: &apiKey, Region: &region}
			maxTokens := 0
			if mi, _ := dao.GetModelProviderManager().GetModelByName("Builtin", pureModelName); mi != nil {
				if mi.MaxTokens == nil {
					maxTokens = 0
				} else {
					maxTokens = *mi.MaxTokens
				}
			}
			return builtinDriver, pureModelName, apiConfig, maxTokens, nil
		}
	}

	pureModelName, instanceName, providerName, err := parseModelName(modelName)
	if err != nil {
		return nil, "", nil, 0, err
	}

	// Direct provider lookup
	provider, provErr := m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
	if provErr != nil {
		return nil, "", nil, 0, fmt.Errorf("provider %q lookup failed: %w", providerName, provErr)
	}
	if provider == nil {
		return nil, "", nil, 0, fmt.Errorf("provider %q not found for model %q", providerName, modelName)
	}

	// Direct instance lookup
	instance, instErr := m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, provider.ID, instanceName)
	if instErr != nil {
		return nil, "", nil, 0, fmt.Errorf("instance %q lookup failed: %w", instanceName, instErr)
	}
	if instance == nil {
		return nil, "", nil, 0, fmt.Errorf("instance %q not found for model %q", instanceName, modelName)
	}

	// Decode api_key and extra fields from the instance row
	apiKey := instance.APIKey
	var extra map[string]string
	_ = json.Unmarshal([]byte(instance.Extra), &extra)
	region := extra["region"]
	baseURL := extra["base_url"]

	// Direct model lookup
	modelObj, modelErr := m.modelDAO.GetByProviderIDAndInstanceIDAndModelTypeAndModelName(
		ctx, dao.DB, provider.ID, instance.ID, int(modelType), pureModelName,
	)
	switch {
	case modelErr == nil:
		// Happy path: tenant enrolled this model.
		// INACTIVE check
		if modelObj.Status == "inactive" {
			return nil, "", nil, 0, fmt.Errorf("model %q is disabled", modelName)
		}

		providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
		if providerInfo == nil {
			return nil, "", nil, 0, fmt.Errorf("provider %q driver not found", providerName)
		}
		driver, driverErr := newModelDriverForBaseURL(providerInfo.ModelDriver, providerName, region, baseURL)
		if driverErr != nil {
			return nil, "", nil, 0, driverErr
		}
		maxTokens := 0
		if mi, _ := dao.GetModelProviderManager().GetModelByName(providerName, pureModelName); mi != nil {
			if mi.MaxTokens == nil {
				maxTokens = 0
			} else {
				maxTokens = *mi.MaxTokens
			}
		}
		maxTokens, driverErr = maxTokensFromTenantModelExtra(modelObj, maxTokens)
		if driverErr != nil {
			return nil, "", nil, 0, driverErr
		}
		apiConfig := &modelModule.APIConfig{ApiKey: &apiKey, Region: &region, BaseURL: &baseURL}
		return driver, modelObj.ModelName, apiConfig, maxTokens, nil
	case errors.Is(modelErr, gorm.ErrRecordNotFound):
		// Tenant hasn't enrolled this model. Fall through to the factory catalog.
		common.Debug("GetModelConfigFromProviderInstance: tenant has no row for model, falling back to factory catalog",
			zap.String("tenantID", tenantID),
			zap.String("providerName", providerName),
			zap.String("instanceName", instanceName),
			zap.String("modelType", modelType.String()),
			zap.String("modelName", pureModelName))
	default:
		// Surface unexpected DAO errors (e.g. transient DB failure) instead of
		// silently resolving from the factory catalog — that would mask disabled
		// or tenant-specific configurations.
		return nil, "", nil, 0, fmt.Errorf("model %q lookup failed: %w", modelName, modelErr)
	}

	// Fallback: factory LLM catalog
	targetFactoryName := providerName
	if region == "intl" && strings.EqualFold(providerName, "siliconflow") {
		targetFactoryName = "siliconflow_intl"
	}
	// Use the case-insensitive FindProvider / findModel helpers. The Go's
	// conf/models/*.json files now use the same names as the Python's
	// conf/llm_factories.json (e.g. "SILICONFLOW"); but a strict
	// `==` here would still fail for any case mismatch even though the Python passes
	// (its FACTORY_LLM_INFOS and the parsed provider_name happen to agree on
	// casing). The rest of the Go codebase uses these helpers too.
	targetProvider := dao.GetModelProviderManager().FindProvider(targetFactoryName)
	if targetProvider == nil {
		return nil, "", nil, 0, fmt.Errorf("model provider config not found: %s", providerName)
	}
	var llmInfo *modelModule.Model
	for i := range targetProvider.Models {
		if strings.EqualFold(targetProvider.Models[i].Name, pureModelName) {
			llmInfo = targetProvider.Models[i]
			break
		}
	}
	if llmInfo == nil {
		return nil, "", nil, 0, fmt.Errorf("model config not found: %s", modelName)
	}
	driver, driverErr := newModelDriverForBaseURL(targetProvider.ModelDriver, providerName, region, baseURL)
	if driverErr != nil {
		return nil, "", nil, 0, driverErr
	}
	apiConfig := &modelModule.APIConfig{ApiKey: &apiKey, Region: &region, BaseURL: &baseURL}
	maxTokens := 0
	if llmInfo.MaxTokens != nil {
		maxTokens = *llmInfo.MaxTokens
	}
	return driver, llmInfo.Name, apiConfig, maxTokens, nil
}

// getModelConfig returns the model driver, model name, API config, and max tokens for a model
func (m *ModelProviderService) getModelConfig(ctx context.Context, tenantID, compositeModelName string) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) {
	modelName, instanceName, providerName, err := parseModelName(compositeModelName)
	if err != nil {
		return nil, "", nil, 0, err
	}

	// Check if provider exists (skip for Builtin provider)
	var providerID string
	if providerName != "Builtin" {
		var provider *entity.TenantModelProvider
		provider, err = m.modelProviderDAO.GetByTenantIDAndProviderName(ctx, dao.DB, tenantID, providerName)
		if err != nil {
			return nil, "", nil, 0, err
		}
		if provider == nil {
			return nil, "", nil, 0, fmt.Errorf("provider %s not found", providerName)
		}
		providerID = provider.ID
	} else {
		common.Debug("getModelConfig skipping provider lookup for Builtin")
	}

	// Get instance (skip for Builtin provider since it doesn't use tenant_model_instance)
	var instance *entity.TenantModelInstance
	if providerName != "Builtin" {
		instance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(ctx, dao.DB, providerID, instanceName)
		if err != nil {
			return nil, "", nil, 0, err
		}
		if instance == nil {
			return nil, "", nil, 0, fmt.Errorf("instance %s not found for provider %s", instanceName, providerName)
		}
		common.Debug("getModelConfig instance found", zap.String("instanceName", instanceName))
	}

	// TODO: if provider name is Builtin, HOW TO?

	var extra map[string]string
	var region string
	var baseURL string
	if instance != nil {
		err = json.Unmarshal([]byte(instance.Extra), &extra)
		if err != nil {
			return nil, "", nil, 0, err
		}
		region = extra["region"]
		baseURL = extra["base_url"]
	}

	providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
	if providerInfo == nil {
		if providerName != "Builtin" {
			return nil, "", nil, 0, fmt.Errorf("model provider config not found: %s", providerName)
		}
	}

	// Get model info to extract max_tokens
	modelInfo, err := dao.GetModelProviderManager().GetModelByName(providerName, modelName)
	maxTokens := 0
	if err == nil && modelInfo != nil {
		if modelInfo.MaxTokens == nil {
			maxTokens = 0
		} else {
			maxTokens = *modelInfo.MaxTokens
		}
	}

	// For Builtin provider, use empty APIKey and skip tenant_model lookup
	var apiKey string
	if providerName == "Builtin" {
		apiKey = ""
		// For Builtin, we need to get the ModelDriver from somewhere
		// Since Builtin models are handled locally, we return a special driver
		builtinDriver := modelModule.GetBuiltinEmbeddingModel(modelName)
		if builtinDriver == nil {
			return nil, "", nil, 0, fmt.Errorf("builtin embedding model %s not found", modelName)
		}
		apiConfig := &modelModule.APIConfig{ApiKey: &apiKey, Region: &region}
		return builtinDriver, modelName, apiConfig, maxTokens, nil
	}

	var modelRecord *entity.TenantModel
	modelRecord, err = m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(ctx, dao.DB, providerID, instance.ID, modelName)
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, "", nil, 0, fmt.Errorf("tenant model %q lookup failed: %w", modelName, err)
		}
		_, err = dao.GetModelProviderManager().GetModelByName(providerName, modelName)
		if err != nil {
			return nil, "", nil, 0, fmt.Errorf("provider %s model %s not found", providerName, modelName)
		}
	}
	maxTokens, err = maxTokensFromTenantModelExtra(modelRecord, maxTokens)
	if err != nil {
		return nil, "", nil, 0, err
	}
	apiKey = instance.APIKey

	driver, err := newModelDriverForBaseURL(providerInfo.ModelDriver, providerName, region, baseURL)
	if err != nil {
		return nil, "", nil, 0, err
	}

	apiConfig := &modelModule.APIConfig{ApiKey: &apiKey, Region: &region, BaseURL: &baseURL}
	return driver, modelName, apiConfig, maxTokens, nil
}

// ListAllModels list all models
func (m *ModelProviderService) ListAllModels(pageIndex, pageSize int) ([]map[string]interface{}, error) {
	models, err := dao.GetModelProviderManager().ListAllModels()
	if err != nil {
		return nil, err
	}
	if pageSize > 0 && pageIndex >= 0 && pageIndex*pageSize < len(models) {
		return models[pageIndex*pageSize : (pageIndex+1)*pageSize], nil
	}
	return models, nil
}

func (m *ModelProviderService) ShowModel(modelName string) (*modelModule.Model, error) {
	return dao.GetModelProviderManager().GetModelByNameOrAlias(modelName), nil
}

// isImage2TextLLM returns true when the named LLM is registered as an
// image2text model for the tenant.
// Returns false on lookup error or empty LLM ID so callers fall back to
// chat — matches Python's branch order where only an EXPLICIT image2text
// registration switches the model type away from chat.
func (m *ModelProviderService) isImage2TextLLM(ctx context.Context, tenantID, llmID string) bool {
	if m == nil || llmID == "" {
		return false
	}
	modelTypes, err := m.ResolveModelType(ctx, tenantID, llmID)
	if err != nil {
		return false
	}
	for _, mt := range modelTypes {
		if mt == entity.ModelTypeImage2Text {
			return true
		}
	}
	return false
}

// GetChatModelConfig resolves the model configuration for a chat dialog.
// If llmID is empty, falls back to the tenant's default chat model.
// When the named LLM is registered as an image2text model, returns the
// IMAGE2TEXT driver/config instead of CHAT.
func (m *ModelProviderService) GetChatModelConfig(ctx context.Context, tenantID string, llmID string) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) {
	if llmID == "" {
		return m.GetTenantDefaultModelByType(ctx, tenantID, entity.ModelTypeChat)
	}
	modelType := entity.ModelTypeChat
	if m.isImage2TextLLM(ctx, tenantID, llmID) {
		modelType = entity.ModelTypeImage2Text
	}
	return m.ResolveModelConfig(ctx, tenantID, modelType, llmID)
}
