2026-03-04 19:17:16 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 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 (
|
2026-04-20 15:31:12 +08:00
|
|
|
|
"encoding/json"
|
2026-04-02 20:20:35 +08:00
|
|
|
|
"errors"
|
2026-03-04 19:17:16 +08:00
|
|
|
|
"fmt"
|
2026-04-02 20:20:35 +08:00
|
|
|
|
"ragflow/internal/common"
|
2026-03-04 19:17:16 +08:00
|
|
|
|
"ragflow/internal/dao"
|
2026-03-27 19:25:18 +08:00
|
|
|
|
"ragflow/internal/entity"
|
2026-04-17 09:55:25 +08:00
|
|
|
|
modelModule "ragflow/internal/entity/models"
|
2026-06-03 17:35:36 +08:00
|
|
|
|
"ragflow/internal/utility"
|
2026-03-04 19:17:16 +08:00
|
|
|
|
"strings"
|
2026-06-03 15:26:46 +08:00
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
|
"go.uber.org/zap"
|
2026-06-03 15:26:46 +08:00
|
|
|
|
"gorm.io/gorm"
|
2026-03-04 19:17:16 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-29 14:44:12 +08:00
|
|
|
|
// parseModelName parses a composite model name in format "model@instance@provider" or "model@provider"
|
2026-07-07 07:10:27 +05:30
|
|
|
|
// 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.
|
2026-04-29 14:44:12 +08:00
|
|
|
|
func parseModelName(compositeName string) (modelName, instanceName, providerName string, err error) {
|
2026-03-04 19:17:16 +08:00
|
|
|
|
parts := strings.Split(compositeName, "@")
|
2026-07-07 07:10:27 +05:30
|
|
|
|
switch len(parts) {
|
|
|
|
|
|
case 3:
|
2026-04-29 14:44:12 +08:00
|
|
|
|
// Format: model@instance@provider
|
|
|
|
|
|
return parts[0], parts[1], parts[2], nil
|
2026-07-07 07:10:27 +05:30
|
|
|
|
case 2:
|
2026-05-06 11:33:32 +08:00
|
|
|
|
// Format: model@provider -> instance defaults to "default"
|
|
|
|
|
|
return parts[0], "default", parts[1], nil
|
2026-07-07 07:10:27 +05:30
|
|
|
|
case 1:
|
2026-04-29 14:44:12 +08:00
|
|
|
|
return parts[0], "", "", fmt.Errorf("provider name missing in model name: %s", compositeName)
|
2026-03-04 19:17:16 +08:00
|
|
|
|
}
|
2026-07-07 07:10:27 +05:30
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
|
2026-07-07 07:10:27 +05:30
|
|
|
|
// 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]
|
2026-03-04 19:17:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 03:58:20 -03:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 08:33:09 -03:00
|
|
|
|
baseURLByRegion := map[string]string{
|
2026-05-20 03:58:20 -03:00
|
|
|
|
region: baseURL,
|
2026-05-29 08:33:09 -03:00
|
|
|
|
}
|
|
|
|
|
|
if region == "" {
|
|
|
|
|
|
baseURLByRegion["default"] = baseURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newDriver := driver.NewInstance(baseURLByRegion)
|
2026-05-20 03:58:20 -03:00
|
|
|
|
if newDriver == nil {
|
|
|
|
|
|
return nil, fmt.Errorf("provider %s does not support custom base_url", providerName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return newDriver, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
func NewModelProviderService() *ModelProviderService {
|
|
|
|
|
|
return &ModelProviderService{
|
|
|
|
|
|
modelProviderDAO: dao.NewTenantModelProviderDAO(),
|
|
|
|
|
|
modelInstanceDAO: dao.NewTenantModelInstanceDAO(),
|
|
|
|
|
|
modelDAO: dao.NewTenantModelDAO(),
|
|
|
|
|
|
modelGroupDAO: dao.NewTenantModelGroupDAO(),
|
|
|
|
|
|
modelGroupMappingDAO: dao.NewTenantModelGroupMappingDAO(),
|
2026-07-10 13:29:01 +08:00
|
|
|
|
tenantDAO: dao.NewTenantDAO(),
|
2026-04-02 20:20:35 +08:00
|
|
|
|
userTenantDAO: dao.NewUserTenantDAO(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ModelProviderService struct {
|
|
|
|
|
|
modelProviderDAO *dao.TenantModelProviderDAO
|
|
|
|
|
|
modelInstanceDAO *dao.TenantModelInstanceDAO
|
|
|
|
|
|
modelDAO *dao.TenantModelDAO
|
|
|
|
|
|
modelGroupDAO *dao.TenantModelGroupDAO
|
|
|
|
|
|
modelGroupMappingDAO *dao.TenantModelGroupMappingDAO
|
2026-07-10 13:29:01 +08:00
|
|
|
|
tenantDAO *dao.TenantDAO
|
2026-04-02 20:20:35 +08:00
|
|
|
|
userTenantDAO *dao.UserTenantDAO
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 19:32:41 +08:00
|
|
|
|
// CheckConnectionRequest carries the credentials and optional instance selector
|
|
|
|
|
|
// for checking provider connectivity without creating a new model instance.
|
|
|
|
|
|
type CheckConnectionRequest struct {
|
|
|
|
|
|
APIKey string `json:"api_key"`
|
|
|
|
|
|
Region string `json:"region"`
|
|
|
|
|
|
BaseURL string `json:"base_url"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
func (m *ModelProviderService) AddModelProvider(providerName, userID string) (common.ErrorCode, error) {
|
2026-06-29 17:03:26 +08:00
|
|
|
|
providerName = strings.TrimSpace(providerName)
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
2026-07-02 10:34:36 +08:00
|
|
|
|
existing, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if existing != nil {
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 14:06:07 +08:00
|
|
|
|
providerID := utility.GenerateToken()
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
|
|
|
|
|
tenantModelProvider := &entity.TenantModelProvider{
|
|
|
|
|
|
ID: providerID,
|
2026-06-29 17:03:26 +08:00
|
|
|
|
ProviderName: providerName,
|
2026-04-02 20:20:35 +08:00
|
|
|
|
TenantID: tenantID,
|
|
|
|
|
|
}
|
|
|
|
|
|
err = m.modelProviderDAO.Create(tenantModelProvider)
|
|
|
|
|
|
if err != nil {
|
2026-05-11 20:19:08 +08:00
|
|
|
|
return common.CodeServerError, fmt.Errorf("fail to create model provider: %s", err.Error())
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *ModelProviderService) ListProvidersOfTenant(userID string) ([]map[string]interface{}, common.ErrorCode, error) {
|
|
|
|
|
|
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(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(tenantID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var result []map[string]interface{}
|
|
|
|
|
|
for _, providerName := range providerNames {
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
provider, err := dao.GetModelProviderManager().GetProviderByName(providerName)
|
|
|
|
|
|
if err != nil {
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
2026-07-14 17:05:21 +08:00
|
|
|
|
// 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.
|
2026-07-13 11:20:02 +08:00
|
|
|
|
|
|
|
|
|
|
// Set has_instance flag. Mirrors Python's:
|
|
|
|
|
|
// provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, name)
|
|
|
|
|
|
// has_instance = bool(provider_obj and TenantModelInstanceService.get_all_by_provider_id(provider_obj.id))
|
|
|
|
|
|
provider["has_instance"] = m.providerHasInstance(tenantID, providerName)
|
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
result = append(result, provider)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 11:20:02 +08:00
|
|
|
|
// providerHasInstance checks whether the given tenant's provider has any
|
|
|
|
|
|
// configured instances. Mirrors the Python pattern:
|
|
|
|
|
|
//
|
|
|
|
|
|
// provider_obj = TenantModelProviderService.get_by_tenant_id_and_provider_name(tenant_id, name)
|
|
|
|
|
|
// has_instance = bool(provider_obj and TenantModelInstanceService.get_all_by_provider_id(provider_obj.id))
|
|
|
|
|
|
func (m *ModelProviderService) providerHasInstance(tenantID, providerName string) bool {
|
|
|
|
|
|
providerObj, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
instances, err := m.modelInstanceDAO.GetAllInstancesByProviderID(providerObj.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return len(instances) > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 17:03:26 +08:00
|
|
|
|
func (m *ModelProviderService) DeleteModelProvider(userID, providerName string) (common.ErrorCode, error) {
|
2026-04-02 20:20:35 +08:00
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
2026-07-13 11:20:02 +08:00
|
|
|
|
// Find the provider first.
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeNotFound, fmt.Errorf("provider %s not found", providerName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete all models and instances under this provider.
|
|
|
|
|
|
instances, err := m.modelInstanceDAO.GetAllInstancesByProviderID(provider.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(instances) > 0 {
|
|
|
|
|
|
instanceIDs := make([]string, len(instances))
|
|
|
|
|
|
for i, inst := range instances {
|
|
|
|
|
|
instanceIDs[i] = inst.ID
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := m.modelDAO.DeleteByInstanceIDs(instanceIDs); err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := m.modelInstanceDAO.DeleteByProviderID(provider.ID); err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
_, err = m.modelProviderDAO.DeleteByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 19:01:00 +08:00
|
|
|
|
func (m *ModelProviderService) ListSupportedModels(providerName, instanceName, userID string) ([]map[string]interface{}, error) {
|
2026-06-29 17:03:26 +08:00
|
|
|
|
providerName = strings.TrimSpace(providerName)
|
2026-04-21 16:52:32 +08:00
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(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(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 17:03:26 +08:00
|
|
|
|
providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
|
2026-04-21 16:52:32 +08:00
|
|
|
|
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 = ®ion
|
|
|
|
|
|
apiConfig.ApiKey = &instance.APIKey
|
|
|
|
|
|
|
2026-05-08 15:54:27 +08:00
|
|
|
|
driver := providerInfo.ModelDriver
|
|
|
|
|
|
|
2026-04-30 16:30:14 +08:00
|
|
|
|
// For local deployed models
|
|
|
|
|
|
if baseURL, ok := extra["base_url"]; ok && baseURL != "" {
|
2026-05-20 03:58:20 -03:00
|
|
|
|
driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
2026-04-30 16:30:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 19:01:00 +08:00
|
|
|
|
modelList, err := driver.ListModels(apiConfig)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var result []map[string]interface{}
|
|
|
|
|
|
for _, model := range modelList {
|
|
|
|
|
|
result = append(result, map[string]interface{}{
|
2026-06-11 17:55:13 +08:00
|
|
|
|
"name": model.Name,
|
|
|
|
|
|
"max_dimension": model.MaxDimension,
|
|
|
|
|
|
"dimensions": model.Dimensions,
|
|
|
|
|
|
"max_tokens": model.MaxTokens,
|
|
|
|
|
|
"model_types": model.ModelTypes,
|
|
|
|
|
|
"thinking": model.Thinking,
|
2026-06-09 19:01:00 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, nil
|
2026-04-21 16:52:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 11:20:02 +08:00
|
|
|
|
// CreateInstanceModelInfo mirrors Python's model_info dict in
|
|
|
|
|
|
// create_provider_instance. See api/apps/services/provider_api_service.py:434.
|
|
|
|
|
|
type CreateInstanceModelInfo struct {
|
|
|
|
|
|
ModelName string `json:"model_name"`
|
|
|
|
|
|
ModelTypes []string `json:"model_type"`
|
|
|
|
|
|
MaxTokens int `json:"max_tokens"`
|
|
|
|
|
|
Extra map[string]interface{} `json:"extra"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *ModelProviderService) CreateProviderInstance(providerName, instanceName, apiKey, baseURL, region, userID string, modelInfo []CreateInstanceModelInfo) (common.ErrorCode, error) {
|
2026-06-29 17:03:26 +08:00
|
|
|
|
providerName = strings.TrimSpace(providerName)
|
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
|
|
|
|
|
// Check if provider exists
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 11:20:02 +08:00
|
|
|
|
// Normalize api_key: VLLM with empty api_key defaults to "x".
|
|
|
|
|
|
// Mirrors Python's _normalize_provider_api_key.
|
|
|
|
|
|
if strings.EqualFold(providerName, "vllm") && apiKey == "" {
|
|
|
|
|
|
apiKey = "x"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Verify the API key against the provider.
|
|
|
|
|
|
// Mirrors Python's verify_api_key (provider_api_service.py:596).
|
|
|
|
|
|
modelVerifyResult := m.verifyProviderAPIKey(providerName, apiKey, region, baseURL, modelInfo)
|
|
|
|
|
|
|
2026-06-15 14:06:07 +08:00
|
|
|
|
instanceID := utility.GenerateToken()
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
2026-04-20 15:31:12 +08:00
|
|
|
|
extra := make(map[string]string)
|
|
|
|
|
|
extra["region"] = region
|
2026-04-29 17:05:08 +08:00
|
|
|
|
extra["base_url"] = baseURL
|
2026-04-20 15:31:12 +08:00
|
|
|
|
extraByte, err := json.Marshal(extra)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, errors.New("fail to marshal extra")
|
|
|
|
|
|
}
|
|
|
|
|
|
extraStr := string(extraByte)
|
|
|
|
|
|
|
2026-07-13 11:20:02 +08:00
|
|
|
|
tenantModelInstance := &entity.TenantModelInstance{
|
2026-04-02 20:20:35 +08:00
|
|
|
|
ID: instanceID,
|
|
|
|
|
|
InstanceName: instanceName,
|
|
|
|
|
|
ProviderID: provider.ID,
|
|
|
|
|
|
APIKey: apiKey,
|
2026-05-29 17:39:41 +08:00
|
|
|
|
Status: "active",
|
2026-04-20 15:31:12 +08:00
|
|
|
|
Extra: extraStr,
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
2026-07-13 11:20:02 +08:00
|
|
|
|
err = m.modelInstanceDAO.Create(tenantModelInstance)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, fmt.Errorf("fail to create model instance: %s", err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add models with verify result in extra.
|
|
|
|
|
|
if len(modelInfo) > 0 {
|
|
|
|
|
|
for _, model := range modelInfo {
|
|
|
|
|
|
if model.Extra == nil {
|
|
|
|
|
|
model.Extra = make(map[string]interface{})
|
|
|
|
|
|
}
|
|
|
|
|
|
verifyStatus := modelVerifyResult[model.ModelName]
|
|
|
|
|
|
if verifyStatus == "" {
|
|
|
|
|
|
verifyStatus = entity.ModelVerifyUnknown
|
|
|
|
|
|
}
|
|
|
|
|
|
model.Extra["verify"] = verifyStatus
|
|
|
|
|
|
if err := m.addModelToInstance(tenantID, providerName, instanceName, model); err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CreateNameOnlyProviderInstance creates a provider instance with only a name,
|
|
|
|
|
|
// skipping API key validation and model creation. Mirrors Python's
|
|
|
|
|
|
// create_name_only_provider_instance in provider_api_service.py:536.
|
|
|
|
|
|
func (m *ModelProviderService) CreateNameOnlyProviderInstance(providerName, instanceName, userID string) (common.ErrorCode, error) {
|
|
|
|
|
|
providerName = strings.TrimSpace(providerName)
|
|
|
|
|
|
|
|
|
|
|
|
if instanceName == "default" {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("instance name cannot be 'default'")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
2026-07-13 11:20:02 +08:00
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeNotFound, fmt.Errorf("provider '%s' does not exist", providerName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instanceID := utility.GenerateToken()
|
|
|
|
|
|
|
|
|
|
|
|
tenantModelInstance := &entity.TenantModelInstance{
|
|
|
|
|
|
ID: instanceID,
|
|
|
|
|
|
InstanceName: instanceName,
|
|
|
|
|
|
ProviderID: provider.ID,
|
|
|
|
|
|
APIKey: "",
|
|
|
|
|
|
Status: "active",
|
|
|
|
|
|
Extra: "{}",
|
|
|
|
|
|
}
|
|
|
|
|
|
err = m.modelInstanceDAO.Create(tenantModelInstance)
|
2026-04-02 20:20:35 +08:00
|
|
|
|
if err != nil {
|
2026-04-29 17:05:08 +08:00
|
|
|
|
return common.CodeServerError, fmt.Errorf("fail to create model instance: %s", err.Error())
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
2026-07-13 11:20:02 +08:00
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 11:20:02 +08:00
|
|
|
|
// verifyProviderAPIKey verifies the API key against the provider by calling
|
|
|
|
|
|
// the driver's CheckConnection. It returns a map from model name to verify
|
|
|
|
|
|
// status (success/fail/unknown). Mirrors Python's verify_api_key in
|
|
|
|
|
|
// provider_api_service.py:596.
|
|
|
|
|
|
func (m *ModelProviderService) verifyProviderAPIKey(providerName, apiKey, region, baseURL string, modelInfo []CreateInstanceModelInfo) map[string]string {
|
|
|
|
|
|
result := make(map[string]string)
|
|
|
|
|
|
|
|
|
|
|
|
providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
|
|
|
|
|
|
if providerInfo == nil {
|
|
|
|
|
|
// Provider not in system pool — mark all models as unknown.
|
|
|
|
|
|
for _, model := range modelInfo {
|
|
|
|
|
|
result[model.ModelName] = entity.ModelVerifyUnknown
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
apiKey = strings.TrimSpace(apiKey)
|
|
|
|
|
|
region = strings.TrimSpace(region)
|
|
|
|
|
|
baseURL = strings.TrimSpace(baseURL)
|
|
|
|
|
|
if region == "" {
|
|
|
|
|
|
region = "default"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
driver := providerInfo.ModelDriver
|
|
|
|
|
|
if strings.EqualFold(providerInfo.Class, "local") {
|
|
|
|
|
|
var err error
|
|
|
|
|
|
driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
for _, model := range modelInfo {
|
|
|
|
|
|
result[model.ModelName] = entity.ModelVerifyFail
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
apiConfig := &modelModule.APIConfig{
|
|
|
|
|
|
ApiKey: &apiKey,
|
|
|
|
|
|
Region: ®ion,
|
|
|
|
|
|
BaseURL: &baseURL,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
verifyErr := driver.CheckConnection(apiConfig)
|
|
|
|
|
|
verifyStatus := entity.ModelVerifySuccess
|
|
|
|
|
|
if verifyErr != nil {
|
|
|
|
|
|
verifyStatus = entity.ModelVerifyFail
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, model := range modelInfo {
|
|
|
|
|
|
result[model.ModelName] = verifyStatus
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// addModelToInstance creates a single model under the given provider instance.
|
|
|
|
|
|
// Mirrors Python's add_model_to_instance in provider_api_service.py:969.
|
|
|
|
|
|
func (m *ModelProviderService) addModelToInstance(tenantID, providerName, instanceName string, model CreateInstanceModelInfo) error {
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("no provider found for provider '%s'", providerName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("no instance found for provider '%s' and instance '%s'", providerName, instanceName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for duplicate model.
|
|
|
|
|
|
_, err = m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(provider.ID, instance.ID, model.ModelName)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
return fmt.Errorf("model '%s' already exists for provider '%s' and instance '%s'", model.ModelName, providerName, instanceName)
|
|
|
|
|
|
}
|
|
|
|
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Compute model type bitmask.
|
|
|
|
|
|
combinedType := entity.ModelType(0)
|
|
|
|
|
|
for _, t := range model.ModelTypes {
|
|
|
|
|
|
combinedType |= entity.ModelTypeFromString(t)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build extra fields. Mirrors Python's extra_fields in add_model_to_instance.
|
|
|
|
|
|
extraFields := map[string]interface{}{
|
|
|
|
|
|
"max_tokens": model.MaxTokens,
|
|
|
|
|
|
}
|
|
|
|
|
|
if model.Extra != nil {
|
|
|
|
|
|
for k, v := range model.Extra {
|
|
|
|
|
|
extraFields[k] = v
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
extraBytes, err := json.Marshal(extraFields)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("fail to marshal extra: %s", err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modelID := utility.GenerateToken()
|
|
|
|
|
|
tenantModel := &entity.TenantModel{
|
|
|
|
|
|
ID: modelID,
|
|
|
|
|
|
ModelName: model.ModelName,
|
|
|
|
|
|
ModelType: int(combinedType),
|
|
|
|
|
|
ProviderID: provider.ID,
|
|
|
|
|
|
InstanceID: instance.ID,
|
|
|
|
|
|
Status: "active",
|
|
|
|
|
|
Extra: string(extraBytes),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := m.modelDAO.Create(tenantModel); err != nil {
|
|
|
|
|
|
return fmt.Errorf("fail to create model '%s': %s", model.ModelName, err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
func (m *ModelProviderService) ListProviderInstances(providerName, userID string) ([]map[string]interface{}, common.ErrorCode, error) {
|
2026-06-29 17:03:26 +08:00
|
|
|
|
providerName = strings.TrimSpace(providerName)
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(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(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
// "provider not connected to this tenant" is a normal, expected state
|
|
|
|
|
|
// (e.g. demo tenant has never added SiliconFlow). Mirrors the Python
|
|
|
|
|
|
// contract in api/apps/services/provider_api_service.py:349-355 which
|
|
|
|
|
|
// returns (False, "No provider found for provider '<name>'") on this
|
|
|
|
|
|
// path. The REST layer maps that to get_error_data_result with
|
|
|
|
|
|
// code=RetCode.DATA_ERROR (=102), so the Go port must do the same —
|
|
|
|
|
|
// NOT a 500 server error.
|
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
return nil, common.CodeDataError, fmt.Errorf("No provider found for provider '%s'", providerName)
|
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if provider exists
|
|
|
|
|
|
instances, err := m.modelInstanceDAO.GetAllInstancesByProviderID(provider.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
// 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))
|
2026-04-02 20:20:35 +08:00
|
|
|
|
for _, instance := range instances {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// convert instance.Extra (JSON string) to map
|
2026-04-20 15:31:12 +08:00
|
|
|
|
var extra map[string]string
|
|
|
|
|
|
err = json.Unmarshal([]byte(instance.Extra), &extra)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
// Emit snake_case keys to match the IProviderInstance TypeScript
|
|
|
|
|
|
// contract (web/src/interfaces/database/llm.ts) and the Python
|
|
|
|
|
|
// `TenantModelInstanceService.query(joinedload(...))` response
|
|
|
|
|
|
// shape. The Go port previously emitted camelCase
|
|
|
|
|
|
// (`instanceName`/`providerID`/`apiKey`), which broke the
|
|
|
|
|
|
// `instance.instance_name` reads in
|
|
|
|
|
|
// web/src/pages/user-setting/setting-model/components/used-model.tsx
|
|
|
|
|
|
// and made `useFetchInstanceModels(providerName,
|
|
|
|
|
|
// instance.instance_name)` hit `/api/v1/providers/<p>/instances/undefined/models`.
|
2026-04-02 20:20:35 +08:00
|
|
|
|
result = append(result, map[string]interface{}{
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
"id": instance.ID,
|
|
|
|
|
|
"instance_name": instance.InstanceName,
|
|
|
|
|
|
"provider_id": instance.ProviderID,
|
|
|
|
|
|
"api_key": instance.APIKey,
|
|
|
|
|
|
"status": instance.Status,
|
|
|
|
|
|
"extra": instance.Extra,
|
2026-04-02 20:20:35 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *ModelProviderService) ShowProviderInstance(providerName, instanceName, userID string) (map[string]interface{}, common.ErrorCode, error) {
|
|
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(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(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// convert instance.Extra (JSON string) to map
|
2026-04-20 15:31:12 +08:00
|
|
|
|
var extra map[string]string
|
|
|
|
|
|
err = json.Unmarshal([]byte(instance.Extra), &extra)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
// Emit snake_case keys to match the IProviderInstance TypeScript
|
|
|
|
|
|
// contract — see ListProviderInstances above. The previous shape
|
|
|
|
|
|
// mixed conventions (`apikey` lowercase, `instanceName`/`providerID`
|
|
|
|
|
|
// camelCase) and broke the front-end's `instance.api_key` /
|
|
|
|
|
|
// `instance.instance_name` reads.
|
2026-04-02 20:20:35 +08:00
|
|
|
|
result := map[string]interface{}{
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
"id": instance.ID,
|
|
|
|
|
|
"instance_name": instance.InstanceName,
|
|
|
|
|
|
"provider_id": instance.ProviderID,
|
|
|
|
|
|
"status": instance.Status,
|
|
|
|
|
|
"api_key": instance.APIKey,
|
|
|
|
|
|
"region": extra["region"],
|
|
|
|
|
|
"base_url": extra["base_url"],
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 21:31:50 +08:00
|
|
|
|
func (m *ModelProviderService) ShowInstanceBalance(providerName, instanceName, userID string) (map[string]interface{}, common.ErrorCode, error) {
|
|
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(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(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(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"]
|
2026-06-03 13:23:20 +08:00
|
|
|
|
baseURL := extra["base_url"]
|
2026-04-21 21:31:50 +08:00
|
|
|
|
apiConfig.Region = ®ion
|
|
|
|
|
|
apiConfig.ApiKey = &instance.APIKey
|
2026-06-03 13:23:20 +08:00
|
|
|
|
apiConfig.BaseURL = &baseURL
|
2026-04-21 21:31:50 +08:00
|
|
|
|
|
|
|
|
|
|
var result map[string]interface{}
|
|
|
|
|
|
result, err = providerInfo.ModelDriver.Balance(apiConfig)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, common.CodeSuccess, nil
|
2026-04-23 10:16:20 +08:00
|
|
|
|
}
|
2026-04-21 21:31:50 +08:00
|
|
|
|
|
2026-06-02 19:32:41 +08:00
|
|
|
|
func (m *ModelProviderService) CheckConnection(providerName, apiKey, region, baseURL string, userID string) (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: ®ion,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := driver.CheckConnection(apiConfig)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *ModelProviderService) CheckInstanceConnection(providerName, instanceName, userID string) (common.ErrorCode, error) {
|
2026-04-23 10:16:20 +08:00
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
|
|
|
|
|
// Check if provider exists
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(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 = ®ion
|
|
|
|
|
|
apiConfig.ApiKey = &instance.APIKey
|
|
|
|
|
|
|
2026-05-08 15:54:27 +08:00
|
|
|
|
driver := providerInfo.ModelDriver
|
|
|
|
|
|
if baseURL, ok := extra["base_url"]; ok && baseURL != "" {
|
2026-05-20 03:58:20 -03:00
|
|
|
|
driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
2026-05-08 15:54:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err = driver.CheckConnection(apiConfig)
|
2026-04-23 10:16:20 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return common.CodeSuccess, nil
|
2026-04-21 21:31:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 12:29:52 +08:00
|
|
|
|
func (m *ModelProviderService) ListTasks(providerName, instanceName, userID string) ([]modelModule.ListTaskStatus, common.ErrorCode, error) {
|
|
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(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(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(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 = ®ion
|
|
|
|
|
|
apiConfig.ApiKey = &instance.APIKey
|
|
|
|
|
|
|
|
|
|
|
|
driver := providerInfo.ModelDriver
|
|
|
|
|
|
if baseURL, ok := extra["base_url"]; ok && baseURL != "" {
|
2026-05-20 03:58:20 -03:00
|
|
|
|
driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
2026-05-15 12:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var listTaskResponse []modelModule.ListTaskStatus
|
|
|
|
|
|
listTaskResponse, err = driver.ListTasks(apiConfig)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return listTaskResponse, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *ModelProviderService) ShowTask(providerName, instanceName, taskID, userID string) (*modelModule.TaskResponse, common.ErrorCode, error) {
|
|
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(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(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(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 = ®ion
|
|
|
|
|
|
apiConfig.ApiKey = &instance.APIKey
|
|
|
|
|
|
|
|
|
|
|
|
driver := providerInfo.ModelDriver
|
|
|
|
|
|
if baseURL, ok := extra["base_url"]; ok && baseURL != "" {
|
2026-05-20 03:58:20 -03:00
|
|
|
|
driver, err = newModelDriverForBaseURL(driver, providerName, region, baseURL)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
2026-05-15 12:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var taskResponse *modelModule.TaskResponse
|
|
|
|
|
|
taskResponse, err = driver.ShowTask(taskID, apiConfig)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return taskResponse, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
// 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.
|
|
|
|
|
|
//
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// Per the Python algorithm, for each (provider × instance) we cross-reference the factory catalog (internal/entity/models/model.go
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
// ProviderManager.Providers) with the per-tenant overrides in
|
|
|
|
|
|
// tenant_model:
|
2026-06-15 10:10:14 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 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
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 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.
|
2026-07-10 13:29:01 +08:00
|
|
|
|
func (m *ModelProviderService) ListTenantAddedModels(userID, ownerTenantID, modelTypeFilter string) ([]map[string]interface{}, common.ErrorCode, error) {
|
|
|
|
|
|
tenant, code, err := m.resolveModelListTenant(userID, ownerTenantID)
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
if err != nil {
|
2026-07-10 13:29:01 +08:00
|
|
|
|
return nil, code, err
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
}
|
2026-07-10 13:29:01 +08:00
|
|
|
|
if tenant == nil {
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
return []map[string]interface{}{}, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-07-10 13:29:01 +08:00
|
|
|
|
tenantID := tenant.ID
|
|
|
|
|
|
tenantName := ""
|
|
|
|
|
|
if tenant.Name != nil {
|
|
|
|
|
|
tenantName = *tenant.Name
|
|
|
|
|
|
}
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
|
|
|
|
|
|
if modelTypeFilter != "" {
|
|
|
|
|
|
modelTypeFilter = strings.ToLower(strings.TrimSpace(modelTypeFilter))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
providers, err := m.modelProviderDAO.GetByTenantID(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(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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Per-tenant enable/disable overrides. In the Go port this is
|
|
|
|
|
|
// typically empty (no writers), but we still honor active/inactive
|
|
|
|
|
|
// rows for correctness and parity.
|
|
|
|
|
|
modelRecords, err := m.modelDAO.GetModelsByProviderIDsAndInstanceIDs(providerIDs, instanceIDs)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
2026-07-08 18:02:11 +08:00
|
|
|
|
activeByKey := make(map[string]int)
|
|
|
|
|
|
inactiveByKey := make(map[string]int)
|
2026-07-10 13:29:01 +08:00
|
|
|
|
activeModelIDByKey := make(map[string]string)
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
for _, rec := range modelRecords {
|
|
|
|
|
|
key := rec.ProviderID + "@" + rec.InstanceID + "@" + rec.ModelName
|
|
|
|
|
|
if rec.Status == "inactive" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
inactiveByKey[key] |= rec.ModelType
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
} else {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
activeByKey[key] |= rec.ModelType
|
2026-07-10 13:29:01 +08:00
|
|
|
|
if activeModelIDByKey[key] == "" {
|
|
|
|
|
|
activeModelIDByKey[key] = rec.ID
|
|
|
|
|
|
}
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Group instances by provider_name for the outer loop.
|
|
|
|
|
|
instancesByProviderName := make(map[string][]*entity.TenantModelInstance)
|
|
|
|
|
|
for _, inst := range instances {
|
|
|
|
|
|
p, ok := providerInfoByID[inst.ProviderID]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
instancesByProviderName[p.ProviderName] = append(instancesByProviderName[p.ProviderName], inst)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
providerManager := dao.GetModelProviderManager()
|
|
|
|
|
|
added := make([]map[string]interface{}, 0)
|
|
|
|
|
|
|
|
|
|
|
|
// factory rank is not present in the Go entity.Provider struct, so we
|
|
|
|
|
|
// follow Python's stable ordering intent (factory rank desc, then
|
|
|
|
|
|
// provider_name, then instance_name) by simply iterating providers in
|
|
|
|
|
|
// the tenant's own order. With one provider today this is a no-op.
|
|
|
|
|
|
for _, p := range providers {
|
|
|
|
|
|
factory := providerManager.FindProvider(p.ProviderName)
|
|
|
|
|
|
if factory == nil {
|
|
|
|
|
|
// Factory not in the static catalog. The tenant has linked
|
|
|
|
|
|
// a provider we have no model list for. Skip — there is
|
|
|
|
|
|
// nothing to expose.
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
factoryInstances := instancesByProviderName[p.ProviderName]
|
|
|
|
|
|
if len(factoryInstances) == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, llm := range factory.Models {
|
|
|
|
|
|
if modelTypeFilter != "" {
|
|
|
|
|
|
match := false
|
|
|
|
|
|
for _, t := range llm.ModelTypes {
|
|
|
|
|
|
if strings.EqualFold(t, modelTypeFilter) {
|
|
|
|
|
|
match = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !match {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, inst := range factoryInstances {
|
|
|
|
|
|
key := p.ID + "@" + inst.ID + "@" + llm.Name
|
2026-07-08 18:02:11 +08:00
|
|
|
|
// Bitmask merge: factory types ∪ active overrides \ inactive overrides.
|
|
|
|
|
|
merged := entity.ModelTypeFromStrings(llm.ModelTypes)
|
|
|
|
|
|
merged |= entity.ModelType(activeByKey[key])
|
|
|
|
|
|
merged &^= entity.ModelType(inactiveByKey[key])
|
|
|
|
|
|
if merged == 0 {
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
added = append(added, map[string]interface{}{
|
2026-07-10 13:29:01 +08:00
|
|
|
|
"model_id": activeModelIDByKey[key],
|
|
|
|
|
|
"tenant_id": tenant.ID,
|
|
|
|
|
|
"tenant_name": tenantName,
|
|
|
|
|
|
"model_type": modelTypesForAPI(merged),
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
"name": llm.Name,
|
|
|
|
|
|
"provider_id": inst.ProviderID,
|
|
|
|
|
|
"provider_name": p.ProviderName,
|
|
|
|
|
|
"instance_id": inst.ID,
|
|
|
|
|
|
"instance_name": inst.InstanceName,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return added, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 13:29:01 +08:00
|
|
|
|
func modelTypesForAPI(modelType entity.ModelType) []string {
|
|
|
|
|
|
types := modelType.HumanReadable()
|
|
|
|
|
|
for i, t := range types {
|
|
|
|
|
|
if t == "image2text" {
|
|
|
|
|
|
types[i] = "vision"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return types
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *ModelProviderService) resolveModelListTenant(userID, ownerTenantID string) (*entity.Tenant, common.ErrorCode, error) {
|
|
|
|
|
|
if ownerTenantID == "" {
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(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(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(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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 11:20:02 +08:00
|
|
|
|
func (m *ModelProviderService) AlterProviderInstance(userID, providerName, instanceIDOrName, newInstanceName, apiKey, baseURL, region string, modelInfo []CreateInstanceModelInfo, verify bool) (common.ErrorCode, error) {
|
|
|
|
|
|
providerName = strings.TrimSpace(providerName)
|
|
|
|
|
|
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeNotFound, fmt.Errorf("provider '%s' does not exist", providerName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Find the instance — try by ID first, then by name.
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByID(instanceIDOrName)
|
|
|
|
|
|
if err != nil || instance.ProviderID != provider.ID {
|
|
|
|
|
|
instance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceIDOrName)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeNotFound, fmt.Errorf("no instance found for provider '%s' and instance '%s'", providerName, instanceIDOrName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Normalize api_key: VLLM with empty api_key defaults to "x".
|
|
|
|
|
|
if strings.EqualFold(providerName, "vllm") && apiKey == "" {
|
|
|
|
|
|
apiKey = "x"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Verify API key if requested.
|
|
|
|
|
|
modelVerifyResult := make(map[string]string)
|
|
|
|
|
|
if verify {
|
|
|
|
|
|
modelVerifyResult = m.verifyProviderAPIKey(providerName, apiKey, region, baseURL, modelInfo)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update instance record.
|
|
|
|
|
|
instanceUpdates := map[string]interface{}{
|
|
|
|
|
|
"api_key": apiKey,
|
|
|
|
|
|
}
|
|
|
|
|
|
if newInstanceName != "" && newInstanceName != instance.InstanceName {
|
|
|
|
|
|
instanceUpdates["instance_name"] = newInstanceName
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
extraFields := make(map[string]interface{})
|
|
|
|
|
|
if baseURL != "" {
|
|
|
|
|
|
extraFields["base_url"] = baseURL
|
|
|
|
|
|
}
|
|
|
|
|
|
if region != "" {
|
|
|
|
|
|
extraFields["region"] = region
|
|
|
|
|
|
}
|
|
|
|
|
|
// Preserve existing extra fields not overwritten.
|
|
|
|
|
|
existingExtra := make(map[string]interface{})
|
|
|
|
|
|
if instance.Extra != "" {
|
|
|
|
|
|
if err := json.Unmarshal([]byte(instance.Extra), &existingExtra); err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for k, v := range extraFields {
|
|
|
|
|
|
existingExtra[k] = v
|
|
|
|
|
|
}
|
|
|
|
|
|
extraBytes, err := json.Marshal(existingExtra)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
instanceUpdates["extra"] = string(extraBytes)
|
|
|
|
|
|
if err := m.modelInstanceDAO.UpdateByID(instance.ID, instanceUpdates); err != nil {
|
|
|
|
|
|
return common.CodeServerError, fmt.Errorf("fail to update instance: %s", err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use the (possibly updated) instance_name for model operations.
|
|
|
|
|
|
effectiveInstanceName := instance.InstanceName
|
|
|
|
|
|
if newInstanceName != "" {
|
|
|
|
|
|
effectiveInstanceName = newInstanceName
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Upsert models: add new ones, update existing ones, remove ones no longer selected.
|
|
|
|
|
|
existingModels, err := m.modelDAO.GetModelsByInstanceID(instance.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
existingModelMap := make(map[string]*entity.TenantModel)
|
|
|
|
|
|
for _, mdl := range existingModels {
|
|
|
|
|
|
existingModelMap[mdl.ModelName] = mdl
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete models that are no longer in the submitted model_info.
|
|
|
|
|
|
submittedModelNames := make(map[string]bool)
|
|
|
|
|
|
if modelInfo != nil {
|
|
|
|
|
|
for _, mdl := range modelInfo {
|
|
|
|
|
|
if mdl.ModelName != "" {
|
|
|
|
|
|
submittedModelNames[mdl.ModelName] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
var idsToRemove []string
|
|
|
|
|
|
for name, mdl := range existingModelMap {
|
|
|
|
|
|
if !submittedModelNames[name] {
|
|
|
|
|
|
idsToRemove = append(idsToRemove, mdl.ID)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(idsToRemove) > 0 {
|
|
|
|
|
|
if _, err := m.modelDAO.DeleteByIDs(idsToRemove); err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add or update models from model_info.
|
|
|
|
|
|
if modelInfo != nil {
|
|
|
|
|
|
for _, mdl := range modelInfo {
|
|
|
|
|
|
if mdl.ModelName == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
// Attach verify status.
|
|
|
|
|
|
if verify {
|
|
|
|
|
|
verifyStatus := modelVerifyResult[mdl.ModelName]
|
|
|
|
|
|
if verifyStatus == "" {
|
|
|
|
|
|
verifyStatus = entity.ModelVerifyUnknown
|
|
|
|
|
|
}
|
|
|
|
|
|
if mdl.Extra == nil {
|
|
|
|
|
|
mdl.Extra = make(map[string]interface{})
|
|
|
|
|
|
}
|
|
|
|
|
|
mdl.Extra["verify"] = verifyStatus
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if existingMdl, exists := existingModelMap[mdl.ModelName]; exists {
|
|
|
|
|
|
// Update existing model.
|
|
|
|
|
|
updates := make(map[string]interface{})
|
|
|
|
|
|
if len(mdl.ModelTypes) > 0 {
|
|
|
|
|
|
combinedType := entity.ModelType(0)
|
|
|
|
|
|
for _, t := range mdl.ModelTypes {
|
|
|
|
|
|
combinedType |= entity.ModelTypeFromString(t)
|
|
|
|
|
|
}
|
|
|
|
|
|
if int(combinedType) != existingMdl.ModelType {
|
|
|
|
|
|
updates["model_type"] = int(combinedType)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
mergedExtra := make(map[string]interface{})
|
|
|
|
|
|
if existingMdl.Extra != "" {
|
|
|
|
|
|
json.Unmarshal([]byte(existingMdl.Extra), &mergedExtra)
|
|
|
|
|
|
}
|
|
|
|
|
|
if mdl.Extra != nil {
|
|
|
|
|
|
for k, v := range mdl.Extra {
|
|
|
|
|
|
mergedExtra[k] = v
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if mdl.MaxTokens > 0 {
|
|
|
|
|
|
mergedExtra["max_tokens"] = mdl.MaxTokens
|
|
|
|
|
|
}
|
|
|
|
|
|
extraBytes, _ := json.Marshal(mergedExtra)
|
|
|
|
|
|
updates["extra"] = string(extraBytes)
|
|
|
|
|
|
if len(updates) > 0 {
|
|
|
|
|
|
if err := m.modelDAO.UpdateByID(existingMdl.ID, updates); err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Add new model.
|
|
|
|
|
|
if err := m.addModelToInstance(tenantID, providerName, effectiveInstanceName, mdl); err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-06-18 14:19:46 +08:00
|
|
|
|
|
|
|
|
|
|
func (m *ModelProviderService) getProviderInstancesByNames(providerID string, instanceNames []string) ([]*entity.TenantModelInstance, []string, error) {
|
|
|
|
|
|
modelInstances := make([]*entity.TenantModelInstance, 0, len(instanceNames))
|
|
|
|
|
|
missingInstanceNames := make([]string, 0)
|
|
|
|
|
|
|
|
|
|
|
|
for _, instanceName := range instanceNames {
|
|
|
|
|
|
modelInstance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(providerID, instanceName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
missingInstanceNames = append(missingInstanceNames, instanceName)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
modelInstances = append(modelInstances, modelInstance)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return modelInstances, missingInstanceNames, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *ModelProviderService) DropProviderInstances(providerName, userID string, instanceNames []string) (common.ErrorCode, error) {
|
|
|
|
|
|
if len(instanceNames) == 0 {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("instances is required")
|
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
|
|
|
|
|
// Check if provider exists
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
2026-06-18 14:19:46 +08:00
|
|
|
|
return common.CodeNotFound, fmt.Errorf("no provider found for provider %q", providerName)
|
feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 14:19:46 +08:00
|
|
|
|
modelInstances, missingInstanceNames, err := m.getProviderInstancesByNames(provider.ID, instanceNames)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(missingInstanceNames) > 0 {
|
|
|
|
|
|
return common.CodeNotFound, fmt.Errorf("no instance found for provider %q and instances %q", providerName, missingInstanceNames)
|
|
|
|
|
|
}
|
2026-04-29 19:18:49 +08:00
|
|
|
|
|
2026-06-18 14:19:46 +08:00
|
|
|
|
for _, modelInstance := range modelInstances {
|
|
|
|
|
|
if _, err = m.modelDAO.DeleteByProviderIDAndInstanceID(provider.ID, modelInstance.ID); err != nil {
|
2026-04-29 19:18:49 +08:00
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 14:19:46 +08:00
|
|
|
|
count, err := m.modelInstanceDAO.DeleteByProviderIDAndInstanceName(provider.ID, modelInstance.InstanceName)
|
2026-04-17 09:55:25 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if count == 0 {
|
2026-06-18 14:19:46 +08:00
|
|
|
|
return common.CodeNotFound, fmt.Errorf("provider instance %q not found", modelInstance.InstanceName)
|
2026-04-17 09:55:25 +08:00
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
func (m *ModelProviderService) DropInstanceModels(providerName, instanceName, userID string, modelIDs, models []string) (common.ErrorCode, error) {
|
2026-04-29 19:18:49 +08:00
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
|
|
|
|
|
// Check if provider exists
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var modelInstance *entity.TenantModelInstance
|
|
|
|
|
|
modelInstance, err = m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
for _, modelID := range modelIDs {
|
|
|
|
|
|
modelID = strings.TrimSpace(modelID)
|
|
|
|
|
|
if modelID == "" {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("model ID is required")
|
|
|
|
|
|
}
|
|
|
|
|
|
var count int64 = 0
|
|
|
|
|
|
count, err = m.modelDAO.DeleteByModelIDAndProviderIDAndInstanceID(modelID, provider.ID, modelInstance.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if count == 0 {
|
|
|
|
|
|
return common.CodeNotFound, fmt.Errorf("model %s not found", modelID)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 19:18:49 +08:00
|
|
|
|
for _, modelName := range models {
|
2026-06-18 17:56:51 +08:00
|
|
|
|
modelName = strings.TrimSpace(modelName)
|
|
|
|
|
|
if modelName == "" {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("model name is required")
|
|
|
|
|
|
}
|
2026-04-29 19:18:49 +08:00
|
|
|
|
// Delete all models of this instance
|
|
|
|
|
|
var count int64 = 0
|
|
|
|
|
|
count, err = m.modelDAO.DeleteByProviderIDAndInstanceIDAndModelName(provider.ID, modelInstance.ID, modelName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if count == 0 {
|
|
|
|
|
|
return common.CodeNotFound, fmt.Errorf("model: %s not found", modelName)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
func (m *ModelProviderService) ListInstanceModels(providerName, instanceName, userID string) ([]map[string]interface{}, error) {
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get instance
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get all models for this instance
|
|
|
|
|
|
disabledModels, err := m.modelDAO.GetModelsByInstanceID(instance.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 17:05:08 +08:00
|
|
|
|
allModels, err := dao.GetModelProviderManager().ListModels(providerName)
|
|
|
|
|
|
|
2026-04-02 20:20:35 +08:00
|
|
|
|
// insert models name into a set
|
|
|
|
|
|
modelNames := make(map[string]bool)
|
|
|
|
|
|
for _, model := range disabledModels {
|
2026-04-29 17:05:08 +08:00
|
|
|
|
if model.Status == "active" {
|
|
|
|
|
|
modelData := map[string]interface{}{
|
|
|
|
|
|
"name": model.ModelName,
|
|
|
|
|
|
}
|
|
|
|
|
|
allModels = append(allModels, modelData)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
modelNames[model.ModelName] = true
|
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
2026-04-29 17:05:08 +08:00
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
|
|
|
|
|
for _, model := range allModels {
|
|
|
|
|
|
// convert model["name"] to string
|
|
|
|
|
|
modelName := model["name"].(string)
|
|
|
|
|
|
if modelNames[modelName] {
|
2026-04-29 17:05:08 +08:00
|
|
|
|
model["status"] = "inactive"
|
2026-04-02 20:20:35 +08:00
|
|
|
|
} else {
|
2026-04-29 17:05:08 +08:00
|
|
|
|
model["status"] = "active"
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return allModels, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
func (m *ModelProviderService) UpdateModelStatus(providerName, instanceName, modelName, userID, modelID, status string) (common.ErrorCode, error) {
|
|
|
|
|
|
modelName = strings.TrimSpace(modelName)
|
|
|
|
|
|
modelID = strings.TrimSpace(modelID)
|
|
|
|
|
|
status = strings.TrimSpace(status)
|
|
|
|
|
|
if status != "active" && status != "inactive" {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("status must be active or inactive")
|
|
|
|
|
|
}
|
|
|
|
|
|
if modelName == "" && modelID == "" {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("model name or model ID is required")
|
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
|
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
|
|
|
|
|
// Check if provider exists
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
var model *entity.TenantModel
|
2026-04-17 09:55:25 +08:00
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
if modelID != "" {
|
|
|
|
|
|
model, err = m.modelDAO.GetByID(modelID)
|
|
|
|
|
|
if err != nil || model == nil {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("model not found")
|
2026-04-17 09:55:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
if model.ProviderID != provider.ID || model.InstanceID != instance.ID {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("model not found")
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
2026-06-18 17:56:51 +08:00
|
|
|
|
|
|
|
|
|
|
if modelName != "" && model.ModelName != modelName {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("model ID does not match model name")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
count, err := m.modelDAO.UpdateStatusByIDAndScope(modelID, provider.ID, instance.ID, status)
|
2026-04-02 20:20:35 +08:00
|
|
|
|
if err != nil {
|
2026-06-18 17:56:51 +08:00
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if count == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("model not found")
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
return common.CodeSuccess, nil
|
2026-06-18 17:56:51 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
model, err = m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(provider.ID, instance.ID, modelName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modelID = utility.GenerateToken()
|
|
|
|
|
|
|
|
|
|
|
|
var modelSchema *modelModule.Model
|
|
|
|
|
|
modelSchema, err = dao.GetModelProviderManager().GetModelByName(providerName, modelName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeNotFound, errors.New(fmt.Sprintf("provider %s model %s not found", providerName, modelName))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get model info from provider
|
|
|
|
|
|
if len(modelSchema.ModelTypes) == 0 {
|
|
|
|
|
|
return common.CodeServerError, fmt.Errorf("provider %s model %s has no model types", providerName, modelName)
|
|
|
|
|
|
}
|
|
|
|
|
|
model = &entity.TenantModel{
|
|
|
|
|
|
ID: modelID,
|
|
|
|
|
|
ModelName: modelName,
|
2026-07-08 18:02:11 +08:00
|
|
|
|
ModelType: int(entity.ModelTypeFromString(modelSchema.ModelTypes[0])),
|
2026-06-18 17:56:51 +08:00
|
|
|
|
ProviderID: provider.ID,
|
|
|
|
|
|
InstanceID: instance.ID,
|
|
|
|
|
|
Status: status,
|
|
|
|
|
|
}
|
|
|
|
|
|
err = m.modelDAO.Create(model)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, errors.New("fail to create model")
|
|
|
|
|
|
}
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
count, err := m.modelDAO.UpdateStatusByIDAndScope(model.ID, provider.ID, instance.ID, status)
|
2026-04-02 20:20:35 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if count == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("model not found")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
type ModelInstanceAndProviderInfo struct {
|
|
|
|
|
|
ProviderEntity *entity.TenantModelProvider
|
|
|
|
|
|
ProviderInfo *modelModule.Provider
|
|
|
|
|
|
InstanceEntity *entity.TenantModelInstance
|
|
|
|
|
|
ModelEntity *entity.TenantModel
|
|
|
|
|
|
ModelInfo *modelModule.Model
|
|
|
|
|
|
APIConfig *modelModule.APIConfig
|
|
|
|
|
|
}
|
2026-04-02 20:20:35 +08:00
|
|
|
|
|
2026-06-16 10:31:27 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
func (m *ModelProviderService) getModelInstanceAndProviderByName(providerName, instanceName, modelName *string, userID string, apiConfig *modelModule.APIConfig) (*ModelInstanceAndProviderInfo, error) {
|
2026-04-02 20:20:35 +08:00
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, err
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(tenants) == 0 {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, err
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
|
|
|
|
|
// Check if provider exists
|
2026-06-15 10:10:14 +08:00
|
|
|
|
providerEntity, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, *providerName)
|
2026-04-02 20:20:35 +08:00
|
|
|
|
if err != nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, err
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
instanceEntity, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(providerEntity.ID, *instanceName)
|
2026-04-02 20:20:35 +08:00
|
|
|
|
if err != nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, err
|
2026-04-02 20:20:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelEntity, err := m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(providerEntity.ID, instanceEntity.ID, *modelName)
|
2026-04-02 20:20:35 +08:00
|
|
|
|
if err != nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// Not found model
|
|
|
|
|
|
modelEntity = nil
|
2026-04-03 18:11:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
providerInfo := dao.GetModelProviderManager().FindProvider(*providerName)
|
|
|
|
|
|
if providerInfo == nil {
|
|
|
|
|
|
return nil, errors.New("provider not found")
|
|
|
|
|
|
}
|
2026-04-29 17:05:08 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelInfo, err := dao.GetModelProviderManager().GetModelByName(*providerName, *modelName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, errors.New(fmt.Sprintf("provider %s model %s not found", *providerName, *modelName))
|
|
|
|
|
|
}
|
2026-06-16 10:31:27 +08:00
|
|
|
|
modelInfo, err = modelInfoWithTenantExtra(modelInfo, modelEntity)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-04-29 17:05:08 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var extra map[string]string
|
|
|
|
|
|
err = json.Unmarshal([]byte(instanceEntity.Extra), &extra)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-04-29 17:05:08 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if apiConfig == nil {
|
|
|
|
|
|
apiConfig = &modelModule.APIConfig{}
|
|
|
|
|
|
}
|
2026-04-29 17:05:08 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
region := extra["region"]
|
|
|
|
|
|
baseURL := extra["base_url"]
|
2026-04-29 17:05:08 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
apiConfig.ApiKey = &instanceEntity.APIKey
|
|
|
|
|
|
apiConfig.BaseURL = &baseURL
|
|
|
|
|
|
apiConfig.Region = ®ion
|
2026-04-30 15:25:01 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var result = &ModelInstanceAndProviderInfo{
|
|
|
|
|
|
ProviderEntity: providerEntity,
|
|
|
|
|
|
ProviderInfo: providerInfo,
|
|
|
|
|
|
InstanceEntity: instanceEntity,
|
|
|
|
|
|
ModelEntity: modelEntity,
|
|
|
|
|
|
ModelInfo: modelInfo,
|
|
|
|
|
|
APIConfig: apiConfig,
|
2026-04-29 17:05:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return result, nil
|
2026-04-03 18:11:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
func (m *ModelProviderService) getModelInstanceAndProviderByID(modelID *string, userID string, apiConfig *modelModule.APIConfig) (*ModelInstanceAndProviderInfo, error) {
|
2026-04-03 18:11:23 +08:00
|
|
|
|
// Get tenant ID from user
|
|
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, err
|
2026-04-03 18:11:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(tenants) == 0 {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, err
|
2026-04-03 18:11:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelEntity, err := m.modelDAO.GetByID(*modelID)
|
2026-04-03 18:11:23 +08:00
|
|
|
|
if err != nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, err
|
2026-04-03 18:11:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
instanceEntity, err := m.modelInstanceDAO.GetByID(modelEntity.InstanceID)
|
2026-04-03 18:11:23 +08:00
|
|
|
|
if err != nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, err
|
2026-04-03 18:11:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
providerEntity, err := m.modelProviderDAO.GetByID(instanceEntity.ProviderID)
|
2026-04-03 18:11:23 +08:00
|
|
|
|
if err != nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-04-03 18:11:23 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if providerEntity.TenantID != tenantID {
|
|
|
|
|
|
return nil, errors.New("provider not found")
|
|
|
|
|
|
}
|
2026-04-03 18:11:23 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
providerInfo := dao.GetModelProviderManager().FindProvider(providerEntity.ProviderName)
|
|
|
|
|
|
if providerInfo == nil {
|
|
|
|
|
|
return nil, errors.New("provider not found")
|
|
|
|
|
|
}
|
2026-05-12 19:44:01 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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))
|
|
|
|
|
|
}
|
2026-06-16 10:31:27 +08:00
|
|
|
|
modelInfo, err = modelInfoWithTenantExtra(modelInfo, modelEntity)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-04-20 15:31:12 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var extra map[string]string
|
|
|
|
|
|
err = json.Unmarshal([]byte(instanceEntity.Extra), &extra)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-04-20 15:31:12 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if apiConfig == nil {
|
|
|
|
|
|
apiConfig = &modelModule.APIConfig{}
|
|
|
|
|
|
}
|
2026-04-03 18:11:23 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
region := extra["region"]
|
|
|
|
|
|
baseURL := extra["base_url"]
|
|
|
|
|
|
|
|
|
|
|
|
apiConfig.ApiKey = &instanceEntity.APIKey
|
|
|
|
|
|
apiConfig.BaseURL = &baseURL
|
|
|
|
|
|
apiConfig.Region = ®ion
|
|
|
|
|
|
|
|
|
|
|
|
var result = &ModelInstanceAndProviderInfo{
|
|
|
|
|
|
ProviderEntity: providerEntity,
|
|
|
|
|
|
ProviderInfo: providerInfo,
|
|
|
|
|
|
InstanceEntity: instanceEntity,
|
|
|
|
|
|
ModelEntity: modelEntity,
|
|
|
|
|
|
ModelInfo: modelInfo,
|
|
|
|
|
|
APIConfig: apiConfig,
|
2026-04-03 18:11:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ChatToModelWithMessages sends messages to the model with messages array
|
|
|
|
|
|
func (m *ModelProviderService) ChatToModelWithMessages(providerName, instanceName, modelName, modelID *string, userID string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig) (*modelModule.ChatResponse, common.ErrorCode, error) {
|
|
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
|
var info *ModelInstanceAndProviderInfo
|
|
|
|
|
|
|
|
|
|
|
|
if modelID != nil {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByID(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-12 19:44:01 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-04-30 16:30:14 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
2026-04-30 16:30:14 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelConfig == nil {
|
|
|
|
|
|
modelConfig = &modelModule.ChatConfig{}
|
|
|
|
|
|
}
|
|
|
|
|
|
modelConfig.ModelClass = info.ModelInfo.Class
|
2026-06-16 10:31:27 +08:00
|
|
|
|
if modelConfig.Thinking == nil && info.ModelInfo.Thinking != nil {
|
|
|
|
|
|
thinking := info.ModelInfo.Thinking.DefaultValue
|
|
|
|
|
|
modelConfig.Thinking = &thinking
|
|
|
|
|
|
}
|
2026-06-18 17:56:51 +08:00
|
|
|
|
resolvedModelName := info.ModelInfo.Name
|
|
|
|
|
|
if info.ModelEntity != nil && info.ModelEntity.ModelName != "" {
|
|
|
|
|
|
resolvedModelName = info.ModelEntity.ModelName
|
|
|
|
|
|
}
|
|
|
|
|
|
resolvedProviderName := info.ProviderEntity.ProviderName
|
2026-06-15 10:10:14 +08:00
|
|
|
|
|
|
|
|
|
|
var response *modelModule.ChatResponse
|
|
|
|
|
|
var modelDriver modelModule.ModelDriver
|
|
|
|
|
|
|
|
|
|
|
|
if info.ModelEntity == nil {
|
|
|
|
|
|
if !info.ModelInfo.ModelTypeMap["chat"] && !info.ModelInfo.ModelTypeMap["vision"] {
|
2026-06-18 17:56:51 +08:00
|
|
|
|
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a chat or multimodal model", resolvedModelName, resolvedProviderName))
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeChat) && !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeImage2Text) {
|
2026-06-18 17:56:51 +08:00
|
|
|
|
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a chat or multimodal model", resolvedModelName, resolvedProviderName))
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, resolvedProviderName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return nil, common.CodeServerError, errors.New("model is inactive")
|
2026-04-30 16:30:14 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
response, err = modelDriver.ChatWithMessages(resolvedModelName, messages, info.APIConfig, modelConfig)
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-04-30 16:30:14 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// ChatToModelStreamWithSender streams chat response directly via sender function ( the best performance, no channel)
|
|
|
|
|
|
func (m *ModelProviderService) ChatToModelStreamWithSender(providerName, instanceName, modelName, modelID *string, userID string, messages []modelModule.Message, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ChatConfig, sender func(*string, *string) error) (common.ErrorCode, error) {
|
2026-04-30 16:30:14 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
var info *ModelInstanceAndProviderInfo
|
2026-04-30 16:30:14 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelID != nil {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByID(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return common.CodeNotFound, err
|
2026-04-30 16:30:14 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return common.CodeNotFound, err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-30 16:30:14 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelConfig == nil {
|
|
|
|
|
|
modelConfig = &modelModule.ChatConfig{}
|
|
|
|
|
|
}
|
|
|
|
|
|
modelConfig.ModelClass = info.ModelInfo.Class
|
2026-06-16 10:31:27 +08:00
|
|
|
|
if modelConfig.Thinking == nil && info.ModelInfo.Thinking != nil {
|
|
|
|
|
|
thinking := info.ModelInfo.Thinking.DefaultValue
|
|
|
|
|
|
modelConfig.Thinking = &thinking
|
|
|
|
|
|
}
|
2026-06-18 17:56:51 +08:00
|
|
|
|
resolvedModelName := info.ModelInfo.Name
|
|
|
|
|
|
if info.ModelEntity != nil && info.ModelEntity.ModelName != "" {
|
|
|
|
|
|
resolvedModelName = info.ModelEntity.ModelName
|
|
|
|
|
|
}
|
|
|
|
|
|
resolvedProviderName := info.ProviderEntity.ProviderName
|
2026-06-15 10:10:14 +08:00
|
|
|
|
|
|
|
|
|
|
var modelDriver modelModule.ModelDriver
|
|
|
|
|
|
|
|
|
|
|
|
if info.ModelEntity == nil {
|
|
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeChat) && !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeImage2Text) {
|
2026-06-18 17:56:51 +08:00
|
|
|
|
return common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a chat or multimodal model", resolvedModelName, resolvedProviderName))
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, resolvedProviderName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return common.CodeServerError, errors.New("model is inactive")
|
2026-04-30 16:30:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
err = modelDriver.ChatStreamlyWithSender(resolvedModelName, messages, info.APIConfig, modelConfig, sender)
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return common.CodeSuccess, nil
|
2026-04-03 18:11:23 +08:00
|
|
|
|
}
|
2026-04-24 15:30:14 +08:00
|
|
|
|
|
2026-06-11 17:55:13 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 17:41:54 +08:00
|
|
|
|
// EmbedText sends texts to the embedding model
|
2026-06-15 10:10:14 +08:00
|
|
|
|
func (m *ModelProviderService) EmbedText(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(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
|
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
|
2026-05-09 17:41:54 +08:00
|
|
|
|
if modelConfig == nil {
|
|
|
|
|
|
modelConfig = &modelModule.EmbeddingConfig{}
|
|
|
|
|
|
}
|
2026-06-18 17:56:51 +08:00
|
|
|
|
resolvedModelName := info.ModelInfo.Name
|
|
|
|
|
|
if info.ModelEntity != nil && info.ModelEntity.ModelName != "" {
|
|
|
|
|
|
resolvedModelName = info.ModelEntity.ModelName
|
|
|
|
|
|
}
|
|
|
|
|
|
resolvedProviderName := info.ProviderEntity.ProviderName
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var modelDriver modelModule.ModelDriver
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if info.ModelEntity == nil {
|
|
|
|
|
|
if !info.ModelInfo.ModelTypeMap["embedding"] {
|
2026-06-18 17:56:51 +08:00
|
|
|
|
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an embedding model", resolvedModelName, resolvedProviderName))
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeEmbedding) {
|
2026-06-18 17:56:51 +08:00
|
|
|
|
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an embedding model", resolvedModelName, resolvedProviderName))
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, resolvedProviderName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return nil, common.CodeServerError, errors.New("model is inactive")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if err = validateEmbeddingDimension(info.ModelInfo, modelConfig.Dimension); err != nil {
|
|
|
|
|
|
return nil, common.CodeBadRequest, err
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var response []modelModule.EmbeddingData
|
2026-06-18 17:56:51 +08:00
|
|
|
|
response, err = modelDriver.Embed(&resolvedModelName, texts, info.APIConfig, modelConfig)
|
2026-05-09 17:41:54 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if response == nil || len(response) == 0 {
|
|
|
|
|
|
return nil, common.CodeServerError, errors.New("empty embed response")
|
|
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return response, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// RerankDocument sends texts to the embedding model
|
|
|
|
|
|
func (m *ModelProviderService) RerankDocument(providerName, instanceName, modelName, modelID *string, userID, query string, documents []string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.RerankConfig) (*modelModule.RerankResponse, common.ErrorCode, error) {
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
var info *ModelInstanceAndProviderInfo
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelID != nil {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByID(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if modelConfig == nil {
|
|
|
|
|
|
modelConfig = &modelModule.RerankConfig{}
|
|
|
|
|
|
}
|
2026-06-18 17:56:51 +08:00
|
|
|
|
resolvedModelName := info.ModelInfo.Name
|
|
|
|
|
|
if info.ModelEntity != nil && info.ModelEntity.ModelName != "" {
|
|
|
|
|
|
resolvedModelName = info.ModelEntity.ModelName
|
|
|
|
|
|
}
|
|
|
|
|
|
resolvedProviderName := info.ProviderEntity.ProviderName
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var modelDriver modelModule.ModelDriver
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if info.ModelEntity == nil {
|
|
|
|
|
|
if !info.ModelInfo.ModelTypeMap["rerank"] {
|
2026-06-18 17:56:51 +08:00
|
|
|
|
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a rerank model", resolvedModelName, resolvedProviderName))
|
2026-06-11 17:55:13 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeRerank) {
|
2026-06-18 17:56:51 +08:00
|
|
|
|
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a rerank model", resolvedModelName, resolvedProviderName))
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
2026-06-11 17:55:13 +08:00
|
|
|
|
|
2026-06-18 17:56:51 +08:00
|
|
|
|
modelDriver, err = newModelDriverForBaseURL(info.ProviderInfo.ModelDriver, resolvedProviderName, *info.APIConfig.Region, *info.APIConfig.BaseURL)
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return nil, common.CodeServerError, errors.New("model is inactive")
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var response *modelModule.RerankResponse
|
2026-06-18 17:56:51 +08:00
|
|
|
|
response, err = modelDriver.Rerank(&resolvedModelName, query, documents, info.APIConfig, modelConfig)
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return response, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// TranscribeAudio transcribe audio file to text
|
|
|
|
|
|
func (m *ModelProviderService) TranscribeAudio(providerName, instanceName, modelName, modelID *string, userID string, audioFile *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ASRConfig) (*modelModule.ASRResponse, common.ErrorCode, error) {
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
var info *ModelInstanceAndProviderInfo
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelID != nil {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByID(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if modelConfig == nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelConfig = &modelModule.ASRConfig{}
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var modelDriver modelModule.ModelDriver
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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))
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeSpeech2Text) {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an ASR model", *modelName, *providerName))
|
|
|
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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")
|
2026-05-09 17:41:54 +08:00
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var response *modelModule.ASRResponse
|
|
|
|
|
|
response, err = modelDriver.TranscribeAudio(modelName, audioFile, apiConfig, modelConfig)
|
2026-05-12 17:17:44 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if response == nil {
|
|
|
|
|
|
return nil, common.CodeServerError, errors.New("empty chat response")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return response, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-05-12 19:44:01 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// TranscribeAudioStream transcribe audio file to text stream directly via sender function ( the best performance, no channel)
|
|
|
|
|
|
func (m *ModelProviderService) TranscribeAudioStream(providerName, instanceName, modelName, modelID *string, userID string, audioFile *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ASRConfig, sender func(*string, *string) error) (common.ErrorCode, error) {
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
var info *ModelInstanceAndProviderInfo
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelID != nil {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByID(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelConfig == nil {
|
|
|
|
|
|
modelConfig = &modelModule.ASRConfig{}
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var modelDriver modelModule.ModelDriver
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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))
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeSpeech2Text) {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an ASR model", *modelName, *providerName))
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
err = modelDriver.TranscribeAudioWithSender(modelName, audioFile, apiConfig, modelConfig, sender)
|
2026-05-12 17:17:44 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// AudioSpeech convert audio to speech
|
|
|
|
|
|
func (m *ModelProviderService) AudioSpeech(providerName, instanceName, modelName, modelID *string, userID string, audioContent *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.TTSConfig) (*modelModule.TTSResponse, common.ErrorCode, error) {
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
var info *ModelInstanceAndProviderInfo
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelID != nil {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByID(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelConfig == nil {
|
|
|
|
|
|
modelConfig = &modelModule.TTSConfig{}
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var modelDriver modelModule.ModelDriver
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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))
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeTTS) {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a TTS model", *modelName, *providerName))
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var response *modelModule.TTSResponse
|
|
|
|
|
|
response, err = modelDriver.AudioSpeech(modelName, audioContent, apiConfig, modelConfig)
|
2026-05-12 17:17:44 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if response == nil {
|
|
|
|
|
|
return nil, common.CodeServerError, errors.New("empty chat response")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return response, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-05-12 19:44:01 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
func (m *ModelProviderService) AudioSpeechStream(providerName, instanceName, modelName, modelID *string, userID string, audioContent *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.TTSConfig, sender func(*string, *string) error) (common.ErrorCode, error) {
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
var info *ModelInstanceAndProviderInfo
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelID != nil {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByID(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelConfig == nil {
|
|
|
|
|
|
modelConfig = &modelModule.TTSConfig{}
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var modelDriver modelModule.ModelDriver
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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))
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeTTS) {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is a TTS model", *modelName, *providerName))
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
err = modelDriver.AudioSpeechWithSender(modelName, audioContent, apiConfig, modelConfig, sender)
|
2026-05-12 17:17:44 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
func (m *ModelProviderService) OCRFile(providerName, instanceName, modelName, modelID *string, userID string, content []byte, url *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.OCRConfig) (*modelModule.OCRFileResponse, common.ErrorCode, error) {
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
var info *ModelInstanceAndProviderInfo
|
2026-05-12 19:44:01 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelID != nil {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByID(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelConfig == nil {
|
|
|
|
|
|
modelConfig = &modelModule.OCRConfig{}
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var modelDriver modelModule.ModelDriver
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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))
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
if !entity.ModelType(info.ModelEntity.ModelType).Has(entity.ModelTypeOCR) {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, common.CodeNotFound, errors.New(fmt.Sprintf("expect model %s@%s is an OCR model", *modelName, *providerName))
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var response *modelModule.OCRFileResponse
|
|
|
|
|
|
response, err = modelDriver.OCRFile(modelName, content, url, apiConfig, modelConfig)
|
2026-05-12 17:17:44 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if response == nil {
|
|
|
|
|
|
return nil, common.CodeServerError, errors.New("empty chat response")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return response, common.CodeSuccess, nil
|
|
|
|
|
|
}
|
2026-05-12 19:44:01 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
func (m *ModelProviderService) ParseFile(providerName, instanceName, modelName, modelID *string, userID string, content []byte, url *string, apiConfig *modelModule.APIConfig, modelConfig *modelModule.ParseFileConfig) (*modelModule.ParseFileResponse, common.ErrorCode, error) {
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
var info *ModelInstanceAndProviderInfo
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelID != nil {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByID(modelID, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
info, err = m.getModelInstanceAndProviderByName(providerName, instanceName, modelName, userID, apiConfig)
|
|
|
|
|
|
if err != nil || info == nil {
|
|
|
|
|
|
return nil, common.CodeNotFound, err
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if modelConfig == nil {
|
|
|
|
|
|
modelConfig = &modelModule.ParseFileConfig{}
|
|
|
|
|
|
}
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var modelDriver modelModule.ModelDriver
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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))
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
modelDriver = info.ProviderInfo.ModelDriver
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// model entity exists
|
|
|
|
|
|
if info.ModelEntity.Status == "active" {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
// 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.
|
2026-05-12 17:17:44 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
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")
|
2026-05-12 17:17:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
var response *modelModule.ParseFileResponse
|
|
|
|
|
|
response, err = modelDriver.ParseFile(modelName, content, url, apiConfig, modelConfig)
|
2026-05-15 12:29:52 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, common.CodeServerError, err
|
|
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if response == nil {
|
|
|
|
|
|
return nil, common.CodeServerError, errors.New("empty chat response")
|
2026-05-15 12:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return response, common.CodeSuccess, nil
|
2026-05-15 12:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 12:59:01 +08:00
|
|
|
|
// GetEmbeddingModel returns an EmbeddingModel wrapper for the given tenant
|
|
|
|
|
|
func (m *ModelProviderService) GetEmbeddingModel(tenantID, compositeModelName string) (*modelModule.EmbeddingModel, error) {
|
2026-07-10 13:29:01 +08:00
|
|
|
|
driver, modelName, apiConfig, maxTokens, err := m.ResolveModelConfig(tenantID, entity.ModelTypeEmbedding, compositeModelName)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-04-30 12:36:03 +08:00
|
|
|
|
return modelModule.NewEmbeddingModel(driver, &modelName, apiConfig, maxTokens), nil
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// GetChatModel returns a ChatModel wrapper for the given tenant
|
2026-04-28 12:59:01 +08:00
|
|
|
|
func (m *ModelProviderService) GetChatModel(tenantID, compositeModelName string) (*modelModule.ChatModel, error) {
|
2026-07-10 13:29:01 +08:00
|
|
|
|
driver, modelName, apiConfig, _, err := m.ResolveModelConfig(tenantID, entity.ModelTypeChat, compositeModelName)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-04-28 13:21:05 +08:00
|
|
|
|
return modelModule.NewChatModel(driver, &modelName, apiConfig), nil
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 20:07:45 -07:00
|
|
|
|
// GetRerankModel returns a RerankModel wrapper for the given tenant
|
|
|
|
|
|
func (m *ModelProviderService) GetRerankModel(tenantID, compositeModelName string) (*modelModule.RerankModel, error) {
|
2026-07-10 13:29:01 +08:00
|
|
|
|
driver, modelName, apiConfig, _, err := m.ResolveModelConfig(tenantID, entity.ModelTypeRerank, compositeModelName)
|
2026-06-09 20:07:45 -07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return modelModule.NewRerankModel(driver, &modelName, apiConfig), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:26:46 +08:00
|
|
|
|
type AddModelRequest struct {
|
|
|
|
|
|
ProviderName string `json:"provider_name"`
|
|
|
|
|
|
InstanceName string `json:"instance_name"`
|
|
|
|
|
|
Models []ModelRequest `json:"models"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
|
func (m *ModelProviderService) GetTenantDefaultModelByType(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")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 13:29:01 +08:00
|
|
|
|
tenant, err := m.tenantDAO.GetByID(tenantID)
|
2026-06-08 11:49:37 +08:00
|
|
|
|
if err != nil {
|
2026-07-10 13:29:01 +08:00
|
|
|
|
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(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))
|
2026-06-08 11:49:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
if modelName == "" {
|
|
|
|
|
|
return nil, "", nil, 0, fmt.Errorf("no default %s model is set", modelType)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 13:29:01 +08:00
|
|
|
|
return m.ResolveModelConfig(tenantID, modelType, modelName)
|
2026-06-08 11:49:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 18:02:11 +08:00
|
|
|
|
// GetModelConfigByID returns model driver and API config for a tenant_model row by its ID.
|
2026-07-10 13:29:01 +08:00
|
|
|
|
func (m *ModelProviderService) GetModelConfigByID(userID string, modelType entity.ModelType, modelID string) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) {
|
2026-07-08 18:02:11 +08:00
|
|
|
|
common.Debug("GetModelConfigByID",
|
|
|
|
|
|
zap.String("userID", userID),
|
2026-07-10 13:29:01 +08:00
|
|
|
|
zap.String("modelType", modelType.String()),
|
2026-07-08 18:02:11 +08:00
|
|
|
|
zap.String("modelID", modelID))
|
|
|
|
|
|
|
|
|
|
|
|
modelEntity, err := m.modelDAO.GetByID(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)
|
|
|
|
|
|
}
|
2026-07-10 13:29:01 +08:00
|
|
|
|
if modelType != 0 && !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())
|
|
|
|
|
|
}
|
2026-07-08 18:02:11 +08:00
|
|
|
|
|
|
|
|
|
|
providerEntity, err := m.modelProviderDAO.GetByID(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().GetUserTenantRelationByUserID(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(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: ®ion, BaseURL: &baseURL}
|
|
|
|
|
|
return modelDriver, modelEntity.ModelName, apiConfig, maxTokens, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 13:29:01 +08:00
|
|
|
|
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(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(modelRef); err == nil {
|
|
|
|
|
|
return m.GetModelConfigByID(tenantID, modelType, modelRef)
|
|
|
|
|
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
return nil, "", nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return m.GetModelConfigFromProviderInstance(tenantID, modelType, modelRef)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m *ModelProviderService) ResolveModelID(tenantID string, modelType entity.ModelType, modelName string) (string, error) {
|
|
|
|
|
|
if modelObj, err := m.modelDAO.GetByID(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(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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(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(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(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(tenantID, modelRef string) ([]entity.ModelType, error) {
|
|
|
|
|
|
modelObj, err := m.modelDAO.GetByID(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(tenantID, modelRef)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// GetModelTypeByName returns the list of model types the given model is enrolled as.
|
|
|
|
|
|
func (m *ModelProviderService) GetModelTypeByName(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(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(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(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
|
2026-07-09 10:19:10 +08:00
|
|
|
|
// 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
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
2026-07-08 18:02:11 +08:00
|
|
|
|
return []entity.ModelType{entity.ModelTypeFromString(targetProvider.Models[i].ModelTypes[0])}, nil
|
2026-06-08 11:49:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:26:46 +08:00
|
|
|
|
type ModelRequest struct {
|
2026-06-16 10:31:27 +08:00
|
|
|
|
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"`
|
2026-04-29 17:05:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:26:46 +08:00
|
|
|
|
func (m *ModelProviderService) AddModel(request *AddModelRequest, userID string) (common.ErrorCode, error) {
|
2026-05-28 23:15:01 -03:00
|
|
|
|
if request == nil {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("request is required")
|
|
|
|
|
|
}
|
2026-06-03 15:26:46 +08:00
|
|
|
|
if len(request.Models) == 0 {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("models is required")
|
2026-05-28 23:15:01 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 17:05:08 +08:00
|
|
|
|
tenants, err := m.userTenantDAO.GetByUserIDAndRole(userID, "owner")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(tenants) == 0 {
|
|
|
|
|
|
return common.CodeNotFound, errors.New("user has no tenants")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tenantID := tenants[0].TenantID
|
|
|
|
|
|
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, request.ProviderName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
instance, err := m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, request.InstanceName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:26:46 +08:00
|
|
|
|
seen := make(map[string]struct{})
|
|
|
|
|
|
models := make([]*entity.TenantModel, 0, len(request.Models))
|
2026-04-29 17:05:08 +08:00
|
|
|
|
|
2026-06-03 15:26:46 +08:00
|
|
|
|
for _, model := range request.Models {
|
|
|
|
|
|
modelName := strings.TrimSpace(model.ModelName)
|
|
|
|
|
|
if len(model.ModelTypes) == 0 {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("model types is required")
|
|
|
|
|
|
}
|
2026-04-29 17:05:08 +08:00
|
|
|
|
|
2026-06-03 15:26:46 +08:00
|
|
|
|
if modelName == "" {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("model name is required")
|
|
|
|
|
|
}
|
2026-07-08 18:02:11 +08:00
|
|
|
|
|
|
|
|
|
|
// Validate each model type string and compute the combined bitmask.
|
|
|
|
|
|
combinedType := entity.ModelType(0)
|
|
|
|
|
|
seenTypes := make(map[string]struct{}, len(model.ModelTypes))
|
|
|
|
|
|
validTypes := make([]string, 0, len(model.ModelTypes))
|
|
|
|
|
|
for _, rawType := range model.ModelTypes {
|
|
|
|
|
|
mt := strings.TrimSpace(rawType)
|
|
|
|
|
|
if mt == "" {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("model type is required")
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, ok := seenTypes[mt]; ok {
|
|
|
|
|
|
continue // skip duplicates
|
|
|
|
|
|
}
|
|
|
|
|
|
seenTypes[mt] = struct{}{}
|
|
|
|
|
|
validTypes = append(validTypes, mt)
|
|
|
|
|
|
t := entity.ModelTypeFromString(mt)
|
|
|
|
|
|
if t == 0 {
|
|
|
|
|
|
return common.CodeBadRequest, fmt.Errorf("invalid model type: %s", mt)
|
|
|
|
|
|
}
|
|
|
|
|
|
combinedType |= t
|
2026-06-03 15:26:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
duplicateKey := strings.ToLower(modelName)
|
|
|
|
|
|
if _, ok := seen[duplicateKey]; ok {
|
|
|
|
|
|
return common.CodeConflict, fmt.Errorf("duplicate model in request: %s", modelName)
|
|
|
|
|
|
}
|
|
|
|
|
|
seen[duplicateKey] = struct{}{}
|
|
|
|
|
|
|
2026-06-08 21:38:15 +08:00
|
|
|
|
_, err = m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(provider.ID, instance.ID, modelName)
|
2026-06-03 15:26:46 +08:00
|
|
|
|
if err == nil {
|
|
|
|
|
|
return common.CodeConflict, fmt.Errorf("model already exists: %s", modelName)
|
|
|
|
|
|
}
|
|
|
|
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
2026-04-29 17:05:08 +08:00
|
|
|
|
|
2026-06-15 14:06:07 +08:00
|
|
|
|
modelID := utility.GenerateToken()
|
2026-06-03 15:26:46 +08:00
|
|
|
|
|
2026-06-16 10:31:27 +08:00
|
|
|
|
if model.MaxDimension < 0 {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("max_dimension must be non-negative")
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, dimension := range model.Dimensions {
|
|
|
|
|
|
if dimension <= 0 {
|
|
|
|
|
|
return common.CodeBadRequest, errors.New("dimensions must contain positive values")
|
|
|
|
|
|
}
|
|
|
|
|
|
if model.MaxDimension > 0 && dimension > model.MaxDimension {
|
|
|
|
|
|
return common.CodeBadRequest, fmt.Errorf("dimension %d exceeds max_dimension %d", dimension, model.MaxDimension)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-03 15:26:46 +08:00
|
|
|
|
extra := map[string]interface{}{
|
2026-06-16 10:31:27 +08:00
|
|
|
|
"max_tokens": model.MaxTokens,
|
|
|
|
|
|
"max_dimension": model.MaxDimension,
|
|
|
|
|
|
"dimensions": model.Dimensions,
|
2026-06-03 15:26:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
if model.Thinking != nil {
|
|
|
|
|
|
extra["thinking"] = *model.Thinking
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 21:38:15 +08:00
|
|
|
|
var extraByte []byte
|
|
|
|
|
|
extraByte, err = json.Marshal(extra)
|
2026-06-03 15:26:46 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return common.CodeServerError, errors.New("fail to marshal extra")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
models = append(models, &entity.TenantModel{
|
|
|
|
|
|
ID: modelID,
|
|
|
|
|
|
ModelName: modelName,
|
2026-07-08 18:02:11 +08:00
|
|
|
|
ModelType: int(combinedType),
|
2026-06-03 15:26:46 +08:00
|
|
|
|
ProviderID: provider.ID,
|
|
|
|
|
|
InstanceID: instance.ID,
|
|
|
|
|
|
Status: "active",
|
|
|
|
|
|
Extra: string(extraByte),
|
|
|
|
|
|
})
|
2026-04-29 17:05:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 21:38:15 +08:00
|
|
|
|
if err = m.modelDAO.CreateBatch(models); err != nil {
|
2026-04-29 17:05:08 +08:00
|
|
|
|
return common.CodeServerError, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return common.CodeSuccess, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// GetModelConfigFromProviderInstance get model config from provider instance
|
|
|
|
|
|
// modelName@instance@provider or
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// "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
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// raise). Otherwise, the factory's LLM catalog is consulted, with
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// region=intl + siliconflow redirected to the siliconflow_intl factory.
|
|
|
|
|
|
func (m *ModelProviderService) GetModelConfigFromProviderInstance(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),
|
2026-07-08 18:02:11 +08:00
|
|
|
|
zap.String("modelType", modelType.String()))
|
2026-06-08 11:49:37 +08:00
|
|
|
|
|
|
|
|
|
|
// TEI builtin embedding short-circuit
|
2026-07-10 11:58:32 +08:00
|
|
|
|
if modelType == entity.ModelTypeEmbedding && strings.Contains(common.GetEnv(common.EnvComposeProfiles), "tei-") {
|
|
|
|
|
|
teiModel := common.GetEnv(common.EnvTEIModel)
|
|
|
|
|
|
teiBaseURL := common.GetEnv(common.EnvTEIBaseURL)
|
2026-07-08 18:02:11 +08:00
|
|
|
|
|
2026-07-07 07:10:27 +05:30
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-07-08 18:02:11 +08:00
|
|
|
|
|
2026-07-07 07:10:27 +05:30
|
|
|
|
// 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 == "") {
|
2026-06-08 11:49:37 +08:00
|
|
|
|
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.
|
2026-07-07 07:10:27 +05:30
|
|
|
|
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: ®ion}
|
|
|
|
|
|
maxTokens := 0
|
|
|
|
|
|
if mi, _ := dao.GetModelProviderManager().GetModelByName("Builtin", pureModelName); mi != nil {
|
|
|
|
|
|
if mi.MaxTokens == nil {
|
|
|
|
|
|
maxTokens = 0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
maxTokens = *mi.MaxTokens
|
|
|
|
|
|
}
|
2026-06-09 19:01:00 +08:00
|
|
|
|
}
|
2026-07-07 07:10:27 +05:30
|
|
|
|
return builtinDriver, pureModelName, apiConfig, maxTokens, nil
|
2026-06-08 11:49:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pureModelName, instanceName, providerName, err := parseModelName(modelName)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
if err != nil {
|
2026-04-30 12:36:03 +08:00
|
|
|
|
return nil, "", nil, 0, err
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// Direct provider lookup
|
|
|
|
|
|
provider, provErr := m.modelProviderDAO.GetByTenantIDAndProviderName(tenantID, providerName)
|
|
|
|
|
|
if provErr != nil {
|
|
|
|
|
|
return nil, "", nil, 0, fmt.Errorf("provider %q lookup failed: %w", providerName, provErr)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
if provider == nil {
|
2026-06-08 11:49:37 +08:00
|
|
|
|
return nil, "", nil, 0, fmt.Errorf("provider %q not found for model %q", providerName, modelName)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// Direct instance lookup
|
|
|
|
|
|
instance, instErr := m.modelInstanceDAO.GetByProviderIDAndInstanceName(provider.ID, instanceName)
|
|
|
|
|
|
if instErr != nil {
|
|
|
|
|
|
return nil, "", nil, 0, fmt.Errorf("instance %q lookup failed: %w", instanceName, instErr)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
if instance == nil {
|
2026-06-08 11:49:37 +08:00
|
|
|
|
return nil, "", nil, 0, fmt.Errorf("instance %q not found for model %q", instanceName, modelName)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// Decode api_key and extra fields from the instance row
|
|
|
|
|
|
apiKey := instance.APIKey
|
2026-04-28 12:59:01 +08:00
|
|
|
|
var extra map[string]string
|
2026-06-08 11:49:37 +08:00
|
|
|
|
_ = json.Unmarshal([]byte(instance.Extra), &extra)
|
|
|
|
|
|
region := extra["region"]
|
|
|
|
|
|
baseURL := extra["base_url"]
|
|
|
|
|
|
|
|
|
|
|
|
// Direct model lookup
|
|
|
|
|
|
modelObj, modelErr := m.modelDAO.GetByProviderIDAndInstanceIDAndModelTypeAndModelName(
|
2026-07-08 18:02:11 +08:00
|
|
|
|
provider.ID, instance.ID, int(modelType), pureModelName,
|
2026-06-08 11:49:37 +08:00
|
|
|
|
)
|
|
|
|
|
|
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 {
|
2026-06-09 19:01:00 +08:00
|
|
|
|
if mi.MaxTokens == nil {
|
|
|
|
|
|
maxTokens = 0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
maxTokens = *mi.MaxTokens
|
|
|
|
|
|
}
|
2026-06-08 11:49:37 +08:00
|
|
|
|
}
|
2026-06-16 10:31:27 +08:00
|
|
|
|
maxTokens, driverErr = maxTokensFromTenantModelExtra(modelObj, maxTokens)
|
|
|
|
|
|
if driverErr != nil {
|
|
|
|
|
|
return nil, "", nil, 0, driverErr
|
|
|
|
|
|
}
|
2026-06-08 11:49:37 +08:00
|
|
|
|
apiConfig := &modelModule.APIConfig{ApiKey: &apiKey, Region: ®ion, 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),
|
2026-07-08 18:02:11 +08:00
|
|
|
|
zap.String("modelType", modelType.String()),
|
2026-06-08 11:49:37 +08:00
|
|
|
|
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
|
2026-07-09 10:19:10 +08:00
|
|
|
|
// 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
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// (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)
|
|
|
|
|
|
}
|
2026-06-09 19:01:00 +08:00
|
|
|
|
var llmInfo *modelModule.Model
|
2026-06-08 11:49:37 +08:00
|
|
|
|
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: ®ion, BaseURL: &baseURL}
|
2026-06-09 19:01:00 +08:00
|
|
|
|
maxTokens := 0
|
|
|
|
|
|
if llmInfo.MaxTokens != nil {
|
|
|
|
|
|
maxTokens = *llmInfo.MaxTokens
|
|
|
|
|
|
}
|
|
|
|
|
|
return driver, llmInfo.Name, apiConfig, maxTokens, nil
|
2026-06-08 11:49:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getModelConfig returns the model driver, model name, API config, and max tokens for a model
|
|
|
|
|
|
func (m *ModelProviderService) getModelConfig(tenantID, compositeModelName string) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) {
|
|
|
|
|
|
modelName, instanceName, providerName, err := parseModelName(compositeModelName)
|
2026-04-28 12:59:01 +08:00
|
|
|
|
if err != nil {
|
2026-04-30 12:36:03 +08:00
|
|
|
|
return nil, "", nil, 0, err
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
2026-06-08 11:49:37 +08:00
|
|
|
|
|
|
|
|
|
|
// Check if provider exists (skip for Builtin provider)
|
|
|
|
|
|
var providerID string
|
|
|
|
|
|
if providerName != "Builtin" {
|
|
|
|
|
|
provider, err := m.modelProviderDAO.GetByTenantIDAndProviderName(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(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))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var extra map[string]string
|
|
|
|
|
|
var region string
|
2026-06-16 10:31:27 +08:00
|
|
|
|
var baseURL string
|
2026-06-08 11:49:37 +08:00
|
|
|
|
if instance != nil {
|
|
|
|
|
|
err = json.Unmarshal([]byte(instance.Extra), &extra)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, "", nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
region = extra["region"]
|
2026-06-16 10:31:27 +08:00
|
|
|
|
baseURL = extra["base_url"]
|
2026-06-08 11:49:37 +08:00
|
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
|
|
|
|
|
|
providerInfo := dao.GetModelProviderManager().FindProvider(providerName)
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if providerInfo == nil {
|
|
|
|
|
|
if providerName != "Builtin" {
|
|
|
|
|
|
return nil, "", nil, 0, fmt.Errorf("model provider config not found: %s", providerName)
|
|
|
|
|
|
}
|
2026-04-30 12:36:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get model info to extract max_tokens
|
|
|
|
|
|
modelInfo, err := dao.GetModelProviderManager().GetModelByName(providerName, modelName)
|
|
|
|
|
|
maxTokens := 0
|
|
|
|
|
|
if err == nil && modelInfo != nil {
|
2026-06-09 19:01:00 +08:00
|
|
|
|
if modelInfo.MaxTokens == nil {
|
|
|
|
|
|
maxTokens = 0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
maxTokens = *modelInfo.MaxTokens
|
|
|
|
|
|
}
|
2026-04-30 12:36:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
|
// 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: ®ion}
|
|
|
|
|
|
return builtinDriver, modelName, apiConfig, maxTokens, nil
|
2026-06-15 10:10:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 10:31:27 +08:00
|
|
|
|
var modelRecord *entity.TenantModel
|
|
|
|
|
|
modelRecord, err = m.modelDAO.GetModelByProviderIDAndInstanceIDAndModelName(providerID, instance.ID, modelName)
|
2026-06-15 10:10:14 +08:00
|
|
|
|
if err != nil {
|
2026-06-16 10:31:27 +08:00
|
|
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
return nil, "", nil, 0, fmt.Errorf("tenant model %q lookup failed: %w", modelName, err)
|
|
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
_, err = dao.GetModelProviderManager().GetModelByName(providerName, modelName)
|
2026-04-30 12:36:03 +08:00
|
|
|
|
if err != nil {
|
2026-06-15 10:10:14 +08:00
|
|
|
|
return nil, "", nil, 0, fmt.Errorf("provider %s model %s not found", providerName, modelName)
|
2026-04-30 12:36:03 +08:00
|
|
|
|
}
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
2026-06-16 10:31:27 +08:00
|
|
|
|
maxTokens, err = maxTokensFromTenantModelExtra(modelRecord, maxTokens)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, "", nil, 0, err
|
|
|
|
|
|
}
|
2026-06-15 10:10:14 +08:00
|
|
|
|
apiKey = instance.APIKey
|
2026-04-28 12:59:01 +08:00
|
|
|
|
|
2026-06-16 10:31:27 +08:00
|
|
|
|
driver, err := newModelDriverForBaseURL(providerInfo.ModelDriver, providerName, region, baseURL)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, "", nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
apiConfig := &modelModule.APIConfig{ApiKey: &apiKey, Region: ®ion, BaseURL: &baseURL}
|
|
|
|
|
|
return driver, modelName, apiConfig, maxTokens, nil
|
2026-04-28 12:59:01 +08:00
|
|
|
|
}
|
2026-06-08 21:38:15 +08:00
|
|
|
|
|
2026-06-15 10:10:14 +08:00
|
|
|
|
// ListAllModels list all models
|
2026-06-08 21:38:15 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 19:01:00 +08:00
|
|
|
|
func (m *ModelProviderService) ShowModel(modelName string) (*modelModule.Model, error) {
|
2026-06-08 21:38:15 +08:00
|
|
|
|
return dao.GetModelProviderManager().GetModelByNameOrAlias(modelName), nil
|
|
|
|
|
|
}
|
2026-06-18 18:07:27 +08:00
|
|
|
|
|
|
|
|
|
|
// 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(tenantID, llmID string) bool {
|
|
|
|
|
|
if m == nil || llmID == "" {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
2026-07-10 13:29:01 +08:00
|
|
|
|
modelTypes, err := m.ResolveModelType(tenantID, llmID)
|
2026-06-18 18:07:27 +08:00
|
|
|
|
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(tenantID string, llmID string) (modelModule.ModelDriver, string, *modelModule.APIConfig, int, error) {
|
|
|
|
|
|
if llmID == "" {
|
|
|
|
|
|
return m.GetTenantDefaultModelByType(tenantID, entity.ModelTypeChat)
|
|
|
|
|
|
}
|
|
|
|
|
|
modelType := entity.ModelTypeChat
|
|
|
|
|
|
if m.isImage2TextLLM(tenantID, llmID) {
|
|
|
|
|
|
modelType = entity.ModelTypeImage2Text
|
|
|
|
|
|
}
|
2026-07-10 13:29:01 +08:00
|
|
|
|
return m.ResolveModelConfig(tenantID, modelType, llmID)
|
2026-06-18 18:07:27 +08:00
|
|
|
|
}
|