From 9eb7c06e73f4ebabb5ff160c06b13fa6599cc9cf Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <1paneldev@sina.com> Date: Mon, 16 Mar 2026 18:29:20 +0800 Subject: [PATCH 1/3] feat: openclaw supports configuring multiple models --- agent/app/api/v2/agents.go | 81 ++ agent/app/dto/agents.go | 110 ++- agent/app/model/agent_account.go | 1 + agent/app/model/agent_account_model.go | 17 + agent/app/provider/catalog.go | 63 +- agent/app/provider/openclaw.go | 6 +- agent/app/repo/agent_account_model.go | 50 + agent/app/service/agents.go | 912 ++++++++++++++++-- .../app/service/agents_account_models_test.go | 187 ++++ agent/app/service/entry.go | 11 +- agent/i18n/lang/en.yaml | 1 + agent/i18n/lang/es-ES.yaml | 1 + agent/i18n/lang/ja.yaml | 1 + agent/i18n/lang/ko.yaml | 1 + agent/i18n/lang/ms.yaml | 1 + agent/i18n/lang/pt-BR.yaml | 1 + agent/i18n/lang/ru.yaml | 1 + agent/i18n/lang/tr.yaml | 1 + agent/i18n/lang/zh-Hant.yaml | 1 + agent/i18n/lang/zh.yaml | 1 + agent/init/migration/migrate.go | 1 + agent/init/migration/migrations/init.go | 10 + .../utils/agent_account_model_pool.go | 104 ++ agent/router/ro_ai.go | 4 + frontend/src/api/interface/ai.ts | 36 +- frontend/src/api/modules/ai.ts | 16 + frontend/src/lang/modules/en.ts | 11 + frontend/src/lang/modules/es-es.ts | 11 + frontend/src/lang/modules/ja.ts | 11 + frontend/src/lang/modules/ko.ts | 11 + frontend/src/lang/modules/ms.ts | 11 + frontend/src/lang/modules/pt-br.ts | 11 + frontend/src/lang/modules/ru.ts | 11 + frontend/src/lang/modules/tr.ts | 11 + frontend/src/lang/modules/zh-Hant.ts | 9 + frontend/src/lang/modules/zh.ts | 9 + .../src/views/ai/agents/agent/add/index.vue | 36 +- .../ai/agents/agent/config/tabs/model.vue | 144 +-- .../src/views/ai/agents/model/add/index.vue | 74 +- frontend/src/views/ai/agents/model/index.vue | 21 +- .../src/views/ai/agents/model/pool/index.vue | 338 +++++++ 41 files changed, 2003 insertions(+), 335 deletions(-) create mode 100644 agent/app/model/agent_account_model.go create mode 100644 agent/app/repo/agent_account_model.go create mode 100644 agent/app/service/agents_account_models_test.go create mode 100644 agent/init/migration/migrations/utils/agent_account_model_pool.go create mode 100644 frontend/src/views/ai/agents/model/pool/index.vue diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index 65cb7181c0fa..c1c40840c71a 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -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 diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 743bf0485aba..7d3e19a5a1bf 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -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 { diff --git a/agent/app/model/agent_account.go b/agent/app/model/agent_account.go index 1280a8b391fb..e2d9dd5e1944 100644 --- a/agent/app/model/agent_account.go +++ b/agent/app/model/agent_account.go @@ -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"` diff --git a/agent/app/model/agent_account_model.go b/agent/app/model/agent_account_model.go new file mode 100644 index 000000000000..bd09e9225911 --- /dev/null +++ b/agent/app/model/agent_account_model.go @@ -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" +} diff --git a/agent/app/provider/catalog.go b/agent/app/provider/catalog.go index 5279e36f52e7..9f1d729e2cdf 100644 --- a/agent/app/provider/catalog.go +++ b/agent/app/provider/catalog.go @@ -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 + } +} diff --git a/agent/app/provider/openclaw.go b/agent/app/provider/openclaw.go index 1a446fea3073..449fcc8130f9 100644 --- a/agent/app/provider/openclaw.go +++ b/agent/app/provider/openclaw.go @@ -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, diff --git a/agent/app/repo/agent_account_model.go b/agent/app/repo/agent_account_model.go new file mode 100644 index 000000000000..02d0b5e8da49 --- /dev/null +++ b/agent/app/repo/agent_account_model.go @@ -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 +} diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index 038e500a5e5a..bad7b33d1ccf 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -29,6 +29,7 @@ import ( openclawutil "github.com/1Panel-dev/1Panel/agent/utils/openclaw" "github.com/1Panel-dev/1Panel/agent/utils/req_helper" "github.com/1Panel-dev/1Panel/agent/utils/xpack" + "gorm.io/gorm" ) type AgentService struct{} @@ -44,6 +45,10 @@ type IAgentService interface { UpdateAccount(req dto.AgentAccountUpdateReq) error SyncAgentsByAccountID(accountID uint) error PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) + GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) + CreateAccountModel(req dto.AgentAccountModelCreateReq) error + UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error + DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error VerifyAccount(req dto.AgentAccountVerifyReq) error DeleteAccount(req dto.AgentAccountDeleteReq) error GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) @@ -171,32 +176,34 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" { return nil, buserr.New("ErrAgentApiKeyRequired") } - apiType, maxTokens, contextWindow = resolveRuntimeParams(provider, account.APIType, account.MaxTokens, account.ContextWindow) - runtimeModel = strings.TrimSpace(req.Model) - if runtimeModel == "" { - return nil, buserr.New("ErrAgentProviderMismatch") + accountModels, err := loadAgentAccountModels(account) + if err != nil { + return nil, err } - if provider == "custom" || provider == "vllm" { - customModelID := normalizeCustomModel(req.Model) - runtimeModel = "custom/" + customModelID + storedModel = strings.TrimSpace(req.Model) + if storedModel == "" { + storedModel = strings.TrimSpace(account.Model) } - if provider == "bailian-coding-plan" { - modelID := runtimeModel - if parts := strings.SplitN(runtimeModel, "/", 2); len(parts) == 2 { - modelID = parts[1] - } - normalizedID := normalizeBailianCodingPlanModelID(modelID) - runtimeModel = "bailian-coding-plan/" + bailianPrimaryModelID(normalizedID) + if storedModel == "" && len(accountModels) > 0 { + storedModel = strings.TrimSpace(accountModels[0].ID) } - if provider == "ark-coding-plan" { - modelID := runtimeModel - if parts := strings.SplitN(runtimeModel, "/", 2); len(parts) == 2 { - modelID = parts[1] - } - normalizedID := normalizeArkCodingPlanModelID(modelID) - runtimeModel = "ark-coding-plan/" + normalizedID + if storedModel == "" { + return nil, buserr.New("ErrAgentModelNotInAccount") + } + selectedAccountModel, ok := findAgentAccountModel(accountModels, storedModel) + if !ok { + return nil, buserr.New("ErrAgentModelNotInAccount") + } + apiType, maxTokens, contextWindow = resolveRuntimeParams( + provider, + account.APIType, + selectedAccountModel.MaxTokens, + selectedAccountModel.ContextWindow, + ) + runtimeModel, err = buildOpenclawPrimaryModel(account, storedModel) + if err != nil { + return nil, err } - storedModel = req.Model apiKey = account.APIKey accountID = account.ID token = strings.TrimSpace(req.Token) @@ -279,13 +286,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { if agentType == constant.AppOpenclaw { go a.writeConfigWithRetry( appInstall, - provider, - req.Model, - apiType, - maxTokens, - contextWindow, - baseURL, - apiKey, + accountID, + storedModel, token, agent.ID, allowedOrigins, @@ -421,7 +423,20 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" { return buserr.New("ErrAgentApiKeyRequired") } - apiType, maxTokens, contextWindow := resolveRuntimeParams(provider, account.APIType, account.MaxTokens, account.ContextWindow) + accountModels, err := loadAgentAccountModels(account) + if err != nil { + return err + } + if _, ok := findAgentAccountModel(accountModels, modelName); !ok { + return buserr.New("ErrAgentModelNotInAccount") + } + selectedAccountModel, _ := findAgentAccountModel(accountModels, modelName) + apiType, maxTokens, contextWindow := resolveRuntimeParams( + provider, + account.APIType, + selectedAccountModel.MaxTokens, + selectedAccountModel.ContextWindow, + ) confDir := "" if agent.ConfigPath != "" { confDir = path.Dir(agent.ConfigPath) @@ -434,7 +449,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error if confDir == "" { return buserr.New("ErrRecordNotFound") } - if err := writeOpenclawConfig(confDir, provider, modelName, apiType, maxTokens, contextWindow, baseURL, account.APIKey, agent.Token, nil); err != nil { + if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil); err != nil { return err } agent.Provider = provider @@ -496,9 +511,6 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { modelName := strings.TrimSpace(req.Model) apiType := normalizeAPIType(req.APIType) if provider == "custom" || provider == "vllm" { - if modelName == "" { - return fmt.Errorf("model is required") - } if !isSupportedAPIType(apiType) { return fmt.Errorf("apiType is invalid") } @@ -520,20 +532,31 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { RememberAPIKey: req.RememberAPIKey, BaseURL: baseURL, Model: "", + Models: "", APIType: apiType, MaxTokens: 0, ContextWindow: 0, Verified: verified, Remark: req.Remark, } - if provider == "custom" || provider == "vllm" { - account.Model = normalizeCustomModel(modelName) + if provider == "custom" || provider == "vllm" || provider == "ollama" { account.MaxTokens = maxTokens account.ContextWindow = contextWindow } if err := agentAccountRepo.Create(account); err != nil { return err } + initialModels, err := buildInitialAgentAccountModels(account, req.Models, modelName) + if err != nil { + _ = agentAccountRepo.DeleteByID(account.ID) + return err + } + if len(initialModels) > 0 { + if err := replacePersistedAgentAccountModels(account.ID, initialModels); err != nil { + _ = agentAccountRepo.DeleteByID(account.ID) + return err + } + } asyncReportAIProviderInstall(provider) return nil } @@ -561,9 +584,6 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { } apiType := normalizeAPIType(req.APIType) rawAPIType := strings.TrimSpace(req.APIType) - if (provider == "custom" || provider == "vllm") && strings.TrimSpace(req.Model) == "" { - return fmt.Errorf("model is required") - } if (provider == "custom" || provider == "vllm") && !isSupportedAPIType(apiType) { return fmt.Errorf("apiType is invalid") } @@ -585,23 +605,35 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { return err } verified := !providercatalog.SkipVerification(provider) - account.Name = req.Name + account.Provider = provider account.APIKey = req.APIKey - account.RememberAPIKey = req.RememberAPIKey account.BaseURL = baseURL - if provider == "custom" || provider == "vllm" { - account.Model = normalizeCustomModel(req.Model) - } account.APIType = apiType - if provider == "custom" || provider == "vllm" { + account.MaxTokens = 0 + account.ContextWindow = 0 + if provider == "custom" || provider == "vllm" || provider == "ollama" { account.MaxTokens = maxTokens account.ContextWindow = contextWindow } + account.Name = req.Name + account.APIKey = req.APIKey + account.RememberAPIKey = req.RememberAPIKey + account.BaseURL = baseURL + account.APIType = apiType account.Remark = req.Remark account.Verified = verified if err := agentAccountRepo.Save(account); err != nil { return err } + if len(req.Models) > 0 || strings.TrimSpace(req.Model) != "" { + accountModels, _, err := normalizeAgentAccountModels(account, req.Models, req.Model, true) + if err != nil { + return err + } + if err := replacePersistedAgentAccountModels(account.ID, accountModels); err != nil { + return err + } + } if req.SyncAgents { if err := a.syncAgentsByAccount(account); err != nil { return err @@ -636,7 +668,8 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age APIKey: apiKey, RememberAPIKey: item.RememberAPIKey, BaseURL: item.BaseURL, - Model: item.Model, + Model: "", + Models: nil, APIType: item.APIType, MaxTokens: item.MaxTokens, ContextWindow: item.ContextWindow, @@ -645,9 +678,119 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age CreatedAt: item.CreatedAt, }) } + for i := range items { + models, err := loadAgentAccountModels(&list[i]) + if err != nil { + return 0, nil, err + } + items[i].Models = models + } return count, items, nil } +func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) { + account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) + if err != nil { + return nil, err + } + return loadAgentAccountModels(account) +} + +func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error { + account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) + if err != nil { + return err + } + models, err := loadAgentAccountModels(account) + if err != nil { + return err + } + normalized, err := normalizeAgentAccountModel(account, req.Model) + if err != nil { + return err + } + if _, ok := findAgentAccountModel(models, normalized.ID); ok { + return buserr.New("ErrRecordExist") + } + inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(normalized.Input)) + if err != nil { + return err + } + sortOrder := len(models) + 1 + record := &model.AgentAccountModel{ + AccountID: account.ID, + Model: normalized.ID, + Name: normalized.Name, + ContextWindow: normalized.ContextWindow, + MaxTokens: normalized.MaxTokens, + Reasoning: normalized.Reasoning, + Input: string(inputPayload), + SortOrder: sortOrder, + } + if err := agentAccountModelRepo.Create(record); err != nil { + return err + } + return a.syncAgentsByAccount(account) +} + +func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error { + account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) + if err != nil { + return err + } + record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.Model.RecordID), repo.WithByAccountID(req.AccountID)) + if err != nil { + return err + } + models, err := loadAgentAccountModels(account) + if err != nil { + return err + } + normalized, err := normalizeAgentAccountModel(account, req.Model) + if err != nil { + return err + } + for _, item := range models { + if item.RecordID == req.Model.RecordID { + continue + } + if item.ID == normalized.ID { + return buserr.New("ErrRecordExist") + } + } + inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(normalized.Input)) + if err != nil { + return err + } + record.Model = normalized.ID + record.Name = normalized.Name + record.ContextWindow = normalized.ContextWindow + record.MaxTokens = normalized.MaxTokens + record.Reasoning = normalized.Reasoning + record.Input = string(inputPayload) + if err := agentAccountModelRepo.Save(record); err != nil { + return err + } + return a.syncAgentsByAccount(account) +} + +func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error { + account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) + if err != nil { + return err + } + if _, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)); err != nil { + return err + } + if err := agentAccountModelRepo.DeleteByID(req.RecordID); err != nil { + return err + } + if err := compactPersistedAgentAccountModelSortOrder(req.AccountID); err != nil { + return err + } + return a.syncAgentsByAccount(account) +} + func (a AgentService) SyncAgentsByAccountID(accountID uint) error { if accountID == 0 { return nil @@ -693,6 +836,9 @@ func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error { if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 { return buserr.New("ErrAgentAccountBound") } + if err := agentAccountModelRepo.Delete(repo.WithByAccountID(req.ID)); err != nil { + return err + } return agentAccountRepo.DeleteByID(req.ID) } @@ -1519,12 +1665,14 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { if err != nil { return err } - baseURL := strings.TrimSpace(account.BaseURL) - if baseURL == "" { - if defaultURL, ok := providerDefaultBaseURL(account.Provider); ok { - baseURL = defaultURL - } + accountModels, err := loadAgentAccountModels(account) + if err != nil { + return err + } + if len(accountModels) == 0 { + return nil } + baseURL := resolveAccountBaseURL(account) for _, agent := range agents { confDir := "" if agent.ConfigPath != "" { @@ -1538,12 +1686,25 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { if confDir == "" { continue } - apiType, maxTokens, contextWindow := resolveRuntimeParams(account.Provider, account.APIType, account.MaxTokens, account.ContextWindow) modelName := agent.Model - if strings.EqualFold(account.Provider, "vllm") && strings.TrimSpace(account.Model) != "" { - modelName = account.Model - } - if err := writeOpenclawConfig(confDir, account.Provider, modelName, apiType, maxTokens, contextWindow, baseURL, account.APIKey, agent.Token, nil); err != nil { + if strings.TrimSpace(modelName) == "" { + modelName = strings.TrimSpace(account.Model) + } + if strings.TrimSpace(modelName) == "" { + modelName = accountModels[0].ID + } + selectedAccountModel, ok := findAgentAccountModel(accountModels, modelName) + if !ok { + modelName = accountModels[0].ID + selectedAccountModel, _ = findAgentAccountModel(accountModels, modelName) + } + apiType, maxTokens, contextWindow := resolveRuntimeParams( + account.Provider, + account.APIType, + selectedAccountModel.MaxTokens, + selectedAccountModel.ContextWindow, + ) + if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil); err != nil { return err } agent.BaseURL = baseURL @@ -1661,7 +1822,7 @@ func migrateOpenclawHTTPSUpgradeWithSystemIP(install *model.AppInstall, fromVers } } } - return migrateOpenclawInstallEnv(install, allowedOrigins) + return writeOpenclawCaddyfile(configPath, []string{allowedOrigin}) } func migrateOpenclawInstallPorts(install *model.AppInstall) { @@ -1793,7 +1954,7 @@ func (a AgentService) waitAndDeleteAgent(agentID uint, appInstallID uint) { } } -func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string, agentID uint, allowedOrigins []string) { +func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, accountID uint, modelName, token string, agentID uint, allowedOrigins []string) { if appInstall == nil { return } @@ -1806,7 +1967,12 @@ func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provide time.Sleep(time.Second) } confDir := path.Join(appInstall.GetPath(), "data", "conf") - if err := writeOpenclawConfig(confDir, provider, modelName, apiType, maxTokens, contextWindow, baseURL, apiKey, token, allowedOrigins); err != nil { + account, err := agentAccountRepo.GetFirst(repo.WithByID(accountID)) + if err != nil { + global.LOG.Errorf("load agent account failed: %v", err) + return + } + if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins); err != nil { global.LOG.Errorf("write openclaw config failed: %v", err) agent, errGet := agentRepo.GetFirst(repo.WithByID(agentID)) if errGet == nil && agent != nil { @@ -1880,8 +2046,9 @@ type agentsConfig struct { } type agentDefaults struct { - UserTimezone string `json:"userTimezone,omitempty"` - Model modelRef `json:"model"` + UserTimezone string `json:"userTimezone,omitempty"` + Model modelRef `json:"model"` + Models map[string]map[string]interface{} `json:"models,omitempty"` } type modelRef struct { @@ -1925,10 +2092,13 @@ type browserConfig struct { DefaultProfile string `json:"defaultProfile"` } -func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string, allowedOrigins []string) error { +func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string) error { if strings.TrimSpace(confDir) == "" { return fmt.Errorf("config dir is required") } + if account == nil { + return fmt.Errorf("account is required") + } if strings.TrimSpace(modelName) == "" { return fmt.Errorf("model is required") } @@ -1941,6 +2111,10 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens return err } } + primaryModel, defaultsModels, models, err := buildOpenclawModelsFromAccount(account, modelName) + if err != nil { + return err + } cfg := openclawConfig{ Gateway: gatewayConfig{ @@ -1960,7 +2134,8 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens Agents: agentsConfig{ Defaults: agentDefaults{ UserTimezone: resolveServerTimezone(), - Model: modelRef{Primary: modelName}, + Model: modelRef{Primary: primaryModel}, + Models: defaultsModels, }, }, Browser: browserConfig{ @@ -1979,20 +2154,7 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens Update: updateConfig{ CheckOnStart: false, }, - } - - resolvedAPIType, resolvedMaxTokens, resolvedContextWindow := resolveRuntimeParams(provider, apiType, maxTokens, contextWindow) - patch, err := providercatalog.BuildOpenClawPatch(provider, modelName, resolvedAPIType, resolvedMaxTokens, resolvedContextWindow, baseURL, apiKey) - if err != nil { - return err - } - cfg.Agents.Defaults.Model.Primary = patch.PrimaryModel - if patch.Models != nil { - modelsMap, err := mapToModelsConfig(patch.Models) - if err != nil { - return err - } - cfg.Models = modelsMap + Models: models, } configPath := path.Join(confDir, "openclaw.json") @@ -2040,6 +2202,9 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens } modelMap := ensureChildMap(defaultsMap, "model") modelMap["primary"] = cfg.Agents.Defaults.Model.Primary + if cfg.Agents.Defaults.Models != nil { + defaultsMap["models"] = cfg.Agents.Defaults.Models + } ensureGatewaySecurityDefaults(conf) gatewayMap := ensureChildMap(conf, "gateway") @@ -2066,13 +2231,589 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens } envPath := path.Join(confDir, ".env") lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)} - if envKey := providerEnvKey(provider); envKey != "" && strings.TrimSpace(apiKey) != "" { - lines = append(lines, fmt.Sprintf("%s=%s", envKey, apiKey)) + if envKey := providerEnvKey(account.Provider); envKey != "" && strings.TrimSpace(account.APIKey) != "" { + lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey)) } content := strings.Join(lines, "\n") + "\n" return fileOp.SaveFile(envPath, content, 0600) } +func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel string) (string, map[string]map[string]interface{}, *modelsConfig, error) { + accountModels, err := loadAgentAccountModels(account) + if err != nil { + return "", nil, nil, err + } + if len(accountModels) == 0 { + return "", nil, nil, fmt.Errorf("model is required") + } + selectedModel = strings.TrimSpace(selectedModel) + if selectedModel == "" { + selectedModel = strings.TrimSpace(account.Model) + } + if selectedModel == "" { + selectedModel = strings.TrimSpace(accountModels[0].ID) + } + if selectedModel == "" { + return "", nil, nil, fmt.Errorf("model is required") + } + + providerKey := "" + providerCfg := modelProvider{} + entries := make([]modelEntry, 0, len(accountModels)) + primaryModel := "" + defaultsModels := make(map[string]map[string]interface{}, len(accountModels)) + for _, item := range accountModels { + resolvedPrimary, entry, key, baseCfg, err := buildOpenclawCatalogModel(account, item) + if err != nil { + return "", nil, nil, err + } + if providerKey == "" { + providerKey = key + providerCfg.ApiKey = baseCfg.ApiKey + providerCfg.BaseUrl = baseCfg.BaseUrl + providerCfg.Api = baseCfg.Api + } + entries = append(entries, entry) + defaultsModels[resolvedPrimary] = map[string]interface{}{} + if strings.TrimSpace(item.ID) == selectedModel { + primaryModel = resolvedPrimary + } + } + if primaryModel == "" { + return "", nil, nil, buserr.New("ErrAgentModelNotInAccount") + } + providerCfg.Models = entries + return primaryModel, defaultsModels, &modelsConfig{ + Mode: "merge", + Providers: map[string]modelProvider{ + providerKey: providerCfg, + }, + }, nil +} + +func buildOpenclawCatalogModel(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) { + primaryModel, inferredEntry, providerKey, providerCfg, err := inferOpenclawCatalogModel(account, model.ID, model.MaxTokens, model.ContextWindow) + if err != nil { + return "", modelEntry{}, "", modelProvider{}, err + } + if strings.TrimSpace(model.Name) != "" { + inferredEntry.Name = strings.TrimSpace(model.Name) + } + if len(model.Input) > 0 { + inferredEntry.Input = sanitizeAgentAccountModelInputs(model.Input) + } + inferredEntry.Reasoning = model.Reasoning + if model.ContextWindow > 0 { + inferredEntry.ContextWindow = model.ContextWindow + } + if model.MaxTokens > 0 { + inferredEntry.MaxTokens = model.MaxTokens + } + return primaryModel, inferredEntry, providerKey, providerCfg, nil +} + +func buildOpenclawPrimaryModel(account *model.AgentAccount, modelID string) (string, error) { + models, err := loadAgentAccountModels(account) + if err != nil { + return "", err + } + item, ok := findAgentAccountModel(models, modelID) + if !ok { + return "", buserr.New("ErrAgentModelNotInAccount") + } + primaryModel, _, _, _, err := inferOpenclawCatalogModel(account, item.ID, item.MaxTokens, item.ContextWindow) + if err != nil { + return "", err + } + return primaryModel, nil +} + +func inferOpenclawCatalogModel(account *model.AgentAccount, modelID string, maxTokens, contextWindow int) (string, modelEntry, string, modelProvider, error) { + baseURL := resolveAccountBaseURL(account) + resolvedAPIType, resolvedMaxTokens, resolvedContextWindow := resolveRuntimeParams(account.Provider, account.APIType, maxTokens, contextWindow) + patch, err := providercatalog.BuildOpenClawPatch(account.Provider, modelID, resolvedAPIType, resolvedMaxTokens, resolvedContextWindow, baseURL, account.APIKey) + if err != nil { + return "", modelEntry{}, "", modelProvider{}, err + } + if patch.Models == nil { + return "", modelEntry{}, "", modelProvider{}, fmt.Errorf("models patch is required") + } + modelsCfg, err := mapToModelsConfig(patch.Models) + if err != nil { + return "", modelEntry{}, "", modelProvider{}, err + } + for key, providerCfg := range modelsCfg.Providers { + if len(providerCfg.Models) == 0 { + continue + } + return patch.PrimaryModel, providerCfg.Models[0], key, modelProvider{ + ApiKey: providerCfg.ApiKey, + BaseUrl: providerCfg.BaseUrl, + Api: providerCfg.Api, + }, nil + } + return "", modelEntry{}, "", modelProvider{}, fmt.Errorf("models patch is invalid") +} + +func resolveAccountBaseURL(account *model.AgentAccount) string { + baseURL := strings.TrimSpace(account.BaseURL) + if baseURL == "" { + if defaultURL, ok := providerDefaultBaseURL(account.Provider); ok { + baseURL = defaultURL + } + } + return baseURL +} + +func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto.AgentAccountModel, legacyModel string) ([]dto.AgentAccountModel, error) { + if account == nil { + return nil, fmt.Errorf("account is required") + } + if len(requested) > 0 || strings.TrimSpace(legacyModel) != "" { + models, _, err := normalizeAgentAccountModels(account, requested, legacyModel, true) + if err != nil { + return nil, err + } + return models, nil + } + meta, ok := providercatalog.Get(account.Provider) + if !ok || len(meta.Models) == 0 { + return nil, nil + } + requested = make([]dto.AgentAccountModel, 0, len(meta.Models)) + for _, item := range meta.Models { + requested = append(requested, dto.AgentAccountModel{ + ID: item.ID, + Name: item.Name, + ContextWindow: item.ContextWindow, + MaxTokens: item.MaxTokens, + Reasoning: item.Reasoning, + Input: append([]string(nil), item.Input...), + }) + } + models, _, err := normalizeAgentAccountModels(account, requested, "", true) + if err != nil { + return nil, err + } + return models, nil +} + +func compactPersistedAgentAccountModelSortOrder(accountID uint) error { + rows, err := agentAccountModelRepo.List(repo.WithByAccountID(accountID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id")) + if err != nil { + return err + } + for index := range rows { + order := index + 1 + if rows[index].SortOrder == order { + continue + } + rows[index].SortOrder = order + if err := agentAccountModelRepo.Save(&rows[index]); err != nil { + return err + } + } + return nil +} + +func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountModel, error) { + models, err := ensurePersistedAgentAccountModels(account) + if err != nil { + return nil, err + } + return models, nil +} + +func LoadLegacyAgentAccountModelsForMigration(account *model.AgentAccount) ([]dto.AgentAccountModel, error) { + if account == nil { + return nil, fmt.Errorf("account is required") + } + if !hasLegacyAgentAccountModels(account) { + return nil, nil + } + models, _, err := loadLegacyAgentAccountModelCatalog(account) + if err != nil { + if strings.TrimSpace(err.Error()) == "model is required" { + return nil, nil + } + return nil, err + } + return models, nil +} + +func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) { + if account == nil { + return nil, fmt.Errorf("account is required") + } + meta, ok := providercatalog.Get(account.Provider) + if !ok || len(meta.Models) == 0 { + return append([]dto.AgentAccountModel(nil), existing...), nil + } + requested := append([]dto.AgentAccountModel(nil), existing...) + seen := make(map[string]struct{}, len(existing)) + for _, item := range existing { + if strings.TrimSpace(item.ID) == "" { + continue + } + seen[strings.TrimSpace(item.ID)] = struct{}{} + } + for _, item := range meta.Models { + if _, ok := seen[strings.TrimSpace(item.ID)]; ok { + continue + } + requested = append(requested, dto.AgentAccountModel{ + ID: item.ID, + Name: item.Name, + ContextWindow: item.ContextWindow, + MaxTokens: item.MaxTokens, + Reasoning: item.Reasoning, + Input: append([]string(nil), item.Input...), + }) + } + if len(requested) == len(existing) { + return append([]dto.AgentAccountModel(nil), existing...), nil + } + normalized, _, err := normalizeAgentAccountModels(account, requested, "", true) + if err != nil { + return nil, err + } + return normalized, nil +} + +func hasLegacyAgentAccountModels(account *model.AgentAccount) bool { + if account == nil { + return false + } + if strings.TrimSpace(account.Models) != "" || strings.TrimSpace(account.Model) != "" { + return true + } + if account.ID > 0 { + if agents, err := agentRepo.List(repo.WithByAccountID(account.ID)); err == nil { + for _, agent := range agents { + if strings.TrimSpace(agent.Model) != "" { + return true + } + } + } + } + if definitions, ok := providerDefinitions()[strings.ToLower(strings.TrimSpace(account.Provider))]; ok { + return len(definitions.Models) > 0 + } + return false +} + +func ensurePersistedAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountModel, error) { + if account == nil { + return nil, fmt.Errorf("account is required") + } + models, err := listPersistedAgentAccountModels(account.ID) + if err != nil { + return nil, err + } + if len(models) > 0 { + return models, nil + } + if !hasLegacyAgentAccountModels(account) { + return nil, nil + } + legacyModels, _, err := loadLegacyAgentAccountModelCatalog(account) + if err != nil { + if strings.TrimSpace(err.Error()) == "model is required" { + return nil, nil + } + return nil, err + } + if len(legacyModels) == 0 { + return nil, nil + } + if account.ID == 0 { + return legacyModels, nil + } + if err := replacePersistedAgentAccountModels(account.ID, legacyModels); err != nil { + return nil, err + } + return listPersistedAgentAccountModels(account.ID) +} + +func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, error) { + if accountID == 0 { + return nil, nil + } + rows, err := agentAccountModelRepo.List(repo.WithByAccountID(accountID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id")) + if err != nil { + return nil, err + } + result := make([]dto.AgentAccountModel, 0, len(rows)) + for _, row := range rows { + inputs := []string{} + if strings.TrimSpace(row.Input) != "" { + _ = json.Unmarshal([]byte(row.Input), &inputs) + } + result = append(result, dto.AgentAccountModel{ + RecordID: row.ID, + ID: strings.TrimSpace(row.Model), + Name: strings.TrimSpace(row.Name), + ContextWindow: row.ContextWindow, + MaxTokens: row.MaxTokens, + Reasoning: row.Reasoning, + Input: sanitizeAgentAccountModelInputs(inputs), + }) + } + return result, nil +} + +func replacePersistedAgentAccountModels(accountID uint, models []dto.AgentAccountModel) error { + return global.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("account_id = ?", accountID).Delete(&model.AgentAccountModel{}).Error; err != nil { + return err + } + for index, item := range models { + inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(item.Input)) + if err != nil { + return err + } + record := &model.AgentAccountModel{ + AccountID: accountID, + Model: strings.TrimSpace(item.ID), + Name: strings.TrimSpace(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 loadLegacyAgentAccountModelCatalog(account *model.AgentAccount) ([]dto.AgentAccountModel, string, error) { + if account == nil { + return nil, "", fmt.Errorf("account is required") + } + models, err := parseAgentAccountModels(account.Models) + if err != nil { + return nil, "", err + } + normalized, _, err := normalizeAgentAccountModels(account, models, account.Model, true) + if err != nil { + return nil, "", err + } + _, defaultModel, err := normalizeAgentAccountModels(account, normalized, account.Model, true) + if err != nil { + return nil, "", err + } + return normalized, defaultModel, nil +} + +func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.AgentAccountModel, defaultModel string, allowFallbackDefault bool) ([]dto.AgentAccountModel, string, error) { + requested := append([]dto.AgentAccountModel(nil), models...) + if len(requested) == 0 { + if strings.TrimSpace(defaultModel) != "" { + requested = []dto.AgentAccountModel{{ID: defaultModel}} + } else { + requested = buildLegacyAgentAccountModels(account) + } + } + normalized := make([]dto.AgentAccountModel, 0, len(requested)) + seen := make(map[string]struct{}, len(requested)) + for _, item := range requested { + normalizedItem, err := normalizeAgentAccountModel(account, item) + if err != nil { + return nil, "", err + } + if strings.TrimSpace(normalizedItem.ID) == "" { + continue + } + if _, ok := seen[normalizedItem.ID]; ok { + continue + } + seen[normalizedItem.ID] = struct{}{} + normalized = append(normalized, normalizedItem) + } + if len(normalized) == 0 { + return nil, "", fmt.Errorf("model is required") + } + + resolvedDefault := strings.TrimSpace(defaultModel) + if resolvedDefault != "" { + defaultItem, err := normalizeAgentAccountModel(account, dto.AgentAccountModel{ID: resolvedDefault}) + if err == nil { + resolvedDefault = defaultItem.ID + } + } + if resolvedDefault == "" && allowFallbackDefault { + resolvedDefault = normalized[0].ID + } + if _, ok := findAgentAccountModel(normalized, resolvedDefault); !ok { + if allowFallbackDefault { + resolvedDefault = normalized[0].ID + } else { + return nil, "", buserr.New("ErrAgentModelNotInAccount") + } + } + return normalized, resolvedDefault, nil +} + +func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAccountModel) (dto.AgentAccountModel, error) { + modelID := strings.TrimSpace(model.ID) + if modelID == "" { + return dto.AgentAccountModel{}, fmt.Errorf("model is required") + } + primaryModel, inferredEntry, _, _, err := inferOpenclawCatalogModel(account, modelID, model.MaxTokens, model.ContextWindow) + if err != nil { + return dto.AgentAccountModel{}, err + } + name := strings.TrimSpace(model.Name) + if name == "" { + name = strings.TrimSpace(inferredEntry.Name) + } + reasoning := model.Reasoning + if !model.Reasoning && model.Name == "" && model.MaxTokens == 0 && model.ContextWindow == 0 && len(model.Input) == 0 { + reasoning = inferredEntry.Reasoning + } + inputs := sanitizeAgentAccountModelInputs(model.Input) + if len(inputs) == 0 { + inputs = sanitizeAgentAccountModelInputs(inferredEntry.Input) + } + contextWindow := model.ContextWindow + if contextWindow <= 0 { + contextWindow = inferredEntry.ContextWindow + } + maxTokens := model.MaxTokens + if maxTokens <= 0 { + maxTokens = inferredEntry.MaxTokens + } + return dto.AgentAccountModel{ + ID: normalizeAgentAccountModelID(account.Provider, primaryModel, modelID), + Name: name, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + Reasoning: reasoning, + Input: inputs, + }, nil +} + +func normalizeAgentAccountModelID(provider, primaryModel, requestedID string) string { + switch strings.ToLower(strings.TrimSpace(provider)) { + case "custom", "vllm": + target := requestedID + if strings.TrimSpace(target) == "" { + target = primaryModel + } + return normalizeCustomModel(target) + case "ollama": + target := strings.TrimSpace(primaryModel) + if strings.HasPrefix(target, "ollama/") { + return target + } + target = strings.TrimSpace(requestedID) + if strings.HasPrefix(target, "ollama/") { + return target + } + target = strings.TrimLeft(strings.TrimSpace(target), "/") + if target == "" { + target = strings.TrimLeft(strings.TrimSpace(primaryModel), "/") + } + if target == "" { + return "" + } + return "ollama/" + target + default: + if strings.TrimSpace(primaryModel) != "" { + return strings.TrimSpace(primaryModel) + } + return strings.TrimSpace(requestedID) + } +} + +func buildLegacyAgentAccountModels(account *model.AgentAccount) []dto.AgentAccountModel { + modelIDs := make([]string, 0, 4) + seen := make(map[string]struct{}, 4) + appendModel := func(value string) { + target := strings.TrimSpace(value) + if target == "" { + return + } + if _, ok := seen[target]; ok { + return + } + seen[target] = struct{}{} + modelIDs = append(modelIDs, target) + } + appendModel(account.Model) + if account.ID > 0 { + if agents, err := agentRepo.List(repo.WithByAccountID(account.ID)); err == nil { + for _, agent := range agents { + appendModel(agent.Model) + } + } + } + if definitions, ok := providerDefinitions()[strings.ToLower(strings.TrimSpace(account.Provider))]; ok && len(definitions.Models) > 0 { + for _, item := range definitions.Models { + appendModel(item.ID) + } + } + models := make([]dto.AgentAccountModel, 0, len(modelIDs)) + for _, modelID := range modelIDs { + models = append(models, dto.AgentAccountModel{ID: modelID}) + } + return models +} + +func parseAgentAccountModels(value string) ([]dto.AgentAccountModel, error) { + trim := strings.TrimSpace(value) + if trim == "" { + return nil, nil + } + var models []dto.AgentAccountModel + if err := json.Unmarshal([]byte(trim), &models); err != nil { + return nil, err + } + return models, nil +} + +func marshalAgentAccountModels(models []dto.AgentAccountModel) (string, error) { + payload, err := json.Marshal(models) + if err != nil { + return "", err + } + return string(payload), nil +} + +func sanitizeAgentAccountModelInputs(values []string) []string { + result := make([]string, 0, len(values)) + seen := make(map[string]struct{}, len(values)) + for _, value := range values { + normalized := strings.ToLower(strings.TrimSpace(value)) + if normalized != "text" && normalized != "image" { + continue + } + if _, ok := seen[normalized]; ok { + continue + } + seen[normalized] = struct{}{} + result = append(result, normalized) + } + if len(result) == 0 { + return []string{"text"} + } + return result +} + +func findAgentAccountModel(models []dto.AgentAccountModel, modelID string) (dto.AgentAccountModel, bool) { + target := strings.TrimSpace(modelID) + for _, item := range models { + if strings.TrimSpace(item.ID) == target { + return item, true + } + } + return dto.AgentAccountModel{}, false +} + func resolveServerTimezone() string { timezone := strings.TrimSpace(common.LoadTimeZoneByCmd()) if timezone == "" { @@ -2136,7 +2877,14 @@ func providerDefinitions() map[string]providerDefinition { } models := make([]dto.ProviderModelInfo, 0, len(meta.Models)) for _, m := range meta.Models { - models = append(models, dto.ProviderModelInfo{ID: m.ID, Name: m.Name}) + models = append(models, dto.ProviderModelInfo{ + ID: m.ID, + Name: m.Name, + ContextWindow: m.ContextWindow, + MaxTokens: m.MaxTokens, + Reasoning: m.Reasoning, + Input: append([]string(nil), m.Input...), + }) } definitions[key] = providerDefinition{ Sort: meta.Sort, @@ -2281,6 +3029,10 @@ func providerModelPrefix(provider string) string { switch strings.ToLower(strings.TrimSpace(provider)) { case "gemini": return "google" + case "minimax": + return "minimax-portal" + case "kimi": + return "moonshot" default: return strings.ToLower(strings.TrimSpace(provider)) } diff --git a/agent/app/service/agents_account_models_test.go b/agent/app/service/agents_account_models_test.go new file mode 100644 index 000000000000..36f7ce25da63 --- /dev/null +++ b/agent/app/service/agents_account_models_test.go @@ -0,0 +1,187 @@ +package service + +import ( + "testing" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" +) + +func TestNormalizeAgentAccountModels(t *testing.T) { + account := &model.AgentAccount{ + Provider: "openai", + APIKey: "sk-test", + BaseURL: "https://api.openai.com/v1", + APIType: "openai-completions", + } + + models, defaultModel, err := normalizeAgentAccountModels(account, []dto.AgentAccountModel{ + {ID: "openai/gpt-4o"}, + }, "openai/gpt-4o", false) + if err != nil { + t.Fatalf("normalizeAgentAccountModels failed: %v", err) + } + if defaultModel != "openai/gpt-4o" { + t.Fatalf("unexpected default model: %s", defaultModel) + } + if len(models) != 1 { + t.Fatalf("unexpected models length: %d", len(models)) + } + if models[0].Name == "" { + t.Fatal("expected model name to be inferred") + } + if models[0].ContextWindow <= 0 || models[0].MaxTokens <= 0 { + t.Fatalf("expected runtime params to be filled: %+v", models[0]) + } + if len(models[0].Input) == 0 { + t.Fatal("expected model input types to be filled") + } +} + +func TestBuildOpenclawModelsFromAccount(t *testing.T) { + account := &model.AgentAccount{ + Provider: "openai", + APIKey: "sk-test", + BaseURL: "https://api.openai.com/v1", + APIType: "openai-completions", + } + models, _, err := normalizeAgentAccountModels(account, []dto.AgentAccountModel{ + {ID: "openai/gpt-4o"}, + {ID: "openai/gpt-4.1"}, + }, "openai/gpt-4o", false) + if err != nil { + t.Fatalf("normalizeAgentAccountModels failed: %v", err) + } + payload, err := marshalAgentAccountModels(models) + if err != nil { + t.Fatalf("marshalAgentAccountModels failed: %v", err) + } + account.Model = "openai/gpt-4o" + account.Models = payload + + primaryModel, defaultsModels, cfg, err := buildOpenclawModelsFromAccount(account, "openai/gpt-4.1") + if err != nil { + t.Fatalf("buildOpenclawModelsFromAccount failed: %v", err) + } + if primaryModel != "openai/gpt-4.1" { + t.Fatalf("unexpected primary model: %s", primaryModel) + } + if cfg == nil || cfg.Mode != "merge" { + t.Fatalf("unexpected models config: %+v", cfg) + } + if len(cfg.Providers) != 1 { + t.Fatalf("unexpected providers length: %d", len(cfg.Providers)) + } + if len(defaultsModels) != 2 { + t.Fatalf("unexpected defaults models length: %d", len(defaultsModels)) + } + for _, provider := range cfg.Providers { + if len(provider.Models) != 2 { + t.Fatalf("unexpected model entries length: %d", len(provider.Models)) + } + } +} + +func TestBuildOpenclawModelsFromAccountGemini(t *testing.T) { + account := &model.AgentAccount{ + Provider: "gemini", + APIKey: "gemini-key", + BaseURL: "https://generativelanguage.googleapis.com", + APIType: "openai-completions", + } + models, _, err := normalizeAgentAccountModels(account, []dto.AgentAccountModel{ + {ID: "google/gemini-flash-latest"}, + }, "google/gemini-flash-latest", false) + if err != nil { + t.Fatalf("normalizeAgentAccountModels failed: %v", err) + } + payload, err := marshalAgentAccountModels(models) + if err != nil { + t.Fatalf("marshalAgentAccountModels failed: %v", err) + } + account.Model = "google/gemini-flash-latest" + account.Models = payload + + primaryModel, defaultsModels, cfg, err := buildOpenclawModelsFromAccount(account, account.Model) + if err != nil { + t.Fatalf("buildOpenclawModelsFromAccount failed: %v", err) + } + if primaryModel != "google/gemini-flash-latest" { + t.Fatalf("unexpected primary model: %s", primaryModel) + } + if cfg == nil || len(cfg.Providers) != 1 { + t.Fatalf("unexpected models config: %+v", cfg) + } + if len(defaultsModels) != 1 { + t.Fatalf("unexpected defaults models length: %d", len(defaultsModels)) + } +} + +func TestLoadLegacyAgentAccountModelsForMigrationSkipsEmptyAccount(t *testing.T) { + account := &model.AgentAccount{ + Provider: "custom", + } + models, err := LoadLegacyAgentAccountModelsForMigration(account) + if err != nil { + t.Fatalf("LoadLegacyAgentAccountModelsForMigration failed: %v", err) + } + if len(models) != 0 { + t.Fatalf("expected no models, got %d", len(models)) + } +} + +func TestEnsurePersistedAgentAccountModelsSkipsEmptyLegacyAccount(t *testing.T) { + account := &model.AgentAccount{ + Provider: "ollama", + } + models, err := ensurePersistedAgentAccountModels(account) + if err != nil { + t.Fatalf("ensurePersistedAgentAccountModels failed: %v", err) + } + if len(models) != 0 { + t.Fatalf("expected no persisted models, got %d", len(models)) + } +} + +func TestLoadLegacyAgentAccountModelsForMigrationUsesCatalogModels(t *testing.T) { + account := &model.AgentAccount{ + Provider: "openai", + } + models, err := LoadLegacyAgentAccountModelsForMigration(account) + if err != nil { + t.Fatalf("LoadLegacyAgentAccountModelsForMigration failed: %v", err) + } + if len(models) < 2 { + t.Fatalf("expected catalog models to be migrated, got %d", len(models)) + } + if models[0].ID == "" { + t.Fatal("expected first catalog model id to be populated") + } +} + +func TestMergeCatalogAgentAccountModelsForMigration(t *testing.T) { + account := &model.AgentAccount{ + Provider: "deepseek", + APIKey: "sk-test", + BaseURL: "https://api.deepseek.com/v1", + APIType: "openai-completions", + } + merged, err := MergeCatalogAgentAccountModelsForMigration(account, []dto.AgentAccountModel{ + {ID: "deepseek/deepseek-chat"}, + }) + if err != nil { + t.Fatalf("MergeCatalogAgentAccountModelsForMigration failed: %v", err) + } + if len(merged) != 3 { + t.Fatalf("expected 3 deepseek catalog models, got %d", len(merged)) + } +} + +func TestModelMatchesProviderSupportsAliasedProviderPrefixes(t *testing.T) { + if !modelMatchesProvider("minimax", "minimax-portal/MiniMax-M2.5") { + t.Fatal("expected minimax provider to accept minimax-portal model prefix") + } + if !modelMatchesProvider("kimi", "moonshot/kimi-k2.5") { + t.Fatal("expected kimi provider to accept moonshot model prefix") + } +} diff --git a/agent/app/service/entry.go b/agent/app/service/entry.go index 31015ce770c7..a27e6fa409f5 100644 --- a/agent/app/service/entry.go +++ b/agent/app/service/entry.go @@ -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() diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index b46a1e821c15..a6024ea9b4d8 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -45,6 +45,7 @@ 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' ErrAgentBaseURLRequired: 'Base URL is required' ErrAgentApiKeyRequired: 'API key is required' ErrAgentComposeRequired: 'Compose content is required' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 66cd70b7a485..b8062ab55fab 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -40,6 +40,7 @@ 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' ErrAgentBaseURLRequired: 'URL base requerida' ErrAgentApiKeyRequired: 'Clave API requerida' ErrAgentComposeRequired: 'Contenido compose requerido' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 24631f86facb..b9c9e53b4146 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -40,6 +40,7 @@ ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応' ErrAgentAccountRequired: 'まずエージェントアカウントを選択' ErrAgentAccountNotVerified: 'アカウント未検証です' ErrAgentProviderMismatch: 'エージェントプロバイダが一致しません' +ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません' ErrAgentBaseURLRequired: 'ベースURLが必要です' ErrAgentApiKeyRequired: 'APIキーが必要です' ErrAgentComposeRequired: 'Composeが必要です' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 1fdf59309228..3a84092c9815 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -40,6 +40,7 @@ ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자' ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요' ErrAgentAccountNotVerified: '에이전트 계정 미확인' ErrAgentProviderMismatch: '에이전트 공급자 불일치' +ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다' ErrAgentBaseURLRequired: '기본 URL 필요' ErrAgentApiKeyRequired: 'API 키 필요' ErrAgentComposeRequired: 'Compose 내용 필요' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index c600744793e8..95457f813c23 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -40,6 +40,7 @@ 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' ErrAgentBaseURLRequired: 'URL asas diperlukan' ErrAgentApiKeyRequired: 'Kunci API diperlukan' ErrAgentComposeRequired: 'Kandungan compose diperlukan' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 9233e877f154..61ec16aadab0 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -40,6 +40,7 @@ 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' ErrAgentBaseURLRequired: 'URL base obrigatória' ErrAgentApiKeyRequired: 'Chave API obrigatória' ErrAgentComposeRequired: 'Conteúdo de compose obrigatório' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 4f647e6ad7e9..4141d44a8cc6 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -40,6 +40,7 @@ ErrAgentProviderNotSupported: 'Провайдер агента не поддер ErrAgentAccountRequired: 'Выберите аккаунт агента' ErrAgentAccountNotVerified: 'Аккаунт не подтверждён' ErrAgentProviderMismatch: 'Провайдер не совпадает' +ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте' ErrAgentBaseURLRequired: 'Нужен базовый URL' ErrAgentApiKeyRequired: 'Нужен API-ключ' ErrAgentComposeRequired: 'Нужен compose' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 897c5fbec05e..4df177d3fe36 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -40,6 +40,7 @@ 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ı' ErrAgentBaseURLRequired: 'Temel URL gerekli' ErrAgentApiKeyRequired: 'API anahtarı gerekli' ErrAgentComposeRequired: 'Compose içeriği gerekli' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 299db6ac813b..7ad06f48803a 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -40,6 +40,7 @@ ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試' ErrAgentAccountRequired: '請選擇智能體帳號後重試' ErrAgentAccountNotVerified: '帳號未驗證通過,請重試' ErrAgentProviderMismatch: '帳號提供商不符,請重試' +ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試' ErrAgentBaseURLRequired: 'Base URL 不可為空,請重試' ErrAgentApiKeyRequired: 'API Key 不可為空,請重試' ErrAgentComposeRequired: '自訂編排內容不可為空,請重試' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 0a36ea853987..535fadd3e3ff 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -45,6 +45,7 @@ ErrAgentProviderNotSupported: "不支持该智能体提供商" ErrAgentAccountRequired: "请选择智能体账号后重试" ErrAgentAccountNotVerified: "账号未通过验证" ErrAgentProviderMismatch: "账号提供商不匹配" +ErrAgentModelNotInAccount: "所选模型未配置在当前账号中" ErrAgentBaseURLRequired: "Base URL 不能为空" ErrAgentApiKeyRequired: "API Key 不能为空" ErrAgentComposeRequired: "自定义编排内容不能为空" diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 2a6ffd852c36..98ba9cd2c08d 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -74,6 +74,7 @@ func InitAgentDB() { migrations.NormalizeAgentAccountVerifiedStatus, migrations.NormalizeOllamaAccountAPIType, migrations.RewriteOpenclawBundledCaddyfile, + migrations.InitAgentAccountModelPool, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index ca33beb50036..f7c7ea080c9e 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -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) + }, +} diff --git a/agent/init/migration/migrations/utils/agent_account_model_pool.go b/agent/init/migration/migrations/utils/agent_account_model_pool.go new file mode 100644 index 000000000000..1904bb584a8b --- /dev/null +++ b/agent/init/migration/migrations/utils/agent_account_model_pool.go @@ -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 +} diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index b2d79e1e7c64..097d1301fcd2 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -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) diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index bab030ce8f48..355338fa3c34 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -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; diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index b87556d5caf2..d5cb46ff7dd1 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -129,6 +129,22 @@ export const pageAgentAccounts = (req: AI.AgentAccountSearch) => { return http.post>(`/ai/agents/accounts/search`, req); }; +export const getAgentAccountModels = (req: AI.AgentAccountModelReq) => { + return http.post(`/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); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 54f9e4ec60f2..50e35b33ea25 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 1ceacd09d508..4e01422e0076 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -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', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index aafabd7bcd8f..47c7e101328d 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -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: '検証済み', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 82563c34ff9e..fb4a05f6267d 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -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: '검증됨', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index ab4b0221e3b8..97abc1d69269 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -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', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 1107e20b0ec8..f2a73156d1bd 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -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', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 2d855f66c5f1..aa72696b228f 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -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: 'Проверено', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 557e3a5fc671..c31f6d891ece 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -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ı', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index b170beb22e06..1c82ab7cbdaa 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -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: '驗證狀態', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index c2483571a8ef..3c0b3c1dd342 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '验证状态', diff --git a/frontend/src/views/ai/agents/agent/add/index.vue b/frontend/src/views/ai/agents/agent/add/index.vue index ccc4a8697533..3e5217457827 100644 --- a/frontend/src/views/ai/agents/agent/add/index.vue +++ b/frontend/src/views/ai/agents/agent/add/index.vue @@ -70,15 +70,11 @@ - - {{ $t('aiTools.agents.manualModel') }} - - - - + + {{ $t('aiTools.agents.accountModelsHelper') }} @@ -134,7 +130,6 @@ const accountOptions = ref([]); const providerOptions = ref>([]); const providerModels = ref>({}); const providerAccountCount = ref>({}); -const manualModel = ref(false); const appInfo = ref(); 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') { diff --git a/frontend/src/views/ai/agents/agent/config/tabs/model.vue b/frontend/src/views/ai/agents/agent/config/tabs/model.vue index c76226a518de..bc953fd64e7e 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/model.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/model.vue @@ -5,19 +5,11 @@ - - - {{ t('aiTools.agents.manualModel') }} - - - - - - - + + {{ t('aiTools.agents.accountModelsHelper') }} @@ -28,11 +20,11 @@ + + From 689bfc4fb6a6cb759051eae7aaac96a3bd070a7c Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <1paneldev@sina.com> Date: Wed, 18 Mar 2026 17:04:16 +0800 Subject: [PATCH 2/3] feat: change openclaw bind type --- agent/app/service/agents.go | 6 +- .../app/service/agents_account_models_test.go | 187 ------------------ 2 files changed, 3 insertions(+), 190 deletions(-) delete mode 100644 agent/app/service/agents_account_models_test.go diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index bad7b33d1ccf..12893c0e7c7b 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -1822,7 +1822,7 @@ func migrateOpenclawHTTPSUpgradeWithSystemIP(install *model.AppInstall, fromVers } } } - return writeOpenclawCaddyfile(configPath, []string{allowedOrigin}) + return migrateOpenclawInstallEnv(install, allowedOrigins) } func migrateOpenclawInstallPorts(install *model.AppInstall) { @@ -2119,7 +2119,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, cfg := openclawConfig{ Gateway: gatewayConfig{ Mode: "local", - Bind: "lan", + Bind: "loopback", Port: openclawGatewayPort, Auth: gatewayAuth{ Mode: "token", @@ -2212,7 +2212,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, gatewayMap["mode"] = "local" } if _, ok := gatewayMap["bind"]; !ok { - gatewayMap["bind"] = "lan" + gatewayMap["bind"] = "loopback" } if _, ok := gatewayMap["port"]; !ok { gatewayMap["port"] = openclawGatewayPort diff --git a/agent/app/service/agents_account_models_test.go b/agent/app/service/agents_account_models_test.go deleted file mode 100644 index 36f7ce25da63..000000000000 --- a/agent/app/service/agents_account_models_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package service - -import ( - "testing" - - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" -) - -func TestNormalizeAgentAccountModels(t *testing.T) { - account := &model.AgentAccount{ - Provider: "openai", - APIKey: "sk-test", - BaseURL: "https://api.openai.com/v1", - APIType: "openai-completions", - } - - models, defaultModel, err := normalizeAgentAccountModels(account, []dto.AgentAccountModel{ - {ID: "openai/gpt-4o"}, - }, "openai/gpt-4o", false) - if err != nil { - t.Fatalf("normalizeAgentAccountModels failed: %v", err) - } - if defaultModel != "openai/gpt-4o" { - t.Fatalf("unexpected default model: %s", defaultModel) - } - if len(models) != 1 { - t.Fatalf("unexpected models length: %d", len(models)) - } - if models[0].Name == "" { - t.Fatal("expected model name to be inferred") - } - if models[0].ContextWindow <= 0 || models[0].MaxTokens <= 0 { - t.Fatalf("expected runtime params to be filled: %+v", models[0]) - } - if len(models[0].Input) == 0 { - t.Fatal("expected model input types to be filled") - } -} - -func TestBuildOpenclawModelsFromAccount(t *testing.T) { - account := &model.AgentAccount{ - Provider: "openai", - APIKey: "sk-test", - BaseURL: "https://api.openai.com/v1", - APIType: "openai-completions", - } - models, _, err := normalizeAgentAccountModels(account, []dto.AgentAccountModel{ - {ID: "openai/gpt-4o"}, - {ID: "openai/gpt-4.1"}, - }, "openai/gpt-4o", false) - if err != nil { - t.Fatalf("normalizeAgentAccountModels failed: %v", err) - } - payload, err := marshalAgentAccountModels(models) - if err != nil { - t.Fatalf("marshalAgentAccountModels failed: %v", err) - } - account.Model = "openai/gpt-4o" - account.Models = payload - - primaryModel, defaultsModels, cfg, err := buildOpenclawModelsFromAccount(account, "openai/gpt-4.1") - if err != nil { - t.Fatalf("buildOpenclawModelsFromAccount failed: %v", err) - } - if primaryModel != "openai/gpt-4.1" { - t.Fatalf("unexpected primary model: %s", primaryModel) - } - if cfg == nil || cfg.Mode != "merge" { - t.Fatalf("unexpected models config: %+v", cfg) - } - if len(cfg.Providers) != 1 { - t.Fatalf("unexpected providers length: %d", len(cfg.Providers)) - } - if len(defaultsModels) != 2 { - t.Fatalf("unexpected defaults models length: %d", len(defaultsModels)) - } - for _, provider := range cfg.Providers { - if len(provider.Models) != 2 { - t.Fatalf("unexpected model entries length: %d", len(provider.Models)) - } - } -} - -func TestBuildOpenclawModelsFromAccountGemini(t *testing.T) { - account := &model.AgentAccount{ - Provider: "gemini", - APIKey: "gemini-key", - BaseURL: "https://generativelanguage.googleapis.com", - APIType: "openai-completions", - } - models, _, err := normalizeAgentAccountModels(account, []dto.AgentAccountModel{ - {ID: "google/gemini-flash-latest"}, - }, "google/gemini-flash-latest", false) - if err != nil { - t.Fatalf("normalizeAgentAccountModels failed: %v", err) - } - payload, err := marshalAgentAccountModels(models) - if err != nil { - t.Fatalf("marshalAgentAccountModels failed: %v", err) - } - account.Model = "google/gemini-flash-latest" - account.Models = payload - - primaryModel, defaultsModels, cfg, err := buildOpenclawModelsFromAccount(account, account.Model) - if err != nil { - t.Fatalf("buildOpenclawModelsFromAccount failed: %v", err) - } - if primaryModel != "google/gemini-flash-latest" { - t.Fatalf("unexpected primary model: %s", primaryModel) - } - if cfg == nil || len(cfg.Providers) != 1 { - t.Fatalf("unexpected models config: %+v", cfg) - } - if len(defaultsModels) != 1 { - t.Fatalf("unexpected defaults models length: %d", len(defaultsModels)) - } -} - -func TestLoadLegacyAgentAccountModelsForMigrationSkipsEmptyAccount(t *testing.T) { - account := &model.AgentAccount{ - Provider: "custom", - } - models, err := LoadLegacyAgentAccountModelsForMigration(account) - if err != nil { - t.Fatalf("LoadLegacyAgentAccountModelsForMigration failed: %v", err) - } - if len(models) != 0 { - t.Fatalf("expected no models, got %d", len(models)) - } -} - -func TestEnsurePersistedAgentAccountModelsSkipsEmptyLegacyAccount(t *testing.T) { - account := &model.AgentAccount{ - Provider: "ollama", - } - models, err := ensurePersistedAgentAccountModels(account) - if err != nil { - t.Fatalf("ensurePersistedAgentAccountModels failed: %v", err) - } - if len(models) != 0 { - t.Fatalf("expected no persisted models, got %d", len(models)) - } -} - -func TestLoadLegacyAgentAccountModelsForMigrationUsesCatalogModels(t *testing.T) { - account := &model.AgentAccount{ - Provider: "openai", - } - models, err := LoadLegacyAgentAccountModelsForMigration(account) - if err != nil { - t.Fatalf("LoadLegacyAgentAccountModelsForMigration failed: %v", err) - } - if len(models) < 2 { - t.Fatalf("expected catalog models to be migrated, got %d", len(models)) - } - if models[0].ID == "" { - t.Fatal("expected first catalog model id to be populated") - } -} - -func TestMergeCatalogAgentAccountModelsForMigration(t *testing.T) { - account := &model.AgentAccount{ - Provider: "deepseek", - APIKey: "sk-test", - BaseURL: "https://api.deepseek.com/v1", - APIType: "openai-completions", - } - merged, err := MergeCatalogAgentAccountModelsForMigration(account, []dto.AgentAccountModel{ - {ID: "deepseek/deepseek-chat"}, - }) - if err != nil { - t.Fatalf("MergeCatalogAgentAccountModelsForMigration failed: %v", err) - } - if len(merged) != 3 { - t.Fatalf("expected 3 deepseek catalog models, got %d", len(merged)) - } -} - -func TestModelMatchesProviderSupportsAliasedProviderPrefixes(t *testing.T) { - if !modelMatchesProvider("minimax", "minimax-portal/MiniMax-M2.5") { - t.Fatal("expected minimax provider to accept minimax-portal model prefix") - } - if !modelMatchesProvider("kimi", "moonshot/kimi-k2.5") { - t.Fatal("expected kimi provider to accept moonshot model prefix") - } -} From 6c3b4af7c9c3821e941b1828375a968ee52930d6 Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <1paneldev@sina.com> Date: Wed, 18 Mar 2026 17:51:14 +0800 Subject: [PATCH 3/3] feat: change openclaw bind type --- agent/app/service/agents.go | 248 ++++++++++++++++++++++++++++++----- agent/i18n/lang/en.yaml | 1 + agent/i18n/lang/es-ES.yaml | 1 + agent/i18n/lang/ja.yaml | 1 + agent/i18n/lang/ko.yaml | 1 + agent/i18n/lang/ms.yaml | 1 + agent/i18n/lang/pt-BR.yaml | 1 + agent/i18n/lang/ru.yaml | 1 + agent/i18n/lang/tr.yaml | 1 + agent/i18n/lang/zh-Hant.yaml | 1 + agent/i18n/lang/zh.yaml | 1 + 11 files changed, 228 insertions(+), 30 deletions(-) diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index 12893c0e7c7b..90b66b38c794 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -190,10 +190,11 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { if storedModel == "" { return nil, buserr.New("ErrAgentModelNotInAccount") } - selectedAccountModel, ok := findAgentAccountModel(accountModels, storedModel) + selectedAccountModel, ok := findAgentAccountModelForProvider(provider, accountModels, storedModel) if !ok { return nil, buserr.New("ErrAgentModelNotInAccount") } + storedModel = strings.TrimSpace(selectedAccountModel.ID) apiType, maxTokens, contextWindow = resolveRuntimeParams( provider, account.APIType, @@ -427,10 +428,11 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error if err != nil { return err } - if _, ok := findAgentAccountModel(accountModels, modelName); !ok { + selectedAccountModel, ok := findAgentAccountModelForProvider(provider, accountModels, modelName) + if !ok { return buserr.New("ErrAgentModelNotInAccount") } - selectedAccountModel, _ := findAgentAccountModel(accountModels, modelName) + modelName = strings.TrimSpace(selectedAccountModel.ID) apiType, maxTokens, contextWindow := resolveRuntimeParams( provider, account.APIType, @@ -622,18 +624,37 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { account.APIType = apiType account.Remark = req.Remark account.Verified = verified - if err := agentAccountRepo.Save(account); err != nil { - return err - } + + var nextAccountModels []dto.AgentAccountModel if len(req.Models) > 0 || strings.TrimSpace(req.Model) != "" { accountModels, _, err := normalizeAgentAccountModels(account, req.Models, req.Model, true) if err != nil { return err } - if err := replacePersistedAgentAccountModels(account.ID, accountModels); err != nil { + nextAccountModels = accountModels + if err := ensureAccountModelsNotBound(account, nextAccountModels); err != nil { return err } } + if err := agentAccountRepo.Save(account); err != nil { + return err + } + if len(nextAccountModels) > 0 { + if err := replacePersistedAgentAccountModels(account.ID, nextAccountModels); err != nil { + return err + } + } else if shouldRefreshAccountModelRuntimeLimits(provider) { + accountModels, err := loadAgentAccountModels(account) + if err != nil { + return err + } + if len(accountModels) > 0 { + accountModels = refreshAccountModelRuntimeLimits(account, accountModels) + if err := replacePersistedAgentAccountModels(account.ID, accountModels); err != nil { + return err + } + } + } if req.SyncAgents { if err := a.syncAgentsByAccount(account); err != nil { return err @@ -709,7 +730,7 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err if err != nil { return err } - if _, ok := findAgentAccountModel(models, normalized.ID); ok { + if _, ok := findAgentAccountModelForProvider(account.Provider, models, normalized.ID); ok { return buserr.New("ErrRecordExist") } inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(normalized.Input)) @@ -754,10 +775,21 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err if item.RecordID == req.Model.RecordID { continue } - if item.ID == normalized.ID { + if sameProviderModelID(account.Provider, item.ID, normalized.ID) { return buserr.New("ErrRecordExist") } } + nextModels := make([]dto.AgentAccountModel, 0, len(models)) + for _, item := range models { + if item.RecordID == req.Model.RecordID { + nextModels = append(nextModels, normalized) + continue + } + nextModels = append(nextModels, item) + } + if err := ensureAccountModelsNotBound(account, nextModels); err != nil { + return err + } inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(normalized.Input)) if err != nil { return err @@ -782,6 +814,20 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err if _, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)); err != nil { return err } + models, err := loadAgentAccountModels(account) + if err != nil { + return err + } + nextModels := make([]dto.AgentAccountModel, 0, len(models)) + for _, item := range models { + if item.RecordID == req.RecordID { + continue + } + nextModels = append(nextModels, item) + } + if err := ensureAccountModelsNotBound(account, nextModels); err != nil { + return err + } if err := agentAccountModelRepo.DeleteByID(req.RecordID); err != nil { return err } @@ -1686,18 +1732,24 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { if confDir == "" { continue } - modelName := agent.Model - if strings.TrimSpace(modelName) == "" { + modelName := strings.TrimSpace(agent.Model) + var selectedAccountModel dto.AgentAccountModel + var ok bool + if modelName != "" { + selectedAccountModel, ok = findAgentAccountModelForProvider(account.Provider, accountModels, modelName) + if !ok { + return buserr.WithName("ErrAgentModelInUse", agent.Name) + } + } else { modelName = strings.TrimSpace(account.Model) + if modelName != "" { + selectedAccountModel, ok = findAgentAccountModelForProvider(account.Provider, accountModels, modelName) + } + if !ok { + selectedAccountModel = accountModels[0] + } } - if strings.TrimSpace(modelName) == "" { - modelName = accountModels[0].ID - } - selectedAccountModel, ok := findAgentAccountModel(accountModels, modelName) - if !ok { - modelName = accountModels[0].ID - selectedAccountModel, _ = findAgentAccountModel(accountModels, modelName) - } + modelName = strings.TrimSpace(selectedAccountModel.ID) apiType, maxTokens, contextWindow := resolveRuntimeParams( account.Provider, account.APIType, @@ -2256,6 +2308,11 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s if selectedModel == "" { return "", nil, nil, fmt.Errorf("model is required") } + selectedAccountModel, ok := findAgentAccountModelForProvider(account.Provider, accountModels, selectedModel) + if !ok { + return "", nil, nil, buserr.New("ErrAgentModelNotInAccount") + } + selectedModel = strings.TrimSpace(selectedAccountModel.ID) providerKey := "" providerCfg := modelProvider{} @@ -2275,7 +2332,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s } entries = append(entries, entry) defaultsModels[resolvedPrimary] = map[string]interface{}{} - if strings.TrimSpace(item.ID) == selectedModel { + if sameProviderModelID(account.Provider, item.ID, selectedModel) { primaryModel = resolvedPrimary } } @@ -2317,7 +2374,7 @@ func buildOpenclawPrimaryModel(account *model.AgentAccount, modelID string) (str if err != nil { return "", err } - item, ok := findAgentAccountModel(models, modelID) + item, ok := findAgentAccountModelForProvider(account.Provider, models, modelID) if !ok { return "", buserr.New("ErrAgentModelNotInAccount") } @@ -2648,7 +2705,7 @@ func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.Agent if resolvedDefault == "" && allowFallbackDefault { resolvedDefault = normalized[0].ID } - if _, ok := findAgentAccountModel(normalized, resolvedDefault); !ok { + if _, ok := findAgentAccountModelForProvider(account.Provider, normalized, resolvedDefault); !ok { if allowFallbackDefault { resolvedDefault = normalized[0].ID } else { @@ -2723,10 +2780,36 @@ func normalizeAgentAccountModelID(provider, primaryModel, requestedID string) st } return "ollama/" + target default: - if strings.TrimSpace(primaryModel) != "" { - return strings.TrimSpace(primaryModel) + target := strings.TrimSpace(requestedID) + if target == "" { + target = strings.TrimSpace(primaryModel) + } + if target == "" { + return "" + } + prefix := poolModelPrefix(provider) + if strings.Contains(target, "/") { + parts := strings.SplitN(target, "/", 2) + targetPrefix := strings.ToLower(strings.TrimSpace(parts[0])) + targetModel := strings.TrimSpace(parts[1]) + if targetModel == "" { + return strings.TrimSpace(target) + } + for _, item := range supportedProviderModelPrefixes(provider) { + if item == targetPrefix { + if prefix != "" { + return prefix + "/" + targetModel + } + return strings.TrimSpace(target) + } + } + return strings.TrimSpace(target) } - return strings.TrimSpace(requestedID) + target = strings.TrimLeft(strings.TrimSpace(target), "/") + if prefix == "" { + return target + } + return prefix + "/" + target } } @@ -2804,16 +2887,87 @@ func sanitizeAgentAccountModelInputs(values []string) []string { return result } -func findAgentAccountModel(models []dto.AgentAccountModel, modelID string) (dto.AgentAccountModel, bool) { +func shouldRefreshAccountModelRuntimeLimits(provider string) bool { + switch strings.ToLower(strings.TrimSpace(provider)) { + case "custom", "vllm", "ollama": + return true + default: + return false + } +} + +func refreshAccountModelRuntimeLimits(account *model.AgentAccount, models []dto.AgentAccountModel) []dto.AgentAccountModel { + refreshed := make([]dto.AgentAccountModel, 0, len(models)) + for _, item := range models { + next := item + next.MaxTokens = account.MaxTokens + next.ContextWindow = account.ContextWindow + refreshed = append(refreshed, next) + } + return refreshed +} + +func normalizeComparableProviderModelID(provider, modelID string) string { target := strings.TrimSpace(modelID) + if target == "" { + return "" + } + if !strings.Contains(target, "/") { + return target + } + parts := strings.SplitN(target, "/", 2) + prefix := strings.ToLower(strings.TrimSpace(parts[0])) + model := strings.TrimSpace(parts[1]) + if model == "" { + return target + } + for _, item := range supportedProviderModelPrefixes(provider) { + if item == prefix { + return model + } + } + return target +} + +func sameProviderModelID(provider, left, right string) bool { + leftTrimmed := strings.TrimSpace(left) + rightTrimmed := strings.TrimSpace(right) + if leftTrimmed == rightTrimmed { + return true + } + leftComparable := normalizeComparableProviderModelID(provider, leftTrimmed) + rightComparable := normalizeComparableProviderModelID(provider, rightTrimmed) + return leftComparable != "" && leftComparable == rightComparable +} + +func findAgentAccountModelForProvider(provider string, models []dto.AgentAccountModel, modelID string) (dto.AgentAccountModel, bool) { for _, item := range models { - if strings.TrimSpace(item.ID) == target { + if sameProviderModelID(provider, item.ID, modelID) { return item, true } } return dto.AgentAccountModel{}, false } +func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error { + if account == nil || account.ID == 0 { + return nil + } + agents, err := agentRepo.List(repo.WithByAccountID(account.ID)) + if err != nil { + return err + } + for _, agent := range agents { + if strings.TrimSpace(agent.Model) == "" { + continue + } + if _, ok := findAgentAccountModelForProvider(account.Provider, models, agent.Model); !ok { + return buserr.WithName("ErrAgentModelInUse", agent.Name) + } + } + return nil +} + func resolveServerTimezone() string { timezone := strings.TrimSpace(common.LoadTimeZoneByCmd()) if timezone == "" { @@ -3021,11 +3175,16 @@ func normalizeAgentType(agentType string) string { } func modelMatchesProvider(provider, modelName string) bool { - prefix := providerModelPrefix(provider) - return prefix != "" && strings.HasPrefix(strings.TrimSpace(modelName), prefix+"/") + target := strings.TrimSpace(modelName) + for _, prefix := range supportedProviderModelPrefixes(provider) { + if prefix != "" && strings.HasPrefix(target, prefix+"/") { + return true + } + } + return false } -func providerModelPrefix(provider string) string { +func runtimeProviderModelPrefix(provider string) string { switch strings.ToLower(strings.TrimSpace(provider)) { case "gemini": return "google" @@ -3038,6 +3197,35 @@ func providerModelPrefix(provider string) string { } } +func poolModelPrefix(provider string) string { + target := strings.ToLower(strings.TrimSpace(provider)) + if definitions, ok := providerDefinitions()[target]; ok && len(definitions.Models) > 0 { + parts := strings.SplitN(strings.TrimSpace(definitions.Models[0].ID), "/", 2) + if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" { + return strings.ToLower(strings.TrimSpace(parts[0])) + } + } + return target +} + +func supportedProviderModelPrefixes(provider string) []string { + values := []string{poolModelPrefix(provider), runtimeProviderModelPrefix(provider)} + result := make([]string, 0, len(values)) + seen := make(map[string]struct{}, len(values)) + for _, value := range values { + target := strings.ToLower(strings.TrimSpace(value)) + if target == "" { + continue + } + if _, ok := seen[target]; ok { + continue + } + seen[target] = struct{}{} + result = append(result, target) + } + return result +} + func isSupportedAgentType(agentType string) bool { switch normalizeAgentType(agentType) { case constant.AppOpenclaw, constant.AppCopaw: diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index a6024ea9b4d8..4d29312b3937 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -46,6 +46,7 @@ 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' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index b8062ab55fab..ce03ef5648ae 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -41,6 +41,7 @@ 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' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index b9c9e53b4146..b43bd405fe87 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -41,6 +41,7 @@ ErrAgentAccountRequired: 'まずエージェントアカウントを選択' ErrAgentAccountNotVerified: 'アカウント未検証です' ErrAgentProviderMismatch: 'エージェントプロバイダが一致しません' ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません' +ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}' ErrAgentBaseURLRequired: 'ベースURLが必要です' ErrAgentApiKeyRequired: 'APIキーが必要です' ErrAgentComposeRequired: 'Composeが必要です' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 3a84092c9815..9a9d4336cc01 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -41,6 +41,7 @@ ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요' ErrAgentAccountNotVerified: '에이전트 계정 미확인' ErrAgentProviderMismatch: '에이전트 공급자 불일치' ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다' +ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}' ErrAgentBaseURLRequired: '기본 URL 필요' ErrAgentApiKeyRequired: 'API 키 필요' ErrAgentComposeRequired: 'Compose 내용 필요' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 95457f813c23..09f1a1fb5234 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -41,6 +41,7 @@ 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' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 61ec16aadab0..13fa48cbe6bf 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -41,6 +41,7 @@ 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' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 4141d44a8cc6..95bb6814d3c3 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -41,6 +41,7 @@ ErrAgentAccountRequired: 'Выберите аккаунт агента' ErrAgentAccountNotVerified: 'Аккаунт не подтверждён' ErrAgentProviderMismatch: 'Провайдер не совпадает' ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте' +ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}' ErrAgentBaseURLRequired: 'Нужен базовый URL' ErrAgentApiKeyRequired: 'Нужен API-ключ' ErrAgentComposeRequired: 'Нужен compose' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 4df177d3fe36..7e460c93e4ca 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -41,6 +41,7 @@ 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' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 7ad06f48803a..2bc157695799 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -41,6 +41,7 @@ ErrAgentAccountRequired: '請選擇智能體帳號後重試' ErrAgentAccountNotVerified: '帳號未驗證通過,請重試' ErrAgentProviderMismatch: '帳號提供商不符,請重試' ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試' +ErrAgentModelInUse: '仍有智能體正在使用該模型:{{ .name }}' ErrAgentBaseURLRequired: 'Base URL 不可為空,請重試' ErrAgentApiKeyRequired: 'API Key 不可為空,請重試' ErrAgentComposeRequired: '自訂編排內容不可為空,請重試' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 535fadd3e3ff..57e2dc4d474e 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -46,6 +46,7 @@ ErrAgentAccountRequired: "请选择智能体账号后重试" ErrAgentAccountNotVerified: "账号未通过验证" ErrAgentProviderMismatch: "账号提供商不匹配" ErrAgentModelNotInAccount: "所选模型未配置在当前账号中" +ErrAgentModelInUse: "仍有智能体正在使用该模型: {{ .name }}" ErrAgentBaseURLRequired: "Base URL 不能为空" ErrAgentApiKeyRequired: "API Key 不能为空" ErrAgentComposeRequired: "自定义编排内容不能为空"