Files
2026-10-11 08:53:26 +08:00

1652 lines
49 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"ragflow/internal/common"
"ragflow/internal/dao"
"ragflow/internal/entity/models"
"ragflow/internal/service"
"regexp"
"sort"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// unsupportedProviders lists catalog providers that the server cannot serve
// yet. They are hidden from the "available" provider listing so the UI never
// offers them for configuration.
var unsupportedProviders = map[string]struct{}{
"MinerU.Net": {},
}
// filterUnsupportedProviders drops providers that the server cannot serve yet.
func filterUnsupportedProviders(providers []map[string]interface{}) []map[string]interface{} {
filtered := make([]map[string]interface{}, 0, len(providers))
for _, provider := range providers {
if name, ok := provider["name"].(string); ok {
if _, unsupported := unsupportedProviders[name]; unsupported {
continue
}
}
filtered = append(filtered, provider)
}
return filtered
}
// ProviderHandler provider handler
type ProviderHandler struct {
userService *service.UserService
modelProviderService *service.ModelProviderService
modelCallService *service.ModelCallService
userTenantDAO *dao.UserTenantDAO
}
// NewProviderHandler create provider handler
func NewProviderHandler(userService *service.UserService, modelProviderService *service.ModelProviderService, modelCallService *service.ModelCallService) *ProviderHandler {
return &ProviderHandler{
userService: userService,
modelProviderService: modelProviderService,
modelCallService: modelCallService,
userTenantDAO: dao.NewUserTenantDAO(),
}
}
func (h *ProviderHandler) ListProviders(c *gin.Context) {
keywords := ""
if queryKeywords := c.Query("available"); queryKeywords != "" {
keywords = queryKeywords
}
// convert keywords to small case
keywords = strings.ToLower(keywords)
if keywords == "true" {
// list pool providers
providers, err := dao.GetModelProviderManager().ListProviders()
if err != nil {
common.ErrorWithCode(c, common.CodeNotFound, err.Error())
return
}
providers = filterUnsupportedProviders(providers)
for _, provider := range providers {
delete(provider, "url_suffix")
delete(provider, "tags")
}
common.SuccessWithData(c, providers, "success")
return
}
userID := c.GetString("user_id")
ctx := c.Request.Context()
// list tenant providers
providers, errorCode, err := h.modelProviderService.ListProvidersOfTenant(ctx, userID)
if err != nil {
common.ResponseWithCodeData(c, errorCode, nil, err.Error())
return
}
common.SuccessWithData(c, providers, "success")
return
}
type AddProviderRequest struct {
ProviderName string `json:"provider_name" binding:"required"`
}
func (h *ProviderHandler) AddProvider(c *gin.Context) {
var req AddProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ResponseWithCodeData(c, common.CodeBadRequest, false, err.Error())
return
}
userID := c.GetString("user_id")
ctx := c.Request.Context()
errorCode, err := h.modelProviderService.AddModelProvider(ctx, req.ProviderName, userID)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithMessage(c, "success")
}
func (h *ProviderHandler) DeleteProvider(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
userID := c.GetString("user_id")
ctx := c.Request.Context()
errorCode, err := h.modelProviderService.DeleteModelProvider(ctx, userID, providerName)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithMessage(c, "success")
}
func (h *ProviderHandler) ShowProvider(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
provider, err := dao.GetModelProviderManager().GetProviderByName(providerName)
if err != nil {
common.ErrorWithCode(c, common.CodeNotFound, err.Error())
return
}
common.SuccessWithData(c, provider, "success")
}
func (h *ProviderHandler) ListModels(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
// 1. Get static models from config (maybe nil when models list is empty)
staticModels, _ := dao.GetModelProviderManager().ListModels(providerName)
if staticModels == nil {
staticModels = []map[string]interface{}{}
}
// 2. Attempt live API fetch when the provider has enough connection data.
apiKey := c.Query("api_key")
baseURL := c.Query("base_url")
var remoteModels []map[string]interface{}
remoteFetched := false
bedrockProvider := strings.EqualFold(providerName, "Bedrock")
bedrockAPIKeyAuth := false
if bedrockProvider && apiKey != "" {
var key struct {
AuthMode string `json:"auth_mode"`
}
if err := json.Unmarshal([]byte(apiKey), &key); err == nil {
bedrockAPIKeyAuth = key.AuthMode == "bedrock_api_key"
}
}
canFetchRemote := baseURL != ""
if bedrockProvider {
// Bedrock's existing SigV4 modes keep using the static catalog. Only
// API-key auth needs a live catalog scoped to the supplied credential.
canFetchRemote = bedrockAPIKeyAuth
}
if canFetchRemote {
providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
if providerInfo != nil && providerInfo.ModelDriver != nil {
region := "default"
baseURLByRegion := map[string]string{region: baseURL}
driver := providerInfo.ModelDriver.NewInstance(baseURLByRegion)
if driver != nil {
apiConfig := &models.APIConfig{
ApiKey: &apiKey,
Region: &region,
}
if liveModels, err := driver.ListModels(c.Request.Context(), apiConfig); err == nil {
remoteFetched = true
for _, m := range liveModels {
legacyMaxTokens := 0
if m.ContextLength != nil {
legacyMaxTokens = *m.ContextLength
}
remoteModels = append(remoteModels, map[string]interface{}{
"name": m.Name,
"model_types": m.ModelTypes,
"context_length": m.ContextLength,
"max_output": m.MaxOutput,
"max_tokens": legacyMaxTokens,
})
}
} else if bedrockAPIKeyAuth {
common.ErrorWithCode(c, common.CodeServerError, err.Error())
return
}
}
}
}
if remoteFetched && bedrockAPIKeyAuth {
if len(remoteModels) == 0 {
common.ErrorWithCode(c, common.CodeDataError, "No Bedrock models were discovered")
return
}
remoteNames := make(map[string]struct{}, len(remoteModels))
for _, model := range remoteModels {
if name, ok := model["name"].(string); ok {
remoteNames[providerModelKey(name)] = struct{}{}
}
}
filtered := staticModels[:0]
for _, model := range staticModels {
if name, ok := model["name"].(string); ok {
if _, exists := remoteNames[providerModelKey(name)]; exists {
filtered = append(filtered, model)
}
}
}
staticModels = filtered
}
// 3. Both empty — return empty success
if len(staticModels) == 0 && len(remoteModels) == 0 {
common.SuccessWithData(c, []map[string]interface{}{}, "success")
return
}
// 4. Merge: static as base, remote overrides on name conflicts.
result := mergeProviderModels(staticModels, remoteModels)
// 5. Fill missing model types using only the merged list.
fillProviderModelMapTypes(result)
common.SuccessWithData(c, result, "success")
}
// mergeProviderModels collapses the bundled catalog listing and a live upstream
// listing into a single entry per model, sorted by name.
//
// Names are matched case-insensitively and trimmed — an upstream listing can
// spell a model differently from the catalog (e.g. "GPT-4o" vs "gpt-4o"), and
// both must collapse instead of showing up as two rows. The first spelling seen
// wins, so a model already saved under the catalog's name keeps it.
//
// Remote entries override the catalog entry, with two exceptions: a remote
// entry that carries no model types inherits the catalog's types, and the
// legacy `max_tokens` alias remains the input/context window. `max_output` is
// kept as a separate provider generation ceiling.
func mergeProviderModels(staticModels, remoteModels []map[string]interface{}) []map[string]interface{} {
merged := make(map[string]map[string]interface{}, len(staticModels)+len(remoteModels))
for _, m := range staticModels {
if maxTokens, ok := m["max_tokens"]; !ok || maxTokens == nil {
if contextLength, ok := m["context_length"]; ok && contextLength != nil {
m["max_tokens"] = contextLength
}
}
name, ok := m["name"].(string)
if !ok {
continue
}
key := providerModelKey(name)
if key == "" {
continue
}
m["name"] = strings.TrimSpace(name)
merged[key] = m
}
for _, m := range remoteModels {
name, ok := m["name"].(string)
if !ok {
continue
}
key := providerModelKey(name)
if key == "" {
continue
}
name = strings.TrimSpace(name)
if existing, exists := merged[key]; exists {
if len(providerModelMapTypes(m)) == 0 && len(providerModelMapTypes(existing)) > 0 {
m["model_types"] = existing["model_types"]
}
if maxTokens, ok := existing["max_tokens"]; ok && maxTokens != nil {
m["max_tokens"] = maxTokens
}
if existingName, ok := existing["name"].(string); ok {
name = existingName
}
}
m["name"] = name
merged[key] = m
}
result := make([]map[string]interface{}, 0, len(merged))
for _, m := range merged {
result = append(result, m)
}
sort.Slice(result, func(i, j int) bool {
ni, _ := result[i]["name"].(string)
nj, _ := result[j]["name"].(string)
return providerModelKey(ni) < providerModelKey(nj)
})
return result
}
func fillProviderModelMapTypes(result []map[string]interface{}) {
list := make([]models.ListModelResponse, 0, len(result))
indexes := make([]int, 0, len(result))
for i, model := range result {
name, _ := model["name"].(string)
name = strings.TrimSpace(name)
if name == "" {
continue
}
list = append(list, models.ListModelResponse{
Name: name,
ModelTypes: providerModelMapTypes(model),
})
indexes = append(indexes, i)
}
list = models.FillMissingModelTypes(list)
for i, model := range list {
result[indexes[i]]["model_types"] = model.ModelTypes
}
}
// providerModelKey is the identity of a model name. Upstream listings and the
// bundled catalog can spell the same model with different case or padding, so
// everything that merges or matches models compares this normalized form.
func providerModelKey(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}
func providerModelMapTypes(model map[string]interface{}) []string {
switch modelTypes := model["model_types"].(type) {
case []string:
return modelTypes
case []interface{}:
types := make([]string, 0, len(modelTypes))
for _, modelType := range modelTypes {
if s, ok := modelType.(string); ok {
types = append(types, s)
}
}
return types
default:
return nil
}
}
func (h *ProviderHandler) ShowModel(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
modelName := c.Param("model_name")
if modelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model name is required")
return
}
model, err := dao.GetModelProviderManager().GetModelByName(providerName, modelName)
if err != nil {
common.ErrorWithCode(c, common.CodeNotFound, err.Error())
return
}
common.SuccessWithData(c, model, "success")
}
type CreateProviderInstanceRequest struct {
InstanceName string `json:"instance_name" binding:"required"`
APIKey string `json:"api_key"`
BaseURL string `json:"base_url"`
Region string `json:"region"`
ModelInfo []service.CreateInstanceModelInfo `json:"model_info"`
}
// instanceNamePattern restricts an instance name to digits, underscores,
// hyphens, and ASCII letters in both cases.
var instanceNamePattern = regexp.MustCompile(`^[0-9A-Za-z_-]+$`)
// validateInstanceName rejects an empty instance name or one carrying any
// character outside digits, underscores, hyphens, and ASCII letters.
func validateInstanceName(instanceName string) error {
if instanceName == "" {
return errors.New("instance name is required")
}
if !instanceNamePattern.MatchString(instanceName) {
return errors.New("instance name may only contain digits, underscores, hyphens, and letters")
}
return nil
}
func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
ctx := c.Request.Context()
var req CreateProviderInstanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
if err := validateInstanceName(req.InstanceName); err != nil {
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
userID := c.GetString("user_id")
// If the request body only contains "instance_name", create a name-only
// instance without API key validation or model creation.
// Mirrors Python's provider_api.py:349 — set(data.keys()) == {"instance_name"}.
if req.APIKey == "" && req.BaseURL == "" && req.Region == "" && len(req.ModelInfo) == 0 {
code, err := h.modelProviderService.CreateNameOnlyProviderInstance(ctx, providerName, req.InstanceName, userID)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
}
common.SuccessWithMessage(c, "success")
return
}
code, err := h.modelProviderService.CreateProviderInstance(ctx, providerName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, userID, req.ModelInfo)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
}
common.SuccessWithMessage(c, "success")
}
func (h *ProviderHandler) ListProviderInstances(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
userID := c.GetString("user_id")
ctx := c.Request.Context()
instances, errorCode, err := h.modelProviderService.ListProviderInstances(ctx, providerName, userID)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, instances, "success")
}
func (h *ProviderHandler) ShowProviderInstance(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceIDOrName := c.Param("instance_id_or_name")
if instanceIDOrName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
userID := c.GetString("user_id")
ctx := c.Request.Context()
instance, errorCode, err := h.modelProviderService.ShowProviderInstance(ctx, providerName, instanceIDOrName, userID)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, instance, "success")
}
func (h *ProviderHandler) ShowInstanceBalance(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
userID := c.GetString("user_id")
ctx := c.Request.Context()
// Get tenant ID from user
balance, errorCode, err := h.modelProviderService.ShowInstanceBalance(ctx, providerName, instanceName, userID)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, balance, "success")
}
func (h *ProviderHandler) CheckConnection(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
ctx := c.Request.Context()
var req service.CheckConnectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, err.Error())
return
}
userID := c.GetString("user_id")
errCode, err := h.modelProviderService.CheckConnection(ctx, providerName, req.APIKey, req.Region, req.BaseURL, req.InstanceID, userID, req.ModelInfo)
if err != nil {
common.ErrorWithCode(c, errCode, err.Error())
return
}
common.SuccessWithMessage(c, "success")
}
func (h *ProviderHandler) CheckInstanceConnection(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
userID := c.GetString("user_id")
instanceInfo, code, err := h.modelProviderService.ShowProviderInstance(ctx, providerName, instanceName, userID)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
}
apikey, _ := instanceInfo["api_key"].(string)
region, _ := instanceInfo["region"].(string)
baseURL, _ := instanceInfo["base_url"].(string)
instanceID, _ := instanceInfo["id"].(string)
errorCode, err := h.modelProviderService.CheckConnection(ctx, providerName, apikey, region, baseURL, instanceID, userID, nil)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithMessage(c, "success")
}
func (h *ProviderHandler) ListTasks(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
userID := c.GetString("user_id")
// Get tenant ID from user
listTaskResponse, errorCode, err := h.modelProviderService.ListTasks(ctx, providerName, instanceName, userID)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, listTaskResponse, "success")
}
func (h *ProviderHandler) ShowTask(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
taskID := c.Param("task_id")
if taskID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Task ID is required")
return
}
userID := c.GetString("user_id")
// Get tenant ID from user
taskResponse, errorCode, err := h.modelProviderService.ShowTask(ctx, providerName, instanceName, taskID, userID)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, taskResponse, "success")
}
type AlterProviderInstanceRequest struct {
InstanceName string `json:"instance_name"`
APIKey string `json:"api_key"`
BaseURL string `json:"base_url"`
Region string `json:"region"`
ModelInfo *[]service.CreateInstanceModelInfo `json:"model_info" binding:"required"`
}
func (h *ProviderHandler) AlterProviderInstance(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
var req AlterProviderInstanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, err.Error())
return
}
userID := c.GetString("user_id")
if userID == "" {
common.ErrorWithCode(c, common.CodeUnauthorized, "Unauthorized")
return
}
code, err := h.modelProviderService.AlterProviderInstance(ctx, userID, providerName, instanceName, req.InstanceName, req.APIKey, req.BaseURL, req.Region, *req.ModelInfo)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
}
common.SuccessWithMessage(c, "success")
}
type DropProviderInstanceRequest struct {
Instances []string `json:"instances" binding:"required,min=1,dive,required"`
}
func (h *ProviderHandler) DropProviderInstance(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
var req DropProviderInstanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
userID := c.GetString("user_id")
ctx := c.Request.Context()
code, err := h.modelProviderService.DropProviderInstances(ctx, providerName, userID, req.Instances)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
}
common.SuccessWithMessage(c, "success")
}
func (h *ProviderHandler) ListInstanceModels(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
keywords := ""
if queryKeywords := c.Query("supported"); queryKeywords != "" {
keywords = queryKeywords
}
// convert keywords to small case
keywords = strings.ToLower(keywords)
if keywords == "true" {
// list supported models
modelList, err := h.modelProviderService.ListSupportedModels(ctx, providerName, instanceName, c.GetString("user_id"))
if err != nil {
common.ErrorWithCode(c, common.CodeServerError, err.Error())
return
}
common.SuccessWithData(c, modelList, "success")
return
}
modelInstances, err := h.modelProviderService.ListInstanceModels(ctx, providerName, instanceName, c.GetString("user_id"))
if err != nil {
common.ErrorWithCode(c, common.CodeNotFound, err.Error())
return
}
common.SuccessWithData(c, modelInstances, "success")
}
type AlterModelRequest struct {
ModelID string `json:"model_id"`
Status string `json:"status"`
MaxTokens int `json:"max_tokens"`
ModelType interface{} `json:"model_type"`
Extra interface{} `json:"extra"`
}
func (h *ProviderHandler) AlterModel(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
var req AlterModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
userID := c.GetString("user_id")
modelName := strings.TrimPrefix(c.Param("model_name"), "/")
modelName = strings.TrimSpace(modelName)
modelID := strings.TrimSpace(req.ModelID)
if modelName == "" && modelID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "model_name or model_id is required")
return
}
status := strings.TrimSpace(req.Status)
if status != "" && status != "active" && status != "inactive" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "status must be 'active' or 'inactive'")
return
}
updateDict := make(map[string]interface{})
if status != "" {
updateDict["status"] = status
}
if req.MaxTokens > 0 {
updateDict["max_tokens"] = req.MaxTokens
}
if req.ModelType != nil {
updateDict["model_type"] = req.ModelType
}
if req.Extra != nil {
updateDict["extra"] = req.Extra
}
if len(updateDict) == 0 {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "at least one update field is required besides model_name or model_id")
return
}
ctx := c.Request.Context()
code, err := h.modelProviderService.AlterModel(ctx, providerName, instanceName, modelName, userID, modelID, updateDict)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
}
common.SuccessWithMessage(c, "success")
}
func prepareProviderInstance(providerName, instanceName, reqProviderName, reqInstanceName string) error {
if providerName == "" {
return errors.New("provider name is required")
}
if instanceName == "" {
return errors.New("instance name is required")
}
if reqProviderName != "" && !strings.EqualFold(reqProviderName, providerName) {
return errors.New("provider name does not match path")
}
if reqInstanceName != "" && !strings.EqualFold(reqInstanceName, instanceName) {
return errors.New("instance name does not match path")
}
return nil
}
func (h *ProviderHandler) AddModel(c *gin.Context) {
var req service.AddModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
println("JSON bind error: %v (type: %T)", err, err)
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, err.Error())
return
}
req.ProviderName = c.Param("provider_id_or_name")
req.InstanceName = c.Param("instance_id_or_name")
if req.ProviderName == "" || req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "provider_name and instance_name are required")
return
}
if req.ModelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "model_name is required")
return
}
userID := c.GetString("user_id")
ctx := c.Request.Context()
code, err := h.modelProviderService.AddModel(ctx, &req, userID)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
}
common.ErrorWithCode(c, code, "success")
}
type DropInstanceModelRequest struct {
ModelNames []string `json:"model_name"`
}
func (h *ProviderHandler) DropInstanceModels(c *gin.Context) {
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
var req DropInstanceModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
if len(req.ModelNames) == 0 {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "model_name is required")
return
}
userID := c.GetString("user_id")
ctx := c.Request.Context()
code, err := h.modelProviderService.DropInstanceModels(ctx, providerName, instanceName, userID, req.ModelNames)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
}
common.SuccessWithMessage(c, "success")
}
type ChatToModelRequest struct {
Question string `json:"question,omitempty"`
Query string `json:"query,omitempty"`
ProviderName *string `json:"provider_name"`
InstanceName *string `json:"instance_name"`
ModelName *string `json:"model_name"`
ModelID *string `json:"model_id"`
Messages []map[string]interface{} `json:"messages"`
Stream bool `json:"stream"`
Thinking bool `json:"thinking"`
Effort *string `json:"effort"`
Verbosity *string `json:"verbosity"`
}
func (h *ProviderHandler) ChatToModel(c *gin.Context) {
ctx := c.Request.Context()
var req ChatToModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
println("JSON bind error: %v (type: %T)", err, err)
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
question, err := service.ResolveCompletionQuestion(req.Question, req.Query, req.Messages)
if err != nil {
common.ErrorWithCode(c, common.CodeArgumentError, err.Error())
return
}
if req.Question != "" || req.Query != "" {
req.Messages = []map[string]interface{}{{"role": "user", "content": question}}
}
if len(req.Messages) > 0 {
req.Messages = req.Messages[len(req.Messages)-1:]
}
if req.ModelID == nil {
if req.ProviderName == nil || *req.ProviderName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
if req.InstanceName == nil || *req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
if req.ModelName == nil || *req.ModelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model name is required")
return
}
} else {
if *req.ModelID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model ID is empty")
return
}
}
modelRef := ""
if req.ModelID != nil {
modelRef = *req.ModelID
} else {
modelRef = fmt.Sprintf("%s@%s@%s", *req.ModelName, *req.InstanceName, *req.ProviderName)
}
if !req.Thinking {
req.Effort = nil
req.Verbosity = nil
}
chatConfig := models.ChatConfig{
Thinking: &req.Thinking,
Stream: &req.Stream,
Vision: nil,
Stop: &[]string{},
DoSample: nil,
MaxTokens: nil,
Temperature: nil,
TopP: nil,
Effort: req.Effort,
Verbosity: req.Verbosity,
}
userID := c.GetString("user_id")
email := c.GetString("email")
modelUsage := common.ModelUsage{
UserID: userID,
UserEmail: email,
Type: "chat",
StartAt: time.Now(),
}
if req.ProviderName != nil {
modelUsage.ProviderName = *req.ProviderName
}
if req.ModelName != nil {
modelUsage.ModelName = *req.ModelName
}
// Check if it's a stream request
if req.Stream {
// Set SSE headers
clearResponseWriteDeadline(c)
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Flush()
// Create sender function that writes directly to response
sender := func(content, reasoningContent *string) error {
// Check for [DONE] marker (OpenAI compatible)
if content != nil {
if *content == "[DONE]" {
c.SSEvent("done", "[DONE]")
return nil
}
message := fmt.Sprintf("[MESSAGE]%s", *content)
c.SSEvent("message", message)
c.Writer.Flush()
}
if reasoningContent != nil {
message := fmt.Sprintf("[REASONING]%s", *reasoningContent)
c.SSEvent("message", message)
c.Writer.Flush()
}
//logger.Info(data)
return nil
}
// Convert []map[string]interface{} to []models.Message
messages := make([]models.Message, len(req.Messages))
for i, msg := range req.Messages {
role, _ := msg["role"].(string)
content := msg["content"]
messages[i] = models.Message{Role: role, Content: content}
}
// Stream response using sender function (the best performance, no channel)
errorCode, err := h.modelCallService.ChatToModelStreamWithSender(
ctx,
modelRef,
userID,
messages,
&chatConfig,
&modelUsage,
sender,
)
if errorCode != common.CodeSuccess {
c.SSEvent("error", err.Error())
}
return
}
// Non-stream response
var response *models.ChatResponse
var errorCode common.ErrorCode
// Convert []map[string]interface{} to []models.Message
messages := make([]models.Message, len(req.Messages))
for i, msg := range req.Messages {
role, _ := msg["role"].(string)
content := msg["content"]
messages[i] = models.Message{Role: role, Content: content}
}
response, errorCode, err = h.modelCallService.ChatToModelWithMessages(
ctx,
modelRef,
userID,
messages,
&chatConfig,
&modelUsage,
)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"reasoning_content": response.ReasonContent,
"answer": response.Answer,
"usage": response.Usage,
})
}
type EmbedTextRequest struct {
ProviderName *string `json:"provider_name"`
InstanceName *string `json:"instance_name"`
ModelName *string `json:"model_name"`
ModelID *string `json:"model_id"`
Texts []string `json:"texts"`
Dimension int `json:"dimension"`
}
func (h *ProviderHandler) EmbedText(c *gin.Context) {
ctx := c.Request.Context()
var req EmbedTextRequest
if err := c.ShouldBindJSON(&req); err != nil {
println("JSON bind error: %v (type: %T)", err, err)
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
if req.ModelID == nil {
if req.ProviderName == nil || *req.ProviderName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
if req.InstanceName == nil || *req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
if req.ModelName == nil || *req.ModelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model name is required")
return
}
} else {
if *req.ModelID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model ID is empty")
return
}
}
userID := c.GetString("user_id")
modelRef := ""
if req.ModelID != nil {
modelRef = *req.ModelID
} else {
modelRef = fmt.Sprintf("%s@%s@%s", *req.ModelName, *req.InstanceName, *req.ProviderName)
}
embeddingConfig := models.EmbeddingConfig{
Dimension: req.Dimension,
}
// Non-stream response
var response []models.EmbeddingData
var errorCode common.ErrorCode
var err error
response, errorCode, err = h.modelCallService.EmbedText(ctx, modelRef, userID, req.Texts, &embeddingConfig)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, response, "success")
}
type RerankDocumentRequest struct {
ProviderName *string `json:"provider_name"`
InstanceName *string `json:"instance_name"`
ModelName *string `json:"model_name"`
ModelID *string `json:"model_id"`
Query string `json:"query"`
Documents []string `json:"documents"`
TopN int `json:"top_n"`
}
func (h *ProviderHandler) RerankDocument(c *gin.Context) {
ctx := c.Request.Context()
var req RerankDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
println("JSON bind error: %v (type: %T)", err, err)
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
if req.ModelID == nil {
if req.ProviderName == nil || *req.ProviderName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
if req.InstanceName == nil || *req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
if req.ModelName == nil || *req.ModelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model name is required")
return
}
} else {
if *req.ModelID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model ID is empty")
return
}
}
userID := c.GetString("user_id")
modelRef := ""
if req.ModelID != nil {
modelRef = *req.ModelID
} else {
modelRef = fmt.Sprintf("%s@%s@%s", *req.ModelName, *req.InstanceName, *req.ProviderName)
}
rerankConfig := models.RerankConfig{
TopN: req.TopN,
}
// Non-stream response
var response *models.RerankResponse
var errorCode common.ErrorCode
var err error
response, errorCode, err = h.modelCallService.RerankDocument(ctx, modelRef, userID, req.Query, req.Documents, &rerankConfig)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, response.Data, "success")
}
type TranscribeAudioRequest struct {
ProviderName *string `json:"provider_name"`
InstanceName *string `json:"instance_name"`
ModelName *string `json:"model_name"`
ModelID *string `json:"model_id"`
File *string `json:"file"`
Language []string `json:"language"`
Prompt int `json:"prompt"`
Stream bool `json:"stream"`
ASRConfig *models.ASRConfig `json:"asr_config"`
}
func (h *ProviderHandler) TranscribeAudio(c *gin.Context) {
ctx := c.Request.Context()
var req TranscribeAudioRequest
if err := c.ShouldBindJSON(&req); err != nil {
println("JSON bind error: %v (type: %T)", err, err)
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
if req.ModelID == nil {
if req.ProviderName == nil || *req.ProviderName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
if req.InstanceName == nil || *req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
if req.ModelName == nil || *req.ModelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model name is required")
return
}
} else {
if *req.ModelID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model ID is empty")
return
}
}
userID := c.GetString("user_id")
modelRef := ""
if req.ModelID != nil {
modelRef = *req.ModelID
} else {
modelRef = fmt.Sprintf("%s@%s@%s", *req.ModelName, *req.InstanceName, *req.ProviderName)
}
asrConfig := models.ASRConfig{}
if req.ASRConfig != nil {
asrConfig = *req.ASRConfig
}
// Check if it's a stream request
if req.Stream {
// Set SSE headers
clearResponseWriteDeadline(c)
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Flush()
// Create sender function that writes directly to response
sender := func(content, reasoningContent *string) error {
// Check for [DONE] marker (OpenAI compatible)
if content != nil {
if *content == "[DONE]" {
c.SSEvent("done", "[DONE]")
return nil
}
message := fmt.Sprintf("[MESSAGE]%s", *content)
c.SSEvent("message", message)
c.Writer.Flush()
}
if reasoningContent != nil {
message := fmt.Sprintf("[REASONING]%s", *reasoningContent)
c.SSEvent("message", message)
c.Writer.Flush()
}
//logger.Info(data)
return nil
}
// Stream response using sender function ( the best performance, no channel)
errorCode, err := h.modelCallService.TranscribeAudioStream(ctx, modelRef, userID, req.File, &asrConfig, sender)
if errorCode != common.CodeSuccess {
c.SSEvent("error", err.Error())
}
return
}
// Non-stream response
var response *models.ASRResponse
var errorCode common.ErrorCode
var err error
response, errorCode, err = h.modelCallService.TranscribeAudio(ctx, modelRef, userID, req.File, &asrConfig)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, response, "success")
}
type AudioSpeechRequest struct {
ProviderName *string `json:"provider_name"`
InstanceName *string `json:"instance_name"`
ModelName *string `json:"model_name"`
ModelID *string `json:"model_id"`
Text *string `json:"text"`
Stream bool `json:"stream"`
TTSConfig *models.TTSConfig `json:"tts_config"`
}
func (h *ProviderHandler) AudioSpeech(c *gin.Context) {
ctx := c.Request.Context()
var req AudioSpeechRequest
if err := c.ShouldBindJSON(&req); err != nil {
println("JSON bind error: %v (type: %T)", err, err)
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
if req.ModelID == nil {
if req.ProviderName == nil || *req.ProviderName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
if req.InstanceName == nil || *req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
if req.ModelName == nil || *req.ModelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model name is required")
return
}
} else {
if *req.ModelID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model ID is empty")
return
}
}
userID := c.GetString("user_id")
modelRef := ""
if req.ModelID != nil {
modelRef = *req.ModelID
} else {
modelRef = fmt.Sprintf("%s@%s@%s", *req.ModelName, *req.InstanceName, *req.ProviderName)
}
ttsConfig := models.TTSConfig{}
if req.TTSConfig != nil {
ttsConfig = *req.TTSConfig
}
// Check if it's a stream request
if req.Stream {
// Set SSE headers
clearResponseWriteDeadline(c)
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Flush()
// Create sender function that writes directly to response
sender := func(content, reasoningContent *string) error {
// Check for [DONE] marker (OpenAI compatible)
if content != nil {
if *content == "[DONE]" {
c.SSEvent("done", "[DONE]")
return nil
}
message := fmt.Sprintf("[MESSAGE]%s", *content)
c.SSEvent("message", message)
c.Writer.Flush()
}
if reasoningContent != nil {
message := fmt.Sprintf("[REASONING]%s", *reasoningContent)
c.SSEvent("message", message)
c.Writer.Flush()
}
//logger.Info(data)
return nil
}
// Stream response using sender function ( the best performance, no channel)
errorCode, err := h.modelCallService.AudioSpeechStream(ctx, modelRef, userID, req.Text, &ttsConfig, sender)
if errorCode != common.CodeSuccess {
c.SSEvent("error", err.Error())
}
return
}
// Non-stream response
var response *models.TTSResponse
var errorCode common.ErrorCode
var err error
response, errorCode, err = h.modelCallService.AudioSpeech(ctx, modelRef, userID, req.Text, &ttsConfig)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, response, "success")
}
type OCRFileRequest struct {
ProviderName *string `json:"provider_name"`
InstanceName *string `json:"instance_name"`
ModelName *string `json:"model_name"`
ModelID *string `json:"model_id"`
Content []byte `json:"content"`
URL *string `json:"url"`
}
func (h *ProviderHandler) OCRFile(c *gin.Context) {
ctx := c.Request.Context()
var req OCRFileRequest
if err := c.ShouldBindJSON(&req); err != nil {
println("JSON bind error: %v (type: %T)", err, err)
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
if req.ModelID == nil {
if req.ProviderName == nil || *req.ProviderName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
if req.InstanceName == nil || *req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
if req.ModelName == nil || *req.ModelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model name is required")
return
}
} else {
if *req.ModelID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model ID is empty")
return
}
}
userID := c.GetString("user_id")
modelRef := ""
if req.ModelID != nil {
modelRef = *req.ModelID
} else {
modelRef = fmt.Sprintf("%s@%s@%s", *req.ModelName, *req.InstanceName, *req.ProviderName)
}
OCRConfig := models.OCRConfig{}
// Non-stream response
var response *models.OCRFileResponse
var errorCode common.ErrorCode
var err error
response, errorCode, err = h.modelCallService.OCRFile(ctx, modelRef, userID, req.Content, req.URL, &OCRConfig)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, response, "success")
}
type ParseFileRequest struct {
ProviderName *string `json:"provider_name"`
InstanceName *string `json:"instance_name"`
ModelName *string `json:"model_name"`
ModelID *string `json:"model_id"`
Content []byte `json:"content"`
URL *string `json:"url"`
// Optional MinerU async /file/parse overrides (setup → provider api_key JSON → env).
Backend *string `json:"backend"`
ServerURL *string `json:"server_url"`
}
func (h *ProviderHandler) ParseFile(c *gin.Context) {
ctx := c.Request.Context()
var req ParseFileRequest
if err := c.ShouldBindJSON(&req); err != nil {
println("JSON bind error: %v (type: %T)", err, err)
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
return
}
if req.ModelID == nil {
if req.ProviderName == nil || *req.ProviderName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
if req.InstanceName == nil || *req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
}
if req.ModelName == nil || *req.ModelName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model name is required")
return
}
} else {
if *req.ModelID == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Model ID is empty")
return
}
}
userID := c.GetString("user_id")
modelRef := ""
if req.ModelID != nil {
modelRef = *req.ModelID
} else {
modelRef = fmt.Sprintf("%s@%s@%s", *req.ModelName, *req.InstanceName, *req.ProviderName)
}
parseFileConfig := models.ParseFileConfig{}
if req.Backend != nil {
parseFileConfig.Backend = strings.TrimSpace(*req.Backend)
}
if req.ServerURL != nil {
parseFileConfig.ServerURL = strings.TrimRight(strings.TrimSpace(*req.ServerURL), "/")
}
// Non-stream response
var response *models.ParseFileResponse
var errorCode common.ErrorCode
var err error
response, errorCode, err = h.modelCallService.ParseFile(ctx, modelRef, userID, req.Content, req.URL, &parseFileConfig)
if err != nil {
common.ErrorWithCode(c, errorCode, err.Error())
return
}
common.SuccessWithData(c, response, "success")
}
// ListTenantAddedModels is the response handler for GET /api/v1/models.
// It is the Go port of Python's
// api/apps/restful_apis/models_api.py:get_added_models and feeds
// web/src/hooks/use-llm-request.tsx → useFetchAllAddedModels. The data
// shape is the array form (one row per (provider × instance × llm) with
// model_type: string[]), matching the IAddedModel interface in
// web/src/interfaces/database/llm.ts:64-71.
//
// The previous contract routed this path to TenantHandler.GetModels →
// TenantService.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. The Go port
// has no writers for tenant_model, so this endpoint must be driven by
// the factory catalog cross-referenced with the tenant's instance list —
// see service.ModelProviderService.ListTenantAddedModels.
func (h *ProviderHandler) ListTenantAddedModels(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
common.ErrorWithCode(c, errorCode, errorMessage)
return
}
modelType := c.Query("type")
ownerTenantID := c.Query("owner_tenant_id")
ctx := c.Request.Context()
addedModels, code, err := h.modelProviderService.ListTenantAddedModels(ctx, user.ID, ownerTenantID, modelType)
if err != nil {
common.ErrorWithCode(c, code, err.Error())
return
}
common.SuccessWithData(c, addedModels, "success")
}