feat: openclaw supports configuring multiple models (#12213)

This commit is contained in:
CityFun
2026-03-18 10:36:11 +00:00
committed by GitHub
parent 23e7d03d8d
commit c68251cada
40 changed files with 2019 additions and 340 deletions
+81
View File
@@ -190,6 +190,87 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
})
}
// @Tags AI
// @Summary List Agent account models
// @Accept json
// @Param request body dto.AgentAccountModelReq true "request"
// @Success 200 {array} dto.AgentAccountModel
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models [post]
func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
var req dto.AgentAccountModelReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list, err := agentService.GetAccountModels(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags AI
// @Summary Create Agent account model
// @Accept json
// @Param request body dto.AgentAccountModelCreateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/create [post]
func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelCreateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.CreateAccountModel(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Update Agent account model
// @Accept json
// @Param request body dto.AgentAccountModelUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/update [post]
func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateAccountModel(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Delete Agent account model
// @Accept json
// @Param request body dto.AgentAccountModelDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/delete [post]
func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.DeleteAccountModel(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Verify Agent account
// @Accept json
+73 -37
View File
@@ -75,31 +75,62 @@ type AgentModelConfigUpdateReq struct {
Model string `json:"model" validate:"required"`
}
type AgentAccountModel struct {
RecordID uint `json:"recordId"`
ID string `json:"id"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
}
type AgentAccountModelReq struct {
AccountID uint `json:"accountId" validate:"required"`
}
type AgentAccountModelCreateReq struct {
AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"`
}
type AgentAccountModelUpdateReq struct {
AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"`
}
type AgentAccountModelDeleteReq struct {
AccountID uint `json:"accountId" validate:"required"`
RecordID uint `json:"recordId" validate:"required"`
}
type AgentAccountCreateReq struct {
Provider string `json:"provider" validate:"required"`
Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Remark string `json:"remark"`
Provider string `json:"provider" validate:"required"`
Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
Model string `json:"model"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Remark string `json:"remark"`
}
type AgentAccountUpdateReq struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
Model string `json:"model"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
}
type AgentAccountVerifyReq struct {
@@ -119,25 +150,30 @@ type AgentAccountSearch struct {
}
type AgentAccountInfo struct {
ID uint `json:"id"`
Provider string `json:"provider"`
ProviderName string `json:"providerName"`
Name string `json:"name"`
APIKey string `json:"apiKey"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseUrl"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
ID uint `json:"id"`
Provider string `json:"provider"`
ProviderName string `json:"providerName"`
Name string `json:"name"`
APIKey string `json:"apiKey"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseUrl"`
Model string `json:"model"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
}
type ProviderModelInfo struct {
ID string `json:"id"`
Name string `json:"name"`
ID string `json:"id"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
}
type ProviderInfo struct {
+1
View File
@@ -7,6 +7,7 @@ type AgentAccount struct {
APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"`
Model string `json:"model"`
Models string `json:"models" gorm:"type:text"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
+17
View File
@@ -0,0 +1,17 @@
package model
type AgentAccountModel struct {
BaseModel
AccountID uint `json:"accountId" gorm:"index"`
Model string `json:"model" gorm:"index"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input string `json:"input" gorm:"type:text"`
SortOrder int `json:"sortOrder" gorm:"index"`
}
func (AgentAccountModel) TableName() string {
return "agent_account_models"
}
+60 -3
View File
@@ -5,8 +5,12 @@ import (
)
type Model struct {
ID string
Name string
ID string
Name string
ContextWindow int
MaxTokens int
Reasoning bool
Input []string
}
type Meta struct {
@@ -264,7 +268,60 @@ func cloneMeta(meta Meta) Meta {
clone := meta
if len(meta.Models) > 0 {
clone.Models = make([]Model, len(meta.Models))
copy(clone.Models, meta.Models)
for i, item := range meta.Models {
clone.Models[i] = normalizeModel(meta.Key, item)
}
}
return clone
}
func normalizeModel(provider string, model Model) Model {
clone := model
clone.ID = strings.TrimSpace(clone.ID)
clone.Name = strings.TrimSpace(clone.Name)
if clone.Name == "" {
clone.Name = clone.ID
}
if clone.MaxTokens <= 0 || clone.ContextWindow <= 0 {
resolvedMaxTokens, resolvedContextWindow := catalogRuntimeDefaults(strings.ToLower(strings.TrimSpace(provider)))
if clone.MaxTokens <= 0 {
clone.MaxTokens = resolvedMaxTokens
}
if clone.ContextWindow <= 0 {
clone.ContextWindow = resolvedContextWindow
}
}
if len(clone.Input) == 0 {
clone.Input = defaultModelInputs(provider)
}
if !clone.Reasoning {
clone.Reasoning = isReasoningModel(clone.ID)
}
return clone
}
func defaultModelInputs(provider string) []string {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "kimi-coding":
return []string{"text", "image"}
default:
return []string{"text"}
}
}
func catalogRuntimeDefaults(provider string) (int, int) {
switch provider {
case "deepseek":
return 8192, 128000
case "zai":
return 131072, 204800
case "openrouter":
return 8192, 128000
case "minimax", "kimi-coding":
return 8192, 200000
case "custom", "vllm":
return 8192, 128000
default:
return 8192, 256000
}
}
+1 -5
View File
@@ -25,7 +25,7 @@ func BuildOpenClawPatch(provider, modelName, apiType string, maxTokens, contextW
case "deepseek":
return buildDeepseekPatch(modelName, baseURL, apiKey), nil
case "gemini":
return buildGeminiPatch(modelName), nil
return buildGenericPatch(provider, modelName, modelID, apiType, maxTokens, contextWindow, baseURL, apiKey), nil
case "moonshot", "kimi":
return buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey), nil
case "bailian-coding-plan":
@@ -47,10 +47,6 @@ func BuildOpenClawPatch(provider, modelName, apiType string, maxTokens, contextW
}
}
func buildGeminiPatch(modelName string) *OpenClawPatch {
return &OpenClawPatch{PrimaryModel: modelName}
}
func buildDeepseekPatch(modelName, baseURL, apiKey string) *OpenClawPatch {
return &OpenClawPatch{
PrimaryModel: modelName,
+50
View File
@@ -0,0 +1,50 @@
package repo
import "github.com/1Panel-dev/1Panel/agent/app/model"
type AgentAccountModelRepo struct{}
type IAgentAccountModelRepo interface {
List(opts ...DBOption) ([]model.AgentAccountModel, error)
GetFirst(opts ...DBOption) (*model.AgentAccountModel, error)
Create(item *model.AgentAccountModel) error
Save(item *model.AgentAccountModel) error
DeleteByID(id uint) error
Delete(opts ...DBOption) error
}
func NewIAgentAccountModelRepo() IAgentAccountModelRepo {
return &AgentAccountModelRepo{}
}
func (a AgentAccountModelRepo) List(opts ...DBOption) ([]model.AgentAccountModel, error) {
var list []model.AgentAccountModel
if err := getDb(opts...).Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (a AgentAccountModelRepo) GetFirst(opts ...DBOption) (*model.AgentAccountModel, error) {
var item model.AgentAccountModel
if err := getDb(opts...).First(&item).Error; err != nil {
return nil, err
}
return &item, nil
}
func (a AgentAccountModelRepo) Create(item *model.AgentAccountModel) error {
return getDb().Create(item).Error
}
func (a AgentAccountModelRepo) Save(item *model.AgentAccountModel) error {
return getDb().Save(item).Error
}
func (a AgentAccountModelRepo) DeleteByID(id uint) error {
return getDb().Delete(&model.AgentAccountModel{}, id).Error
}
func (a AgentAccountModelRepo) Delete(opts ...DBOption) error {
return getDb(opts...).Delete(&model.AgentAccountModel{}).Error
}
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -12,11 +12,12 @@ var (
appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
appIgnoreUpgradeRepo = repo.NewIAppIgnoreUpgradeRepo()
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
agentRepo = repo.NewIAgentRepo()
agentAccountRepo = repo.NewIAgentAccountRepo()
aiRepo = repo.NewIAiRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
tensorrtLLMRepo = repo.NewITensorRTLLMRepo()
agentRepo = repo.NewIAgentRepo()
agentAccountRepo = repo.NewIAgentAccountRepo()
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()
+2
View File
@@ -45,6 +45,8 @@ ErrAgentProviderNotSupported: 'Unsupported agent provider'
ErrAgentAccountRequired: 'Select an agent account first'
ErrAgentAccountNotVerified: 'Agent account not verified'
ErrAgentProviderMismatch: 'Agent provider mismatch'
ErrAgentModelNotInAccount: 'Model is not configured in the selected account'
ErrAgentModelInUse: 'A bound agent is still using this model: {{ .name }}'
ErrAgentBaseURLRequired: 'Base URL is required'
ErrAgentApiKeyRequired: 'API key is required'
ErrAgentComposeRequired: 'Compose content is required'
+2
View File
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
ErrAgentProviderMismatch: 'Proveedor de agente no coincide'
ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada'
ErrAgentModelInUse: 'Un agente vinculado todavía usa este modelo: {{ .name }}'
ErrAgentBaseURLRequired: 'URL base requerida'
ErrAgentApiKeyRequired: 'Clave API requerida'
ErrAgentComposeRequired: 'Contenido compose requerido'
+2
View File
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
ErrAgentAccountNotVerified: 'アカウント未検証です'
ErrAgentProviderMismatch: 'エージェントプロバイダが一致しません'
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}'
ErrAgentBaseURLRequired: 'ベースURLが必要です'
ErrAgentApiKeyRequired: 'APIキーが必要です'
ErrAgentComposeRequired: 'Composeが必要です'
+2
View File
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
ErrAgentAccountNotVerified: '에이전트 계정 미확인'
ErrAgentProviderMismatch: '에이전트 공급자 불일치'
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}'
ErrAgentBaseURLRequired: '기본 URL 필요'
ErrAgentApiKeyRequired: 'API 키 필요'
ErrAgentComposeRequired: 'Compose 내용 필요'
+2
View File
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
ErrAgentProviderMismatch: 'Penyedia ejen tidak sepadan'
ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih'
ErrAgentModelInUse: 'Masih ada ejen terikat yang menggunakan model ini: {{ .name }}'
ErrAgentBaseURLRequired: 'URL asas diperlukan'
ErrAgentApiKeyRequired: 'Kunci API diperlukan'
ErrAgentComposeRequired: 'Kandungan compose diperlukan'
+2
View File
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
ErrAgentAccountNotVerified: 'Conta de agente não verificada'
ErrAgentProviderMismatch: 'Provedor de agente diferente'
ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada'
ErrAgentModelInUse: 'Ainda há um agente vinculado usando este modelo: {{ .name }}'
ErrAgentBaseURLRequired: 'URL base obrigatória'
ErrAgentApiKeyRequired: 'Chave API obrigatória'
ErrAgentComposeRequired: 'Conteúdo de compose obrigatório'
+2
View File
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: 'Провайдер агента не поддер
ErrAgentAccountRequired: 'Выберите аккаунт агента'
ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
ErrAgentProviderMismatch: 'Провайдер не совпадает'
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}'
ErrAgentBaseURLRequired: 'Нужен базовый URL'
ErrAgentApiKeyRequired: 'Нужен API-ключ'
ErrAgentComposeRequired: 'Нужен compose'
+2
View File
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
ErrAgentProviderMismatch: 'Ajans sağlayıcısı eşleşmiyor'
ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı'
ErrAgentModelInUse: 'Bu modeli hâlâ kullanan bağlı bir ajan var: {{ .name }}'
ErrAgentBaseURLRequired: 'Temel URL gerekli'
ErrAgentApiKeyRequired: 'API anahtarı gerekli'
ErrAgentComposeRequired: 'Compose içeriği gerekli'
+2
View File
@@ -40,6 +40,8 @@ ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試'
ErrAgentAccountRequired: '請選擇智能體帳號後重試'
ErrAgentAccountNotVerified: '帳號未驗證通過,請重試'
ErrAgentProviderMismatch: '帳號提供商不符,請重試'
ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試'
ErrAgentModelInUse: '仍有智能體正在使用該模型:{{ .name }}'
ErrAgentBaseURLRequired: 'Base URL 不可為空,請重試'
ErrAgentApiKeyRequired: 'API Key 不可為空,請重試'
ErrAgentComposeRequired: '自訂編排內容不可為空,請重試'
+2
View File
@@ -45,6 +45,8 @@ ErrAgentProviderNotSupported: "不支持该智能体提供商"
ErrAgentAccountRequired: "请选择智能体账号后重试"
ErrAgentAccountNotVerified: "账号未通过验证"
ErrAgentProviderMismatch: "账号提供商不匹配"
ErrAgentModelNotInAccount: "所选模型未配置在当前账号中"
ErrAgentModelInUse: "仍有智能体正在使用该模型: {{ .name }}"
ErrAgentBaseURLRequired: "Base URL 不能为空"
ErrAgentApiKeyRequired: "API Key 不能为空"
ErrAgentComposeRequired: "自定义编排内容不能为空"
+1
View File
@@ -74,6 +74,7 @@ func InitAgentDB() {
migrations.NormalizeAgentAccountVerifiedStatus,
migrations.NormalizeOllamaAccountAPIType,
migrations.RewriteOpenclawBundledCaddyfile,
migrations.InitAgentAccountModelPool,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+10
View File
@@ -1102,3 +1102,13 @@ var RewriteOpenclawBundledCaddyfile = &gormigrate.Migration{
return migrationutils.RewriteOpenclawBundledCaddyfile(tx)
},
}
var InitAgentAccountModelPool = &gormigrate.Migration{
ID: "20260319-init-agent-account-model-pool",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.AgentAccountModel{}); err != nil {
return err
}
return migrationutils.MigrateAgentAccountModelPool(tx)
},
}
@@ -0,0 +1,104 @@
package utils
import (
"encoding/json"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/service"
"gorm.io/gorm"
)
func MigrateAgentAccountModelPool(tx *gorm.DB) error {
var accounts []model.AgentAccount
if err := tx.Find(&accounts).Error; err != nil {
return err
}
for _, account := range accounts {
count := int64(0)
if err := tx.Model(&model.AgentAccountModel{}).Where("account_id = ?", account.ID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
continue
}
models, err := buildMigratedAgentAccountModels(tx, &account)
if err != nil {
return err
}
if len(models) == 0 {
continue
}
for index, item := range models {
inputPayload, err := json.Marshal(item.Input)
if err != nil {
return err
}
record := &model.AgentAccountModel{
AccountID: account.ID,
Model: item.ID,
Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: string(inputPayload),
SortOrder: index + 1,
}
if err := tx.Create(record).Error; err != nil {
return err
}
}
}
return nil
}
func buildMigratedAgentAccountModels(tx *gorm.DB, account *model.AgentAccount) ([]dto.AgentAccountModel, error) {
if account == nil {
return nil, nil
}
requested := make([]dto.AgentAccountModel, 0)
if strings.TrimSpace(account.Models) != "" {
if err := json.Unmarshal([]byte(account.Models), &requested); err != nil {
return nil, err
}
}
seen := make(map[string]struct{}, len(requested))
for _, item := range requested {
target := strings.TrimSpace(item.ID)
if target == "" {
continue
}
seen[target] = struct{}{}
}
appendModel := func(modelID string) {
target := strings.TrimSpace(modelID)
if target == "" {
return
}
if _, ok := seen[target]; ok {
return
}
seen[target] = struct{}{}
requested = append(requested, dto.AgentAccountModel{ID: target})
}
appendModel(account.Model)
if account.ID > 0 {
var agents []model.Agent
if err := tx.Where("account_id = ?", account.ID).Find(&agents).Error; err != nil {
return nil, err
}
for _, agent := range agents {
appendModel(agent.Model)
}
}
models, err := service.MergeCatalogAgentAccountModelsForMigration(account, requested)
if err != nil {
if strings.TrimSpace(err.Error()) == "model is required" {
return nil, nil
}
return nil, err
}
return models, nil
}
+4
View File
@@ -49,6 +49,10 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)
aiToolsRouter.POST("/agents/accounts/update", baseApi.UpdateAgentAccount)
aiToolsRouter.POST("/agents/accounts/search", baseApi.PageAgentAccounts)
aiToolsRouter.POST("/agents/accounts/models", baseApi.GetAgentAccountModels)
aiToolsRouter.POST("/agents/accounts/models/create", baseApi.CreateAgentAccountModel)
aiToolsRouter.POST("/agents/accounts/models/update", baseApi.UpdateAgentAccountModel)
aiToolsRouter.POST("/agents/accounts/models/delete", baseApi.DeleteAgentAccountModel)
aiToolsRouter.POST("/agents/accounts/verify", baseApi.VerifyAgentAccount)
aiToolsRouter.POST("/agents/accounts/delete", baseApi.DeleteAgentAccount)
aiToolsRouter.POST("/agents/channel/feishu/get", baseApi.GetAgentFeishuConfig)
+34 -2
View File
@@ -309,9 +309,42 @@ export namespace AI {
model: string;
}
export interface AgentAccountModel {
recordId: number;
id: string;
name: string;
contextWindow: number;
maxTokens: number;
reasoning: boolean;
input: string[];
}
export interface AgentAccountModelReq {
accountId: number;
}
export interface AgentAccountModelCreateReq {
accountId: number;
model: AgentAccountModel;
}
export interface AgentAccountModelUpdateReq {
accountId: number;
model: AgentAccountModel;
}
export interface AgentAccountModelDeleteReq {
accountId: number;
recordId: number;
}
export interface ProviderModelInfo {
id: string;
name: string;
contextWindow: number;
maxTokens: number;
reasoning: boolean;
input: string[];
}
export interface ProviderInfo {
@@ -327,7 +360,6 @@ export namespace AI {
apiKey: string;
rememberApiKey: boolean;
baseURL: string;
model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
@@ -340,7 +372,6 @@ export namespace AI {
apiKey: string;
rememberApiKey: boolean;
baseURL: string;
model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
@@ -364,6 +395,7 @@ export namespace AI {
rememberApiKey: boolean;
baseUrl: string;
model: string;
models: AgentAccountModel[];
apiType: string;
maxTokens: number;
contextWindow: number;
+16
View File
@@ -129,6 +129,22 @@ export const pageAgentAccounts = (req: AI.AgentAccountSearch) => {
return http.post<ResPage<AI.AgentAccountItem>>(`/ai/agents/accounts/search`, req);
};
export const getAgentAccountModels = (req: AI.AgentAccountModelReq) => {
return http.post<AI.AgentAccountModel[]>(`/ai/agents/accounts/models`, req);
};
export const createAgentAccountModel = (req: AI.AgentAccountModelCreateReq) => {
return http.post(`/ai/agents/accounts/models/create`, req);
};
export const updateAgentAccountModel = (req: AI.AgentAccountModelUpdateReq) => {
return http.post(`/ai/agents/accounts/models/update`, req);
};
export const deleteAgentAccountModel = (req: AI.AgentAccountModelDeleteReq) => {
return http.post(`/ai/agents/accounts/models/delete`, req);
};
export const verifyAgentAccount = (req: AI.AgentAccountVerifyReq) => {
return http.post(`/ai/agents/accounts/verify`, req);
};
+11
View File
@@ -688,6 +688,17 @@ const message = {
provider: 'Provider',
apiKey: 'API Key',
baseUrl: 'Base URL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
accountCreateHelper:
'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Manual input',
verified: 'Verified',
+11
View File
@@ -696,6 +696,17 @@ const message = {
provider: 'Proveedor de modelos',
apiKey: 'Clave API',
baseUrl: 'URL base',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
accountCreateHelper:
'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Entrada manual de modelo',
verified: 'Verificado',
+11
View File
@@ -689,6 +689,17 @@ const message = {
provider: 'モデルプロバイダー',
apiKey: 'API キー',
baseUrl: 'ベースURL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
accountCreateHelper:
'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'トークン',
manualModel: '手動入力',
verified: '検証済み',
+11
View File
@@ -681,6 +681,17 @@ const message = {
provider: '모델 제공자',
apiKey: 'API ',
baseUrl: '기본 URL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
accountCreateHelper:
'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: '토큰',
manualModel: '수동 입력',
verified: '검증됨',
+11
View File
@@ -696,6 +696,17 @@ const message = {
provider: 'Penyedia model',
apiKey: 'Kunci API',
baseUrl: 'URL asas',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
accountCreateHelper:
'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Input manual',
verified: 'Disahkan',
+11
View File
@@ -691,6 +691,17 @@ const message = {
provider: 'Provedor de modelos',
apiKey: 'Chave API',
baseUrl: 'URL base',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
accountCreateHelper:
'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Entrada manual',
verified: 'Verificado',
+11
View File
@@ -688,6 +688,17 @@ const message = {
provider: 'Поставщик моделей',
apiKey: 'API ключ',
baseUrl: 'Базовый URL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
accountCreateHelper:
'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Токен',
manualModel: 'Ручной ввод',
verified: 'Проверено',
+11
View File
@@ -692,6 +692,17 @@ const message = {
provider: 'Model sağlayıcı',
apiKey: 'API anahtarı',
baseUrl: 'Temel URL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
accountCreateHelper:
'After creating the model account, you can continue managing its model pool. If the provider catalog already defines Models, they will be imported automatically.',
modelPool: 'Model Pool',
modelPoolHelper:
'Manage the models exposed by this account here. Agent creation and OpenClaw model switching both use this pool.',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Manuel giriş',
verified: 'Doğrulandı',
+9
View File
@@ -656,6 +656,15 @@ const message = {
provider: '模型供應商',
apiKey: 'API Key',
baseUrl: 'Base URL',
accountModels: '模型池',
accountModelsHelper: '配置該帳號可提供給 OpenClaw 使用與切換的模型列表',
accountModelsRequired: '請至少配置一個模型',
accountModelsDuplicate: '模型池中存在重複模型請檢查後重試',
accountCreateHelper: '建立模型帳號後可繼續修改模型池如果供應商 catalog 已提供 Models將自動匯入模型池',
modelPool: '模型池',
modelPoolHelper: '在此維護該模型帳號的模型池建立智能體與 OpenClaw 模型切換都會使用這些模型',
modelInputTypes: '輸入類型',
reasoning: '推理模型',
token: 'Token',
manualModel: '手動輸入模型',
verified: '驗證狀態',
+9
View File
@@ -655,6 +655,15 @@ const message = {
provider: '模型供应商',
apiKey: 'API Key',
baseUrl: 'Base URL',
accountModels: '模型池',
accountModelsHelper: '配置账号可提供给 OpenClaw 使用和切换的模型列表',
accountModelsRequired: '请至少配置一个模型',
accountModelsDuplicate: '模型池中存在重复模型请检查后重试',
accountCreateHelper: '创建模型账号后可继续修改模型池如果供应商 catalog 已提供 Models将自动导入模型池',
modelPool: '模型池',
modelPoolHelper: '在这里维护该模型账号的模型池智能体创建和 OpenClaw 模型切换都会使用这些模型',
modelInputTypes: '输入类型',
reasoning: '推理模型',
token: 'Token',
manualModel: '手动输入模型',
verified: '验证状态',
@@ -70,15 +70,11 @@
</el-button>
</span>
</el-form-item>
<el-form-item>
<el-checkbox v-model="manualModel">{{ $t('aiTools.agents.manualModel') }}</el-checkbox>
</el-form-item>
<el-form-item :label="$t('aiTools.model.model')" prop="model">
<el-input v-if="manualModel" v-model="form.model" />
<el-select v-else v-model="form.model" filterable @change="handleModelChange">
<el-select v-model="form.model" filterable>
<el-option v-for="item in filteredModels" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<span class="input-help">{{ $t('aiTools.agents.accountModelsHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.baseUrl')" v-if="form.accountId" prop="baseURL">
<el-input v-model="form.baseURL" disabled />
@@ -134,7 +130,6 @@ const accountOptions = ref<AI.AgentAccountItem[]>([]);
const providerOptions = ref<Array<{ label: string; value: string }>>([]);
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
const providerAccountCount = ref<Record<string, number>>({});
const manualModel = ref(false);
const appInfo = ref<App.AppDTO>();
const accountAddRef = ref();
const systemIP = ref('');
@@ -199,7 +194,10 @@ const rules = reactive({
specifyIP: [Rules.ipv4orV6],
});
const filteredModels = computed(() => providerModels.value[form.provider] || []);
const filteredModels = computed(() => {
const selected = accountOptions.value.find((item) => item.id === form.accountId);
return selected?.models || [];
});
const syncAllowedOriginsWithDefault = (force = false) => {
if (form.agentType !== 'openclaw') {
@@ -316,7 +314,6 @@ const handleProviderChange = () => {
form.baseURL = '';
form.accountId = undefined as unknown as number;
loadAccounts();
setDefaultModel();
};
const handleAgentTypeChange = async () => {
@@ -345,12 +342,6 @@ const handleAgentTypeChange = async () => {
await loadVersions('copaw');
};
const handleModelChange = () => {
if (manualModel.value) {
return;
}
};
const handleAccountChange = () => {
if (form.agentType !== 'openclaw') {
return;
@@ -362,8 +353,8 @@ const handleAccountChange = () => {
form.apiType = selected.apiType || 'openai-completions';
form.maxTokens = selected.maxTokens || 8192;
form.contextWindow = selected.contextWindow || 128000;
if ((selected.provider === 'custom' || selected.provider === 'vllm') && selected.model && !manualModel.value) {
form.model = selected.model;
if (!selected.models?.some((item) => item.id === form.model)) {
form.model = selected.models?.[0]?.id || '';
}
}
setDefaultModel();
@@ -373,19 +364,13 @@ const setDefaultModel = () => {
if (form.agentType !== 'openclaw') {
return;
}
if (manualModel.value) {
return;
}
const models = filteredModels.value;
if (models.length > 0 && !form.model) {
form.model = models[0].id;
return;
}
if (form.provider === 'custom' || form.provider === 'vllm') {
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if (selected?.model && !form.model) {
form.model = selected.model;
}
if (models.length === 0) {
form.model = '';
}
};
@@ -458,7 +443,6 @@ const openDrawer = async (agentType?: 'openclaw' | 'copaw') => {
const targetType = agentType === 'copaw' ? 'copaw' : 'openclaw';
form.name = targetType === 'copaw' ? 'CoPaw' : 'OpenClaw';
open.value = true;
manualModel.value = false;
form.agentType = targetType;
form.token = getRandomStr(32).toLowerCase();
if (form.agentType === 'copaw') {
@@ -5,19 +5,11 @@
<el-option v-for="item in accountOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.manualModel" @change="handleManualModelChange">
{{ t('aiTools.agents.manualModel') }}
</el-checkbox>
</el-form-item>
<el-form-item :label="t('aiTools.model.model')" prop="model">
<el-input v-if="form.manualModel && isOllamaProvider" v-model="ollamaModelSuffix">
<template #prepend>ollama/</template>
</el-input>
<el-input v-else-if="form.manualModel" v-model="form.model" />
<el-select v-else v-model="form.model" filterable>
<el-select v-model="form.model" filterable>
<el-option v-for="item in modelOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<span class="input-help">{{ t('aiTools.agents.accountModelsHelper') }}</span>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="saveModel">
@@ -28,11 +20,11 @@
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import { getAgentProviders, pageAgentAccounts, updateAgentModelConfig } from '@/api/modules/ai';
import { pageAgentAccounts, updateAgentModelConfig } from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
import { useGlobalStore } from '@/composables/useGlobalStore';
@@ -47,59 +39,19 @@ const { isIntl } = useGlobalStore();
const blockedProviders = new Set(['ark-coding-plan', 'bailian-coding-plan']);
const agentId = ref(0);
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
const accountOptions = ref<AI.AgentAccountItem[]>([]);
const modelOptions = ref<AI.ProviderModelInfo[]>([]);
const provdier = ref('');
const getModelPrefix = (provider: string) => {
switch (provider) {
case 'gemini':
return 'google';
default:
return provider;
}
};
const modelOptions = ref<AI.AgentAccountModel[]>([]);
const form = reactive({
accountId: undefined as unknown as number,
manualModel: false,
model: '',
});
const isOllamaProvider = computed(() => provdier.value === 'ollama');
const ollamaModelSuffix = computed({
get: () => {
const model = form.model || '';
if (model.startsWith('ollama/')) {
return model.slice('ollama/'.length);
}
return model;
},
set: (value: string) => {
form.model = `ollama/${value || ''}`;
},
});
const rules = reactive({
accountId: [Rules.requiredSelect],
model: [Rules.requiredInput],
model: [Rules.requiredSelect],
});
const loadProviders = async () => {
if (Object.keys(providerModels.value).length > 0) {
return;
}
const res = await getAgentProviders();
const data = res.data || [];
const filteredData = isIntl.value ? data.filter((item) => !blockedProviders.has(item.provider)) : data;
providerModels.value = filteredData.reduce((acc, item) => {
acc[item.provider] = item.models || [];
return acc;
}, {} as Record<string, AI.ProviderModelInfo[]>);
};
const loadAccounts = async () => {
const res = await pageAgentAccounts({
page: 1,
@@ -111,66 +63,22 @@ const loadAccounts = async () => {
accountOptions.value = isIntl.value ? items.filter((item) => !blockedProviders.has(item.provider)) : items;
};
const setModelsByProvider = (provider: string) => {
modelOptions.value = providerModels.value[provider] || [];
const setModelOptionsByAccount = (accountId: number) => {
const selected = accountOptions.value.find((item) => item.id === accountId);
modelOptions.value = selected?.models || [];
if (!modelOptions.value.some((item) => item.id === form.model)) {
form.model = modelOptions.value[0]?.id || '';
}
};
const handleAccountChange = () => {
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if (!selected) {
modelOptions.value = [];
form.model = '';
return;
}
provdier.value = selected.provider;
if (selected.provider === 'custom' || selected.provider === 'vllm') {
form.manualModel = true;
form.model = selected.model || form.model;
modelOptions.value = [];
return;
}
if (selected.provider === 'ollama') {
form.manualModel = true;
if (!form.model.startsWith('ollama/')) {
form.model = 'ollama/';
}
setModelsByProvider(selected.provider);
return;
}
const modelPrefix = getModelPrefix(selected.provider);
setModelsByProvider(selected.provider);
if (!form.manualModel && (!form.model || !form.model.startsWith(`${modelPrefix}/`))) {
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
}
};
const handleManualModelChange = (val: unknown) => {
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if ((selected?.provider === 'custom' || selected?.provider === 'vllm') && !Boolean(val)) {
form.manualModel = true;
return;
}
if (selected?.provider === 'ollama' && Boolean(val) && !form.model.startsWith('ollama/')) {
form.model = 'ollama/';
}
if (Boolean(val)) {
return;
}
if (!selected) {
form.model = '';
return;
}
const modelPrefix = getModelPrefix(selected.provider);
if (!form.model || !form.model.startsWith(`${modelPrefix}/`)) {
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
}
setModelOptionsByAccount(form.accountId);
};
const load = async (agent: AI.AgentItem) => {
loading.value = true;
try {
agentId.value = agent.id;
await loadProviders();
await loadAccounts();
if (accountOptions.value.length === 0) {
form.accountId = undefined as unknown as number;
@@ -181,30 +89,8 @@ const load = async (agent: AI.AgentItem) => {
const currentAccount =
accountOptions.value.find((item) => item.id === agent.accountId) || accountOptions.value[0];
form.accountId = currentAccount.id;
provdier.value = currentAccount.provider;
setModelsByProvider(currentAccount.provider);
const modelPrefix = getModelPrefix(currentAccount.provider);
const inProviderModels = modelOptions.value.some((item) => item.id === agent.model);
form.manualModel =
currentAccount.provider === 'custom' ||
currentAccount.provider === 'vllm' ||
currentAccount.provider === 'ollama' ||
!inProviderModels;
if (agent.model && (form.manualModel || agent.model.startsWith(`${modelPrefix}/`))) {
form.model = agent.model;
} else {
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
}
if (
(currentAccount.provider === 'custom' || currentAccount.provider === 'vllm') &&
currentAccount.model &&
!form.model
) {
form.model = currentAccount.model;
}
if (currentAccount.provider === 'ollama' && !form.model.startsWith('ollama/')) {
form.model = 'ollama/';
}
form.model = agent.model || currentAccount.models?.[0]?.id || '';
setModelOptionsByAccount(currentAccount.id);
} finally {
loading.value = false;
}
@@ -5,7 +5,7 @@
<el-input v-model="form.name" />
</el-form-item>
<el-form-item :label="$t('aiTools.agents.provider')" prop="provider">
<el-select v-model="form.provider" @change="handleProviderChange" :disabled="form.id > 0">
<el-select v-model="form.provider" :disabled="form.id > 0" @change="handleProviderChange">
<el-option
v-for="item in providerOptions"
:key="item.value"
@@ -26,18 +26,14 @@
<el-form-item :label="$t('aiTools.agents.baseUrl')" prop="baseURL">
<el-input v-model="form.baseURL" :disabled="!editableBaseURLProviders.includes(form.provider)" />
</el-form-item>
<el-form-item
:label="$t('aiTools.model.model')"
prop="model"
v-if="form.provider === 'custom' || form.provider === 'vllm'"
>
<el-input v-model="form.model" placeholder="gpt-4o-mini" />
</el-form-item>
<el-form-item
:label="'API ' + $t('commons.table.type')"
prop="apiType"
v-if="form.provider === 'custom' || form.provider === 'vllm' || form.provider === 'ollama'"
>
<el-alert
:title="$t('aiTools.agents.accountCreateHelper')"
type="info"
:closable="false"
show-icon
class="mb-4"
/>
<el-form-item :label="'API ' + $t('commons.table.type')" prop="apiType">
<el-select v-model="form.apiType">
<el-option label="openai-completions" value="openai-completions" />
<el-option label="openai-responses" value="openai-responses" />
@@ -48,18 +44,10 @@
/>
</el-select>
</el-form-item>
<el-form-item
label="Max Tokens"
prop="maxTokens"
v-if="form.provider === 'custom' || form.provider === 'vllm'"
>
<el-form-item label="Max Tokens" prop="maxTokens">
<el-input-number v-model="form.maxTokens" :min="1" :max="2000000" />
</el-form-item>
<el-form-item
label="Context Window"
prop="contextWindow"
v-if="form.provider === 'custom' || form.provider === 'vllm'"
>
<el-form-item label="Context Window" prop="contextWindow">
<el-input-number v-model="form.contextWindow" :min="1" :max="2000000" />
</el-form-item>
<el-form-item :label="$t('website.remark')" prop="remark">
@@ -88,6 +76,7 @@ import { Rules } from '@/global/form-rules';
import { createAgentAccount, getAgentProviders, updateAgentAccount } from '@/api/modules/ai';
import i18n from '@/lang';
import { getAgentProviderDisplayName } from '@/utils/agent';
import { MsgError } from '@/utils/message';
import { useGlobalStore } from '@/composables/useGlobalStore';
const emit = defineEmits(['search']);
@@ -105,7 +94,6 @@ const form = reactive({
provider: '',
name: '',
baseURL: '',
model: '',
apiType: 'openai-completions',
maxTokens: 8192,
contextWindow: 128000,
@@ -124,7 +112,6 @@ const rules = reactive({
name: [Rules.requiredInput],
apiKey: [Rules.requiredInput],
baseURL: [Rules.requiredInput],
model: [Rules.requiredInput],
apiType: [Rules.requiredSelect],
});
@@ -142,7 +129,6 @@ const submit = async () => {
baseURL: form.baseURL,
apiKey: form.apiKey,
rememberApiKey: form.rememberApiKey,
model: form.model,
apiType: form.apiType,
maxTokens: form.maxTokens,
contextWindow: form.contextWindow,
@@ -156,7 +142,6 @@ const submit = async () => {
baseURL: form.baseURL,
apiKey: form.apiKey,
rememberApiKey: form.rememberApiKey,
model: form.model,
apiType: form.apiType,
maxTokens: form.maxTokens,
contextWindow: form.contextWindow,
@@ -165,6 +150,8 @@ const submit = async () => {
}
emit('search');
open.value = false;
} catch (error: any) {
MsgError(String(error?.message || i18n.global.t('commons.res.commonError')));
} finally {
loading.value = false;
}
@@ -174,11 +161,15 @@ const handleClose = () => {
formRef.value?.resetFields();
loading.value = false;
form.id = 0;
form.model = '';
form.provider = '';
form.name = '';
form.baseURL = '';
form.apiType = 'openai-completions';
form.maxTokens = 8192;
form.contextWindow = 128000;
form.apiKey = '';
form.rememberApiKey = false;
form.remark = '';
form.syncAgents = false;
};
@@ -189,7 +180,6 @@ interface OpenParams {
baseURL?: string;
apiKey?: string;
rememberApiKey?: boolean;
model?: string;
apiType?: string;
maxTokens?: number;
contextWindow?: number;
@@ -199,6 +189,9 @@ interface OpenParams {
const openDrawer = async (params?: OpenParams) => {
open.value = true;
loading.value = false;
if (providerOptions.value.length === 0) {
await loadProviders();
}
if (params?.id) {
form.id = params.id;
form.provider = params.provider || '';
@@ -206,7 +199,6 @@ const openDrawer = async (params?: OpenParams) => {
form.baseURL = params.baseURL || '';
form.apiKey = params.apiKey || '';
form.rememberApiKey = params.rememberApiKey || false;
form.model = params.model || '';
form.apiType = params.apiType || 'openai-completions';
form.maxTokens = params.maxTokens || 8192;
form.contextWindow = params.contextWindow || 128000;
@@ -219,15 +211,11 @@ const openDrawer = async (params?: OpenParams) => {
form.baseURL = '';
form.apiKey = '';
form.rememberApiKey = false;
form.model = '';
form.apiType = 'openai-completions';
form.maxTokens = 8192;
form.contextWindow = 128000;
form.remark = '';
form.syncAgents = false;
if (providerOptions.value.length === 0) {
await loadProviders();
}
if (params?.provider) {
form.provider = params.provider;
} else if (providerOptions.value.length > 0) {
@@ -259,20 +247,14 @@ const handleProviderChange = () => {
if (form.provider === 'custom' || form.provider === 'vllm') {
form.baseURL = '';
form.apiType = form.apiType || 'openai-completions';
form.maxTokens = form.maxTokens || 8192;
form.contextWindow = form.contextWindow || 128000;
form.model = form.model || '';
return;
}
if (form.provider === 'ollama') {
} else if (form.provider === 'ollama') {
form.baseURL = '';
form.apiType = 'openai-completions';
return;
}
if (form.provider !== 'ollama') {
form.baseURL = providerBaseURL.value[form.provider] || '';
form.apiType = 'openai-responses';
} else {
form.baseURL = '';
form.baseURL = providerBaseURL.value[form.provider] || '';
if (!form.apiType) {
form.apiType = 'openai-completions';
}
}
};
+19 -2
View File
@@ -36,11 +36,17 @@
width="180"
:formatter="dateFormat"
/>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" />
<fu-table-operations
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
width="180"
/>
</ComplexTable>
</template>
</LayoutContent>
<AddDialog ref="addRef" @search="search" />
<ModelPoolDialog ref="modelPoolRef" @updated="search" />
</div>
</template>
@@ -50,6 +56,7 @@ import { deleteAgentAccount, pageAgentAccounts } from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import RouterMenu from '@/views/ai/agents/index.vue';
import AddDialog from '@/views/ai/agents/model/add/index.vue';
import ModelPoolDialog from '@/views/ai/agents/model/pool/index.vue';
import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { dateFormat } from '@/utils/util';
@@ -57,6 +64,7 @@ import { getAgentProviderDisplayName } from '@/utils/agent';
const items = ref<AI.AgentAccountItem[]>([]);
const addRef = ref();
const modelPoolRef = ref();
const searchName = ref('');
const buttons = [
@@ -64,6 +72,10 @@ const buttons = [
label: i18n.global.t('commons.button.edit'),
click: (row: AI.AgentAccountItem) => onEdit(row),
},
{
label: i18n.global.t('aiTools.agents.modelPool'),
click: (row: AI.AgentAccountItem) => onManageModelPool(row),
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: AI.AgentAccountItem) => onDelete(row),
@@ -105,7 +117,6 @@ const onEdit = (row: AI.AgentAccountItem) => {
baseURL: row.baseUrl,
apiKey: row.apiKey,
rememberApiKey: row.rememberApiKey,
model: row.model,
apiType: row.apiType,
maxTokens: row.maxTokens,
contextWindow: row.contextWindow,
@@ -114,6 +125,12 @@ const onEdit = (row: AI.AgentAccountItem) => {
}
};
const onManageModelPool = (row: AI.AgentAccountItem) => {
if (modelPoolRef.value?.open) {
modelPoolRef.value.open(row);
}
};
const isVerificationSkipped = (provider: string) => {
const key = (provider || '').toLowerCase();
return key === 'custom' || key === 'vllm' || key === 'ollama' || key === 'kimi-coding';
@@ -0,0 +1,338 @@
<template>
<DrawerPro v-model="open" :header="headerTitle" size="large" @close="handleClose">
<template #content>
<div v-loading="loading">
<el-alert
:title="$t('aiTools.agents.modelPoolHelper')"
type="info"
:closable="false"
show-icon
class="mb-4"
/>
<div class="toolbar">
<el-button type="primary" @click="openCreate">
{{ $t('commons.button.add') }}
</el-button>
</div>
<ComplexTable :data="items">
<el-table-column :label="$t('aiTools.model.model')" prop="id" min-width="220" />
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="180" />
<el-table-column label="Context Window" prop="contextWindow" min-width="140" />
<el-table-column label="Max Tokens" prop="maxTokens" min-width="120" />
<el-table-column :label="$t('aiTools.agents.modelInputTypes')" min-width="140">
<template #default="{ row }">
{{ (row.input || []).join(', ') || '-' }}
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.agents.reasoning')" min-width="120">
<template #default="{ row }">
<el-tag :type="row.reasoning ? 'success' : 'info'">
{{ row.reasoning ? $t('commons.true') : $t('commons.false') }}
</el-tag>
</template>
</el-table-column>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" />
</ComplexTable>
</div>
</template>
<template #footer>
<span class="dialog-footer">
<el-button @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</DrawerPro>
<DrawerPro v-model="editorOpen" :header="editorTitle" size="normal" @close="handleEditorClose">
<el-form ref="formRef" :model="form" label-position="top" :rules="rules" v-loading="saving">
<el-form-item :label="$t('aiTools.model.model')" prop="id">
<el-select
v-model="form.id"
filterable
allow-create
default-first-option
:reserve-keyword="false"
@change="handleModelIDChange"
>
<el-option
v-for="model in providerModelOptions"
:key="model.id"
:label="model.name"
:value="model.id"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="Context Window" prop="contextWindow">
<el-input-number v-model="form.contextWindow" :min="1" :max="2000000" />
</el-form-item>
<el-form-item label="Max Tokens" prop="maxTokens">
<el-input-number v-model="form.maxTokens" :min="1" :max="2000000" />
</el-form-item>
<el-form-item :label="$t('aiTools.agents.modelInputTypes')" prop="input">
<el-checkbox-group v-model="form.input">
<el-checkbox label="text">Text</el-checkbox>
<el-checkbox label="image">Image</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.reasoning')" prop="reasoning">
<el-switch v-model="form.reasoning" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="saving" @click="editorOpen = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="saving" type="primary" @click="submit">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DrawerPro>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import {
createAgentAccountModel,
deleteAgentAccountModel,
getAgentAccountModels,
getAgentProviders,
updateAgentAccountModel,
} from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import i18n from '@/lang';
import { MsgError } from '@/utils/message';
import { getAgentProviderDisplayName } from '@/utils/agent';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { Rules } from '@/global/form-rules';
const emit = defineEmits(['updated']);
const open = ref(false);
const editorOpen = ref(false);
const loading = ref(false);
const saving = ref(false);
const formRef = ref<FormInstance>();
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
const items = ref<AI.AgentAccountModel[]>([]);
const { isIntl } = useGlobalStore();
const account = reactive({
id: 0,
name: '',
provider: '',
});
const form = reactive<AI.AgentAccountModel>({
recordId: 0,
id: '',
name: '',
contextWindow: 128000,
maxTokens: 8192,
reasoning: false,
input: ['text'],
});
const headerTitle = computed(() => {
const accountName = account.name || getAgentProviderDisplayName(account.provider, account.provider);
return `${accountName} · ${i18n.global.t('aiTools.agents.modelPool')}`;
});
const editorTitle = computed(() =>
form.recordId ? i18n.global.t('commons.button.edit') : i18n.global.t('commons.button.add'),
);
const providerModelOptions = computed(() => providerModels.value[account.provider] || []);
const rules = reactive({
id: [Rules.requiredInput],
name: [Rules.requiredInput],
});
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: (row: AI.AgentAccountModel) => openEdit(row),
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: AI.AgentAccountModel) => onDelete(row),
},
];
const buildModelItem = (model?: Partial<AI.AgentAccountModel>): AI.AgentAccountModel => ({
recordId: Number(model?.recordId || 0),
id: String(model?.id || '').trim(),
name: String(model?.name || '').trim(),
contextWindow: Number(model?.contextWindow || 0) || 128000,
maxTokens: Number(model?.maxTokens || 0) || 8192,
reasoning: Boolean(model?.reasoning),
input: Array.from(new Set((model?.input || []).filter((value) => value === 'text' || value === 'image'))),
});
const loadProviders = async () => {
if (Object.keys(providerModels.value).length > 0) {
return;
}
const res = await getAgentProviders();
const data = res.data || [];
const blockedProviders = new Set(['ark-coding-plan', 'bailian-coding-plan']);
const filteredData = isIntl.value ? data.filter((item) => !blockedProviders.has(item.provider)) : data;
providerModels.value = filteredData.reduce((acc, item) => {
acc[item.provider] = item.models || [];
return acc;
}, {} as Record<string, AI.ProviderModelInfo[]>);
};
const search = async () => {
if (!account.id) {
items.value = [];
return;
}
loading.value = true;
try {
const res = await getAgentAccountModels({ accountId: account.id });
items.value = (res.data || []).map((item) => buildModelItem(item));
} finally {
loading.value = false;
}
};
const resetForm = () => {
form.recordId = 0;
form.id = '';
form.name = '';
form.contextWindow = 128000;
form.maxTokens = 8192;
form.reasoning = false;
form.input = ['text'];
};
const openCreate = () => {
resetForm();
const firstModel = providerModelOptions.value[0];
if (firstModel) {
form.id = firstModel.id;
form.name = firstModel.name || firstModel.id;
form.contextWindow = firstModel.contextWindow || 128000;
form.maxTokens = firstModel.maxTokens || 8192;
form.reasoning = firstModel.reasoning;
form.input = firstModel.input?.length ? [...firstModel.input] : ['text'];
}
editorOpen.value = true;
};
const openEdit = (row: AI.AgentAccountModel) => {
Object.assign(form, buildModelItem(row));
editorOpen.value = true;
};
const handleModelIDChange = (value: string) => {
form.id = String(value || '').trim();
const matched = providerModelOptions.value.find((item) => item.id === form.id);
if (!matched) {
return;
}
if (!form.name) {
form.name = matched.name || matched.id;
}
form.contextWindow = matched.contextWindow || form.contextWindow || 128000;
form.maxTokens = matched.maxTokens || form.maxTokens || 8192;
form.reasoning = matched.reasoning;
form.input = matched.input?.length ? [...matched.input] : ['text'];
};
const submit = async () => {
if (!account.id || !formRef.value) {
return;
}
await formRef.value.validate();
const duplicate = items.value.some((item) => item.id === form.id && item.recordId !== form.recordId);
if (duplicate) {
MsgError(i18n.global.t('aiTools.agents.accountModelsDuplicate'));
return;
}
if (!form.input?.length) {
form.input = ['text'];
}
saving.value = true;
try {
if (form.recordId) {
await updateAgentAccountModel({
accountId: account.id,
model: buildModelItem(form),
});
} else {
await createAgentAccountModel({
accountId: account.id,
model: buildModelItem(form),
});
}
editorOpen.value = false;
await search();
emit('updated');
} catch (error: any) {
MsgError(String(error?.message || i18n.global.t('commons.res.commonError')));
} finally {
saving.value = false;
}
};
const onDelete = async (row: AI.AgentAccountModel) => {
await ElMessageBox.confirm(
i18n.global.t('commons.msg.delete', [row.name || row.id]),
i18n.global.t('commons.button.delete'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
},
);
await deleteAgentAccountModel({
accountId: account.id,
recordId: row.recordId,
});
await search();
emit('updated');
};
const handleEditorClose = () => {
formRef.value?.resetFields();
resetForm();
saving.value = false;
};
const handleClose = () => {
loading.value = false;
account.id = 0;
account.name = '';
account.provider = '';
items.value = [];
editorOpen.value = false;
handleEditorClose();
};
const openDrawer = async (row: AI.AgentAccountItem) => {
account.id = row.id;
account.name = row.name;
account.provider = row.provider;
open.value = true;
await loadProviders();
await search();
};
defineExpose({
open: openDrawer,
});
</script>
<style lang="scss" scoped>
.toolbar {
display: flex;
justify-content: flex-start;
margin-bottom: 16px;
margin-top: 5px;
}
</style>