Compare commits

...
Author SHA1 Message Date
CityFun 8644579eda feat: Agent Model Account support custom config (#11933)
* feat: Agent Model Account support custom config

* feat: Add agent gatewayControlUi config
2026-02-24 18:35:38 +08:00
24 changed files with 492 additions and 125 deletions
+58 -40
View File
@@ -9,6 +9,9 @@ type AgentCreateReq struct {
BridgePort int `json:"bridgePort" validate:"required"` BridgePort int `json:"bridgePort" validate:"required"`
Provider string `json:"provider" validate:"required"` Provider string `json:"provider" validate:"required"`
Model string `json:"model" validate:"required"` Model string `json:"model" validate:"required"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
@@ -28,26 +31,29 @@ type AgentCreateReq struct {
} }
type AgentItem struct { type AgentItem struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Provider string `json:"provider"` Provider string `json:"provider"`
ProviderName string `json:"providerName"` ProviderName string `json:"providerName"`
Model string `json:"model"` Model string `json:"model"`
BaseURL string `json:"baseUrl"` APIType string `json:"apiType"`
APIKey string `json:"apiKey"` MaxTokens int `json:"maxTokens"`
Token string `json:"token"` ContextWindow int `json:"contextWindow"`
Status string `json:"status"` BaseURL string `json:"baseUrl"`
Message string `json:"message"` APIKey string `json:"apiKey"`
AppInstallID uint `json:"appInstallId"` Token string `json:"token"`
AccountID uint `json:"accountId"` Status string `json:"status"`
AppVersion string `json:"appVersion"` Message string `json:"message"`
Container string `json:"containerName"` AppInstallID uint `json:"appInstallId"`
WebUIPort int `json:"webUIPort"` AccountID uint `json:"accountId"`
BridgePort int `json:"bridgePort"` AppVersion string `json:"appVersion"`
Path string `json:"path"` Container string `json:"containerName"`
ConfigPath string `json:"configPath"` WebUIPort int `json:"webUIPort"`
Upgradable bool `json:"upgradable"` BridgePort int `json:"bridgePort"`
CreatedAt time.Time `json:"createdAt"` Path string `json:"path"`
ConfigPath string `json:"configPath"`
Upgradable bool `json:"upgradable"`
CreatedAt time.Time `json:"createdAt"`
} }
type AgentDeleteReq struct { type AgentDeleteReq struct {
@@ -67,20 +73,28 @@ type AgentModelConfigUpdateReq struct {
} }
type AgentAccountCreateReq struct { type AgentAccountCreateReq struct {
Provider string `json:"provider" validate:"required"` Provider string `json:"provider" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"` APIKey string `json:"apiKey" validate:"required"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
Remark string `json:"remark"` Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Remark string `json:"remark"`
} }
type AgentAccountUpdateReq struct { type AgentAccountUpdateReq struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"` APIKey string `json:"apiKey" validate:"required"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
Remark string `json:"remark"` Model string `json:"model"`
SyncAgents bool `json:"syncAgents"` APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
} }
type AgentAccountVerifyReq struct { type AgentAccountVerifyReq struct {
@@ -100,15 +114,19 @@ type AgentAccountSearch struct {
} }
type AgentAccountInfo struct { type AgentAccountInfo struct {
ID uint `json:"id"` ID uint `json:"id"`
Provider string `json:"provider"` Provider string `json:"provider"`
ProviderName string `json:"providerName"` ProviderName string `json:"providerName"`
Name string `json:"name"` Name string `json:"name"`
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
Verified bool `json:"verified"` Model string `json:"model"`
Remark string `json:"remark"` APIType string `json:"apiType"`
CreatedAt time.Time `json:"createdAt"` MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
} }
type ProviderModelInfo struct { type ProviderModelInfo struct {
+14 -11
View File
@@ -2,15 +2,18 @@ package model
type Agent struct { type Agent struct {
BaseModel BaseModel
Name string `json:"name" gorm:"not null;unique"` Name string `json:"name" gorm:"not null;unique"`
Provider string `json:"provider"` Provider string `json:"provider"`
Model string `json:"model"` Model string `json:"model"`
BaseURL string `json:"baseUrl"` APIType string `json:"apiType"`
APIKey string `json:"apiKey"` MaxTokens int `json:"maxTokens"`
Token string `json:"token"` ContextWindow int `json:"contextWindow"`
Status string `json:"status"` BaseURL string `json:"baseUrl"`
Message string `json:"message"` APIKey string `json:"apiKey"`
AppInstallID uint `json:"appInstallId"` Token string `json:"token"`
AccountID uint `json:"accountId"` Status string `json:"status"`
ConfigPath string `json:"configPath"` Message string `json:"message"`
AppInstallID uint `json:"appInstallId"`
AccountID uint `json:"accountId"`
ConfigPath string `json:"configPath"`
} }
+10 -6
View File
@@ -2,12 +2,16 @@ package model
type AgentAccount struct { type AgentAccount struct {
BaseModel BaseModel
Provider string `json:"provider"` Provider string `json:"provider"`
Name string `json:"name"` Name string `json:"name"`
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
Verified bool `json:"verified"` Model string `json:"model"`
Remark string `json:"remark"` APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
} }
func (AgentAccount) TableName() string { func (AgentAccount) TableName() string {
+22 -11
View File
@@ -1,6 +1,8 @@
package provider package provider
import "strings" import (
"strings"
)
type Model struct { type Model struct {
ID string ID string
@@ -21,13 +23,13 @@ var catalog = map[string]Meta{
"ollama": { "ollama": {
Key: "ollama", Key: "ollama",
DisplayName: "Ollama", DisplayName: "Ollama",
Sort: 1, Sort: 10,
Enabled: true, Enabled: true,
}, },
"deepseek": { "deepseek": {
Key: "deepseek", Key: "deepseek",
DisplayName: "DeepSeek", DisplayName: "DeepSeek",
Sort: 2, Sort: 20,
DefaultBaseURL: "https://api.deepseek.com/v1", DefaultBaseURL: "https://api.deepseek.com/v1",
EnvKey: "DEEPSEEK_API_KEY", EnvKey: "DEEPSEEK_API_KEY",
Enabled: true, Enabled: true,
@@ -37,10 +39,19 @@ var catalog = map[string]Meta{
{ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B"}, {ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B"},
}, },
}, },
"custom": {
Key: "custom",
DisplayName: "Custom",
Sort: 30,
DefaultBaseURL: "",
EnvKey: "CUSTOM_API_KEY",
Enabled: true,
Models: []Model{},
},
"openai": { "openai": {
Key: "openai", Key: "openai",
DisplayName: "OpenAI", DisplayName: "OpenAI",
Sort: 3, Sort: 40,
DefaultBaseURL: "https://api.openai.com/v1", DefaultBaseURL: "https://api.openai.com/v1",
EnvKey: "OPENAI_API_KEY", EnvKey: "OPENAI_API_KEY",
Enabled: true, Enabled: true,
@@ -56,7 +67,7 @@ var catalog = map[string]Meta{
"anthropic": { "anthropic": {
Key: "anthropic", Key: "anthropic",
DisplayName: "Anthropic", DisplayName: "Anthropic",
Sort: 4, Sort: 50,
DefaultBaseURL: "https://api.anthropic.com", DefaultBaseURL: "https://api.anthropic.com",
EnvKey: "ANTHROPIC_API_KEY", EnvKey: "ANTHROPIC_API_KEY",
Enabled: true, Enabled: true,
@@ -71,7 +82,7 @@ var catalog = map[string]Meta{
"gemini": { "gemini": {
Key: "gemini", Key: "gemini",
DisplayName: "Gemini", DisplayName: "Gemini",
Sort: 5, Sort: 60,
DefaultBaseURL: "https://generativelanguage.googleapis.com", DefaultBaseURL: "https://generativelanguage.googleapis.com",
EnvKey: "GEMINI_API_KEY", EnvKey: "GEMINI_API_KEY",
Enabled: true, Enabled: true,
@@ -87,7 +98,7 @@ var catalog = map[string]Meta{
"minimax": { "minimax": {
Key: "minimax", Key: "minimax",
DisplayName: "MiniMax (CN)", DisplayName: "MiniMax (CN)",
Sort: 6, Sort: 70,
DefaultBaseURL: "https://api.minimaxi.com/anthropic", DefaultBaseURL: "https://api.minimaxi.com/anthropic",
EnvKey: "MINIMAX_API_KEY", EnvKey: "MINIMAX_API_KEY",
Enabled: true, Enabled: true,
@@ -99,7 +110,7 @@ var catalog = map[string]Meta{
"moonshot": { "moonshot": {
Key: "moonshot", Key: "moonshot",
DisplayName: "Moonshot (Global)", DisplayName: "Moonshot (Global)",
Sort: 7, Sort: 80,
DefaultBaseURL: "https://api.moonshot.ai/v1", DefaultBaseURL: "https://api.moonshot.ai/v1",
EnvKey: "MOONSHOT_API_KEY", EnvKey: "MOONSHOT_API_KEY",
Enabled: true, Enabled: true,
@@ -112,7 +123,7 @@ var catalog = map[string]Meta{
"kimi": { "kimi": {
Key: "kimi", Key: "kimi",
DisplayName: "Kimi (CN)", DisplayName: "Kimi (CN)",
Sort: 8, Sort: 90,
DefaultBaseURL: "https://api.moonshot.cn/v1", DefaultBaseURL: "https://api.moonshot.cn/v1",
EnvKey: "KIMI_API_KEY", EnvKey: "KIMI_API_KEY",
Enabled: true, Enabled: true,
@@ -125,7 +136,7 @@ var catalog = map[string]Meta{
"kimi-coding": { "kimi-coding": {
Key: "kimi-coding", Key: "kimi-coding",
DisplayName: "Kimi Coding", DisplayName: "Kimi Coding",
Sort: 9, Sort: 100,
DefaultBaseURL: "https://api.moonshot.cn/anthropic/v1", DefaultBaseURL: "https://api.moonshot.cn/anthropic/v1",
EnvKey: "KIMI_API_KEY", EnvKey: "KIMI_API_KEY",
Enabled: true, Enabled: true,
@@ -136,7 +147,7 @@ var catalog = map[string]Meta{
"qwen": { "qwen": {
Key: "qwen", Key: "qwen",
DisplayName: "Qwen", DisplayName: "Qwen",
Sort: 10, Sort: 110,
DefaultBaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", DefaultBaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
EnvKey: "QWEN_API_KEY", EnvKey: "QWEN_API_KEY",
Enabled: false, Enabled: false,
+215 -50
View File
@@ -81,6 +81,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" { if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" {
return nil, buserr.New("ErrAgentApiKeyRequired") return nil, buserr.New("ErrAgentApiKeyRequired")
} }
apiType, maxTokens, contextWindow := resolveRuntimeParams(provider, account.APIType, account.MaxTokens, account.ContextWindow)
if err := checkPortExist(req.WebUIPort); err != nil { if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err return nil, err
} }
@@ -109,6 +110,9 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
params := map[string]interface{}{ params := map[string]interface{}{
"PROVIDER": provider, "PROVIDER": provider,
"MODEL": req.Model, "MODEL": req.Model,
"API_TYPE": apiType,
"MAX_TOKENS": maxTokens,
"CONTEXT_WINDOW": contextWindow,
"BASE_URL": baseURL, "BASE_URL": baseURL,
"API_KEY": account.APIKey, "API_KEY": account.APIKey,
"OPENCLAW_GATEWAY_TOKEN": token, "OPENCLAW_GATEWAY_TOKEN": token,
@@ -147,22 +151,25 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
} }
configPath := path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json") configPath := path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json")
agent := &model.Agent{ agent := &model.Agent{
Name: req.Name, Name: req.Name,
Provider: provider, Provider: provider,
Model: req.Model, Model: req.Model,
BaseURL: baseURL, APIType: apiType,
APIKey: account.APIKey, MaxTokens: maxTokens,
Token: token, ContextWindow: contextWindow,
Status: appInstall.Status, BaseURL: baseURL,
Message: appInstall.Message, APIKey: account.APIKey,
AppInstallID: appInstall.ID, Token: token,
AccountID: account.ID, Status: appInstall.Status,
ConfigPath: configPath, Message: appInstall.Message,
AppInstallID: appInstall.ID,
AccountID: account.ID,
ConfigPath: configPath,
} }
if err := agentRepo.Create(agent); err != nil { if err := agentRepo.Create(agent); err != nil {
return nil, err return nil, err
} }
go a.writeConfigWithRetry(appInstall, provider, req.Model, baseURL, req.APIKey, token, agent.ID) go a.writeConfigWithRetry(appInstall, provider, req.Model, apiType, maxTokens, contextWindow, baseURL, account.APIKey, token, agent.ID)
item := buildAgentItem(agent, appInstall, nil) item := buildAgentItem(agent, appInstall, nil)
return &item, nil return &item, nil
@@ -287,6 +294,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" { if provider != "ollama" && strings.TrimSpace(account.APIKey) == "" {
return buserr.New("ErrAgentApiKeyRequired") return buserr.New("ErrAgentApiKeyRequired")
} }
apiType, maxTokens, contextWindow := resolveRuntimeParams(provider, account.APIType, account.MaxTokens, account.ContextWindow)
confDir := "" confDir := ""
if agent.ConfigPath != "" { if agent.ConfigPath != "" {
confDir = path.Dir(agent.ConfigPath) confDir = path.Dir(agent.ConfigPath)
@@ -299,11 +307,14 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
if confDir == "" { if confDir == "" {
return buserr.New("ErrRecordNotFound") return buserr.New("ErrRecordNotFound")
} }
if err := writeOpenclawConfig(confDir, provider, modelName, baseURL, account.APIKey, agent.Token); err != nil { if err := writeOpenclawConfig(confDir, provider, modelName, apiType, maxTokens, contextWindow, baseURL, account.APIKey, agent.Token); err != nil {
return err return err
} }
agent.Provider = provider agent.Provider = provider
agent.Model = modelName agent.Model = modelName
agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
agent.BaseURL = baseURL agent.BaseURL = baseURL
agent.APIKey = account.APIKey agent.APIKey = account.APIKey
agent.AccountID = account.ID agent.AccountID = account.ID
@@ -338,7 +349,10 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
return buserr.New("ErrAgentApiKeyRequired") return buserr.New("ErrAgentApiKeyRequired")
} }
baseURL := strings.TrimSpace(req.BaseURL) baseURL := strings.TrimSpace(req.BaseURL)
if baseURL == "" { if provider == "custom" && baseURL == "" {
return buserr.New("ErrAgentBaseURLRequired")
}
if provider != "custom" && baseURL == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok { if defaultURL, ok := providerDefaultBaseURL(provider); ok {
baseURL = defaultURL baseURL = defaultURL
} }
@@ -349,16 +363,36 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 { if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 {
return buserr.New("ErrRecordExist") return buserr.New("ErrRecordExist")
} }
modelName := strings.TrimSpace(req.Model)
apiType := normalizeAPIType(req.APIType)
if provider == "custom" {
if modelName == "" {
return fmt.Errorf("model is required")
}
if !isSupportedAPIType(apiType) {
return fmt.Errorf("apiType is invalid")
}
}
if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: apiKey}); err != nil { if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: apiKey}); err != nil {
return err return err
} }
_, maxTokens, contextWindow := resolveRuntimeParams(provider, apiType, req.MaxTokens, req.ContextWindow)
account := &model.AgentAccount{ account := &model.AgentAccount{
Provider: provider, Provider: provider,
Name: req.Name, Name: req.Name,
APIKey: apiKey, APIKey: apiKey,
BaseURL: baseURL, BaseURL: baseURL,
Verified: true, Model: "",
Remark: req.Remark, APIType: apiType,
MaxTokens: 0,
ContextWindow: 0,
Verified: true,
Remark: req.Remark,
}
if provider == "custom" {
account.Model = normalizeCustomModel(modelName)
account.MaxTokens = maxTokens
account.ContextWindow = contextWindow
} }
return agentAccountRepo.Create(account) return agentAccountRepo.Create(account)
} }
@@ -370,7 +404,10 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
} }
provider := strings.ToLower(strings.TrimSpace(account.Provider)) provider := strings.ToLower(strings.TrimSpace(account.Provider))
baseURL := strings.TrimSpace(req.BaseURL) baseURL := strings.TrimSpace(req.BaseURL)
if baseURL == "" { if provider == "custom" && baseURL == "" {
return buserr.New("ErrAgentBaseURLRequired")
}
if provider != "custom" && baseURL == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok { if defaultURL, ok := providerDefaultBaseURL(provider); ok {
baseURL = defaultURL baseURL = defaultURL
} }
@@ -378,12 +415,31 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
if provider == "ollama" && baseURL == "" { if provider == "ollama" && baseURL == "" {
return buserr.New("ErrAgentBaseURLRequired") return buserr.New("ErrAgentBaseURLRequired")
} }
apiType := normalizeAPIType(req.APIType)
if provider == "custom" && strings.TrimSpace(req.Model) == "" {
return fmt.Errorf("model is required")
}
if provider == "custom" && !isSupportedAPIType(apiType) {
return fmt.Errorf("apiType is invalid")
}
if provider != "custom" {
apiType = normalizeAPIType(account.APIType)
}
_, maxTokens, contextWindow := resolveRuntimeParams(provider, apiType, req.MaxTokens, req.ContextWindow)
if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: req.APIKey}); err != nil { if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: req.APIKey}); err != nil {
return err return err
} }
account.Name = req.Name account.Name = req.Name
account.APIKey = req.APIKey account.APIKey = req.APIKey
account.BaseURL = baseURL account.BaseURL = baseURL
if provider == "custom" {
account.Model = normalizeCustomModel(req.Model)
}
account.APIType = apiType
if provider == "custom" {
account.MaxTokens = maxTokens
account.ContextWindow = contextWindow
}
account.Remark = req.Remark account.Remark = req.Remark
account.Verified = true account.Verified = true
if err := agentAccountRepo.Save(account); err != nil { if err := agentAccountRepo.Save(account); err != nil {
@@ -412,15 +468,19 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
items := make([]dto.AgentAccountInfo, 0, len(list)) items := make([]dto.AgentAccountInfo, 0, len(list))
for _, item := range list { for _, item := range list {
items = append(items, dto.AgentAccountInfo{ items = append(items, dto.AgentAccountInfo{
ID: item.ID, ID: item.ID,
Provider: item.Provider, Provider: item.Provider,
ProviderName: providerDisplayName(item.Provider), ProviderName: providerDisplayName(item.Provider),
Name: item.Name, Name: item.Name,
APIKey: item.APIKey, APIKey: item.APIKey,
BaseURL: item.BaseURL, BaseURL: item.BaseURL,
Verified: item.Verified, Model: item.Model,
Remark: item.Remark, APIType: item.APIType,
CreatedAt: item.CreatedAt, MaxTokens: item.MaxTokens,
ContextWindow: item.ContextWindow,
Verified: item.Verified,
Remark: item.Remark,
CreatedAt: item.CreatedAt,
}) })
} }
return count, items, nil return count, items, nil
@@ -447,6 +507,9 @@ func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
if provider == "ollama" { if provider == "ollama" {
return nil return nil
} }
if provider == "custom" {
return nil
}
return verifyProvider(provider, baseURL, apiKey) return verifyProvider(provider, baseURL, apiKey)
} }
@@ -642,12 +705,16 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
if confDir == "" { if confDir == "" {
continue continue
} }
if err := writeOpenclawConfig(confDir, account.Provider, agent.Model, baseURL, account.APIKey, agent.Token); err != nil { apiType, maxTokens, contextWindow := resolveRuntimeParams(account.Provider, account.APIType, account.MaxTokens, account.ContextWindow)
if err := writeOpenclawConfig(confDir, account.Provider, agent.Model, apiType, maxTokens, contextWindow, baseURL, account.APIKey, agent.Token); err != nil {
return err return err
} }
agent.BaseURL = baseURL agent.BaseURL = baseURL
agent.APIKey = account.APIKey agent.APIKey = account.APIKey
agent.Provider = account.Provider agent.Provider = account.Provider
agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
_ = agentRepo.Save(&agent) _ = agentRepo.Save(&agent)
} }
return nil return nil
@@ -713,20 +780,23 @@ func verifyMinimax(baseURL, apiKey string) error {
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem { func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
item := dto.AgentItem{ item := dto.AgentItem{
ID: agent.ID, ID: agent.ID,
Name: agent.Name, Name: agent.Name,
Provider: agent.Provider, Provider: agent.Provider,
ProviderName: providerDisplayName(agent.Provider), ProviderName: providerDisplayName(agent.Provider),
Model: agent.Model, Model: agent.Model,
BaseURL: agent.BaseURL, APIType: agent.APIType,
APIKey: maskKey(agent.APIKey), MaxTokens: agent.MaxTokens,
Token: agent.Token, ContextWindow: agent.ContextWindow,
Status: agent.Status, BaseURL: agent.BaseURL,
Message: agent.Message, APIKey: maskKey(agent.APIKey),
AppInstallID: agent.AppInstallID, Token: agent.Token,
AccountID: agent.AccountID, Status: agent.Status,
ConfigPath: agent.ConfigPath, Message: agent.Message,
CreatedAt: agent.CreatedAt, AppInstallID: agent.AppInstallID,
AccountID: agent.AccountID,
ConfigPath: agent.ConfigPath,
CreatedAt: agent.CreatedAt,
} }
if appInstall != nil && appInstall.ID > 0 { if appInstall != nil && appInstall.ID > 0 {
item.Container = appInstall.ContainerName item.Container = appInstall.ContainerName
@@ -792,7 +862,7 @@ func (a AgentService) waitAndDeleteAgent(agentID uint, appInstallID uint) {
} }
} }
func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provider, modelName, baseURL, apiKey, token string, agentID uint) { func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string, agentID uint) {
if appInstall == nil { if appInstall == nil {
return return
} }
@@ -805,7 +875,7 @@ func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provide
time.Sleep(time.Second) time.Sleep(time.Second)
} }
confDir := path.Join(appInstall.GetPath(), "data", "conf") confDir := path.Join(appInstall.GetPath(), "data", "conf")
if err := writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token); err != nil { if err := writeOpenclawConfig(confDir, provider, modelName, apiType, maxTokens, contextWindow, baseURL, apiKey, token); err != nil {
global.LOG.Errorf("write openclaw config failed: %v", err) global.LOG.Errorf("write openclaw config failed: %v", err)
agent, errGet := agentRepo.GetFirst(repo.WithByID(agentID)) agent, errGet := agentRepo.GetFirst(repo.WithByID(agentID))
if errGet == nil && agent != nil { if errGet == nil && agent != nil {
@@ -848,7 +918,8 @@ type gatewayConfig struct {
} }
type gatewayControlUi struct { type gatewayControlUi struct {
AllowInsecureAuth bool `json:"allowInsecureAuth"` DangerouslyDisableDeviceAuth bool `json:"dangerouslyDisableDeviceAuth"`
DangerouslyAllowHostHeaderOriginFallback bool `json:"dangerouslyAllowHostHeaderOriginFallback"`
} }
type gatewayAuth struct { type gatewayAuth struct {
@@ -897,7 +968,7 @@ type modelCost struct {
CacheWrite float64 `json:"cacheWrite"` CacheWrite float64 `json:"cacheWrite"`
} }
func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token string) error { func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string) error {
if strings.TrimSpace(confDir) == "" { if strings.TrimSpace(confDir) == "" {
return fmt.Errorf("config dir is required") return fmt.Errorf("config dir is required")
} }
@@ -924,7 +995,8 @@ func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token st
Token: token, Token: token,
}, },
ControlUi: gatewayControlUi{ ControlUi: gatewayControlUi{
AllowInsecureAuth: true, DangerouslyDisableDeviceAuth: true,
DangerouslyAllowHostHeaderOriginFallback: true,
}, },
}, },
Agents: agentsConfig{ Agents: agentsConfig{
@@ -1038,6 +1110,43 @@ func writeOpenclawConfig(confDir, provider, modelName, baseURL, apiKey, token st
}, },
}, },
} }
} else if provider == "custom" {
primary := modelName
if !strings.Contains(primary, "/") {
primary = "custom/" + strings.TrimSpace(primary)
}
cfg.Agents.Defaults.Model.Primary = primary
base := strings.TrimSpace(baseURL)
plainKey := strings.TrimSpace(apiKey)
if !strings.Contains(modelName, "/") {
modelName = primary
}
customModelID := modelID
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
customModelID = parts[1]
}
useAPIType, useMaxTokens, useContextWindow := resolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"custom": {
ApiKey: plainKey,
BaseUrl: base,
Api: useAPIType,
Models: []modelEntry{
{
ID: customModelID,
Name: customModelID,
Reasoning: strings.Contains(strings.ToLower(customModelID), "reason") || strings.Contains(strings.ToLower(customModelID), "thinking"),
Input: []string{"text"},
ContextWindow: useContextWindow,
MaxTokens: useMaxTokens,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "ollama" { } else if provider == "ollama" {
cfg.Agents.Defaults.Model.Primary = modelName cfg.Agents.Defaults.Model.Primary = modelName
cfg.Models = &modelsConfig{ cfg.Models = &modelsConfig{
@@ -1275,6 +1384,62 @@ func toInt(value interface{}) int {
} }
} }
func normalizeCustomModel(modelName string) string {
trim := strings.TrimSpace(modelName)
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 {
if strings.EqualFold(parts[0], "custom") {
return strings.TrimSpace(parts[1])
}
}
return trim
}
func normalizeAPIType(apiType string) string {
trim := strings.ToLower(strings.TrimSpace(apiType))
if trim == "" {
return "openai-completions"
}
return trim
}
func isSupportedAPIType(apiType string) bool {
switch normalizeAPIType(apiType) {
case "openai-completions", "openai-responses":
return true
default:
return false
}
}
func resolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) {
resolvedAPI := normalizeAPIType(apiType)
resolvedMaxTokens := maxTokens
resolvedContextWindow := contextWindow
if resolvedMaxTokens <= 0 {
switch provider {
case "deepseek":
resolvedMaxTokens = 8192
case "minimax", "kimi-coding", "custom":
resolvedMaxTokens = 8192
default:
resolvedMaxTokens = 8192
}
}
if resolvedContextWindow <= 0 {
switch provider {
case "deepseek":
resolvedContextWindow = 128000
case "minimax", "kimi-coding":
resolvedContextWindow = 200000
case "custom":
resolvedContextWindow = 128000
default:
resolvedContextWindow = 256000
}
}
return resolvedAPI, resolvedMaxTokens, resolvedContextWindow
}
func generateToken() string { func generateToken() string {
bytes := make([]byte, 24) bytes := make([]byte, 24)
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {
+1
View File
@@ -65,6 +65,7 @@ func InitAgentDB() {
migrations.AddWebsiteAcmeAccountColumn, migrations.AddWebsiteAcmeAccountColumn,
migrations.AddAgentTables, migrations.AddAgentTables,
migrations.MigrateOpenclawAgents, migrations.MigrateOpenclawAgents,
migrations.AddAgentCustomModelFields,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)
+16
View File
@@ -941,3 +941,19 @@ var MigrateOpenclawAgents = &gormigrate.Migration{
return nil return nil
}, },
} }
var AddAgentCustomModelFields = &gormigrate.Migration{
ID: "20260224-add-agent-custom-model-fields",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Agent{}, &model.AgentAccount{}); err != nil {
return err
}
if err := tx.Model(&model.AgentAccount{}).Where("api_type = '' OR api_type IS NULL").Update("api_type", "openai-completions").Error; err != nil {
return err
}
if err := tx.Model(&model.Agent{}).Where("api_type = '' OR api_type IS NULL").Update("api_type", "openai-completions").Error; err != nil {
return err
}
return nil
},
}
+18
View File
@@ -243,6 +243,9 @@ export namespace AI {
bridgePort: number; bridgePort: number;
provider: string; provider: string;
model: string; model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
accountId: number; accountId: number;
apiKey: string; apiKey: string;
baseURL: string; baseURL: string;
@@ -267,6 +270,9 @@ export namespace AI {
provider: string; provider: string;
providerName: string; providerName: string;
model: string; model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
baseUrl: string; baseUrl: string;
apiKey: string; apiKey: string;
token: string; token: string;
@@ -317,6 +323,10 @@ export namespace AI {
name: string; name: string;
apiKey: string; apiKey: string;
baseURL: string; baseURL: string;
model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
remark: string; remark: string;
} }
@@ -325,6 +335,10 @@ export namespace AI {
name: string; name: string;
apiKey: string; apiKey: string;
baseURL: string; baseURL: string;
model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
remark: string; remark: string;
syncAgents: boolean; syncAgents: boolean;
} }
@@ -343,6 +357,10 @@ export namespace AI {
name: string; name: string;
apiKey: string; apiKey: string;
baseUrl: string; baseUrl: string;
model: string;
apiType: string;
maxTokens: number;
contextWindow: number;
verified: boolean; verified: boolean;
remark: string; remark: string;
createdAt: string; createdAt: string;
+2
View File
@@ -713,6 +713,8 @@ const message = {
feishuSaveSuccess: 'Saved successfully', feishuSaveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code', pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully', pairingApproveSuccess: 'Pairing approved successfully',
customModelHelper: 'For custom model accounts, model names must start with custom/.',
customProviderHelper: 'Custom model providers do not validate whether the account is available.',
}, },
model: { model: {
model: 'Models', model: 'Models',
+2
View File
@@ -709,6 +709,8 @@ const message = {
feishuSaveSuccess: 'Saved successfully', feishuSaveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code', pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully', pairingApproveSuccess: 'Pairing approved successfully',
customModelHelper: 'En la cuenta de modelo personalizada, el nombre del modelo debe empezar por custom/',
customProviderHelper: 'En el proveedor de modelo personalizado no se valida si la cuenta está disponible',
}, },
model: { model: {
model: 'Modelo', model: 'Modelo',
+2
View File
@@ -698,6 +698,8 @@ const message = {
feishuSaveSuccess: 'Saved successfully', feishuSaveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code', pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully', pairingApproveSuccess: 'Pairing approved successfully',
customModelHelper: 'カスタムモデルアカウントでは、モデル名は必ず custom/ で始めてください',
customProviderHelper: 'カスタムモデルプロバイダーでは、アカウントの有効性を検証しません',
}, },
model: { model: {
model: 'モデル', model: 'モデル',
+2
View File
@@ -695,6 +695,8 @@ const message = {
feishuSaveSuccess: 'Saved successfully', feishuSaveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code', pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully', pairingApproveSuccess: 'Pairing approved successfully',
customModelHelper: '사용자 정의 모델 계정의 모델명은 반드시 custom/ 로 시작해야 합니다',
customProviderHelper: '사용자 정의 모델 공급자는 계정 사용 가능 여부를 검증하지 않습니다',
}, },
model: { model: {
model: '모델', model: '모델',
+2
View File
@@ -710,6 +710,8 @@ const message = {
feishuSaveSuccess: 'Saved successfully', feishuSaveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code', pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully', pairingApproveSuccess: 'Pairing approved successfully',
customModelHelper: 'Akaun model tersuai, nama model mesti bermula dengan custom/',
customProviderHelper: 'Penyedia model tersuai tidak mengesahkan sama ada akaun boleh digunakan',
}, },
model: { model: {
model: 'Model', model: 'Model',
+2
View File
@@ -707,6 +707,8 @@ const message = {
feishuSaveSuccess: 'Saved successfully', feishuSaveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code', pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully', pairingApproveSuccess: 'Pairing approved successfully',
customModelHelper: 'Conta de modelo personalizada: o nome do modelo deve começar com custom/',
customProviderHelper: 'Provedores de modelo personalizados não validam se a conta está disponível',
}, },
model: { model: {
model: 'Modelo', model: 'Modelo',
+2
View File
@@ -703,6 +703,8 @@ const message = {
feishuSaveSuccess: 'Saved successfully', feishuSaveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code', pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully', pairingApproveSuccess: 'Pairing approved successfully',
customModelHelper: 'Для пользовательской учетной записи модели имя модели должно начинаться с custom/',
customProviderHelper: 'Для пользовательского провайдера модели доступность учетной записи не проверяется',
}, },
model: { model: {
model: 'Модель', model: 'Модель',
+2
View File
@@ -717,6 +717,8 @@ const message = {
feishuSaveSuccess: 'Saved successfully', feishuSaveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code', pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully', pairingApproveSuccess: 'Pairing approved successfully',
customModelHelper: 'Özel model hesabında model adı custom/ ile başlamalıdır',
customProviderHelper: 'Özel model sağlayıcısında hesabın kullanılabilirliği doğrulanmaz',
}, },
model: { model: {
model: 'Model', model: 'Model',
+2
View File
@@ -685,6 +685,8 @@ const message = {
feishuSaveSuccess: '保存成功', feishuSaveSuccess: '保存成功',
pairingCodeRequired: '請輸入配對碼', pairingCodeRequired: '請輸入配對碼',
pairingApproveSuccess: '配對成功', pairingApproveSuccess: '配對成功',
customModelHelper: '自訂模型帳號,模型需固定以 custom/ 開頭',
customProviderHelper: '自訂模型供應商不驗證帳號是否可用',
}, },
model: { model: {
model: '模型', model: '模型',
+2
View File
@@ -687,6 +687,8 @@ const message = {
feishuSaveSuccess: '保存成功', feishuSaveSuccess: '保存成功',
pairingCodeRequired: '请输入配对码', pairingCodeRequired: '请输入配对码',
pairingApproveSuccess: '配对成功', pairingApproveSuccess: '配对成功',
customModelHelper: '自定义模型账号,模型固定以 custom/ 开头',
customProviderHelper: '自定义模型供应商不验证账号是否可用',
}, },
model: { model: {
model: '模型', model: '模型',
+8
View File
@@ -0,0 +1,8 @@
import i18n from '@/lang';
export const getAgentProviderDisplayName = (provider: string, displayName?: string): string => {
if (provider === 'custom' || displayName === 'Custom') {
return i18n.global.t('container.custom');
}
return displayName || provider;
};
@@ -44,9 +44,13 @@
</el-form-item> </el-form-item>
<el-form-item :label="$t('aiTools.model.model')" prop="model"> <el-form-item :label="$t('aiTools.model.model')" prop="model">
<el-input v-if="manualModel" v-model="form.model" /> <el-input v-if="manualModel" v-model="form.model" />
<el-select v-else v-model="form.model" filterable @change="handleModelChange"> <el-select v-else v-model="form.model" filterable @change="handleModelChange">
<el-option v-for="item in filteredModels" :key="item.id" :label="item.name" :value="item.id" /> <el-option v-for="item in filteredModels" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
<span class="input-help" v-if="form.provider == 'custom'">
{{ $t('aiTools.agents.customModelHelper') }}
</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('aiTools.agents.account')" prop="accountId"> <el-form-item :label="$t('aiTools.agents.account')" prop="accountId">
<el-select v-model="form.accountId" @change="handleAccountChange"> <el-select v-model="form.accountId" @change="handleAccountChange">
@@ -95,6 +99,7 @@ import { createAgent, getAgentProviders, pageAgentAccounts } from '@/api/modules
import { AI } from '@/api/interface/ai'; import { AI } from '@/api/interface/ai';
import { getAppByKey, getAppDetail } from '@/api/modules/app'; import { getAppByKey, getAppDetail } from '@/api/modules/app';
import { getRandomStr, newUUID } from '@/utils/util'; import { getRandomStr, newUUID } from '@/utils/util';
import { getAgentProviderDisplayName } from '@/utils/agent';
import { App } from '@/api/interface/app'; import { App } from '@/api/interface/app';
import AdvancedSetting from '@/components/advanced-setting/index.vue'; import AdvancedSetting from '@/components/advanced-setting/index.vue';
import AccountAddDialog from '@/views/ai/agents/model/add/index.vue'; import AccountAddDialog from '@/views/ai/agents/model/add/index.vue';
@@ -120,6 +125,9 @@ const form = reactive({
provider: 'deepseek', provider: 'deepseek',
accountId: undefined as unknown as number, accountId: undefined as unknown as number,
model: '', model: '',
apiType: 'openai-completions',
maxTokens: 8192,
contextWindow: 128000,
apiKey: '', apiKey: '',
baseURL: '', baseURL: '',
token: '', token: '',
@@ -175,7 +183,7 @@ const loadProviders = async () => {
const data = res.data || []; const data = res.data || [];
providerOptions.value = data.map((item) => ({ providerOptions.value = data.map((item) => ({
value: item.provider, value: item.provider,
label: item.displayName || item.provider, label: getAgentProviderDisplayName(item.provider, item.displayName),
})); }));
providerModels.value = data.reduce((acc, item) => { providerModels.value = data.reduce((acc, item) => {
acc[item.provider] = item.models || []; acc[item.provider] = item.models || [];
@@ -246,6 +254,12 @@ const handleAccountChange = () => {
if (selected) { if (selected) {
form.baseURL = selected.baseUrl || ''; form.baseURL = selected.baseUrl || '';
form.apiKey = selected.apiKey || ''; form.apiKey = selected.apiKey || '';
form.apiType = selected.apiType || 'openai-completions';
form.maxTokens = selected.maxTokens || 8192;
form.contextWindow = selected.contextWindow || 128000;
if (selected.provider === 'custom' && selected.model && !manualModel.value) {
form.model = `custom/${selected.model}`;
}
} }
setDefaultModel(); setDefaultModel();
}; };
@@ -257,6 +271,13 @@ const setDefaultModel = () => {
const models = filteredModels.value; const models = filteredModels.value;
if (models.length > 0 && !form.model) { if (models.length > 0 && !form.model) {
form.model = models[0].id; form.model = models[0].id;
return;
}
if (form.provider === 'custom') {
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if (selected?.model && !form.model) {
form.model = `custom/${selected.model}`;
}
} }
}; };
@@ -276,6 +297,9 @@ const submit = async () => {
bridgePort: form.bridgePort, bridgePort: form.bridgePort,
provider: form.provider, provider: form.provider,
model: form.model, model: form.model,
apiType: form.apiType,
maxTokens: form.maxTokens,
contextWindow: form.contextWindow,
accountId: form.accountId, accountId: form.accountId,
apiKey: form.apiKey, apiKey: form.apiKey,
baseURL: form.baseURL, baseURL: form.baseURL,
@@ -15,6 +15,9 @@
<el-select v-else v-model="form.model" filterable> <el-select v-else v-model="form.model" filterable>
<el-option v-for="item in modelOptions" :key="item.id" :label="item.name" :value="item.id" /> <el-option v-for="item in modelOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
<span class="input-help" v-if="provdier == 'custom'">
{{ $t('aiTools.agents.customModelHelper') }}
</span>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="saving" @click="saveModel"> <el-button type="primary" :loading="saving" @click="saveModel">
@@ -44,6 +47,7 @@ const agentId = ref(0);
const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({}); const providerModels = ref<Record<string, AI.ProviderModelInfo[]>>({});
const accountOptions = ref<AI.AgentAccountItem[]>([]); const accountOptions = ref<AI.AgentAccountItem[]>([]);
const modelOptions = ref<AI.ProviderModelInfo[]>([]); const modelOptions = ref<AI.ProviderModelInfo[]>([]);
const provdier = ref('');
const form = reactive({ const form = reactive({
accountId: undefined as unknown as number, accountId: undefined as unknown as number,
@@ -89,6 +93,13 @@ const handleAccountChange = () => {
form.model = ''; form.model = '';
return; return;
} }
provdier.value = selected.provider;
if (selected.provider === 'custom') {
form.manualModel = true;
form.model = selected.model ? `custom/${selected.model}` : form.model;
modelOptions.value = [];
return;
}
setModelsByProvider(selected.provider); setModelsByProvider(selected.provider);
if (!form.manualModel && (!form.model || !form.model.startsWith(`${selected.provider}/`))) { if (!form.manualModel && (!form.model || !form.model.startsWith(`${selected.provider}/`))) {
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : ''; form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
@@ -96,10 +107,14 @@ const handleAccountChange = () => {
}; };
const handleManualModelChange = (val: unknown) => { const handleManualModelChange = (val: unknown) => {
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if (selected?.provider === 'custom' && !Boolean(val)) {
form.manualModel = true;
return;
}
if (Boolean(val)) { if (Boolean(val)) {
return; return;
} }
const selected = accountOptions.value.find((item) => item.id === form.accountId);
if (!selected) { if (!selected) {
form.model = ''; form.model = '';
return; return;
@@ -126,12 +141,15 @@ const load = async (agent: AI.AgentItem) => {
form.accountId = currentAccount.id; form.accountId = currentAccount.id;
setModelsByProvider(currentAccount.provider); setModelsByProvider(currentAccount.provider);
const inProviderModels = modelOptions.value.some((item) => item.id === agent.model); const inProviderModels = modelOptions.value.some((item) => item.id === agent.model);
form.manualModel = !inProviderModels; form.manualModel = currentAccount.provider === 'custom' || !inProviderModels;
if (agent.model && (form.manualModel || agent.model.startsWith(`${currentAccount.provider}/`))) { if (agent.model && (form.manualModel || agent.model.startsWith(`${currentAccount.provider}/`))) {
form.model = agent.model; form.model = agent.model;
} else { } else {
form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : ''; form.model = modelOptions.value.length > 0 ? modelOptions.value[0].id : '';
} }
if (currentAccount.provider === 'custom' && currentAccount.model && !form.model) {
form.model = `custom/${currentAccount.model}`;
}
} finally { } finally {
loading.value = false; loading.value = false;
} }
+2 -1
View File
@@ -38,7 +38,7 @@
min-width="120" min-width="120"
> >
<template #default="{ row }"> <template #default="{ row }">
{{ row.providerName || row.provider }} <span>{{ getAgentProviderDisplayName(row.provider, row.providerName) }}</span>
<div> <div>
<span>{{ row.model }}</span> <span>{{ row.model }}</span>
</div> </div>
@@ -115,6 +115,7 @@ import TerminalDialog from '@/views/container/container/terminal/index.vue';
import i18n from '@/lang'; import i18n from '@/lang';
import PortJumpDialog from '@/components/port-jump/index.vue'; import PortJumpDialog from '@/components/port-jump/index.vue';
import DockerStatus from '@/views/container/docker-status/index.vue'; import DockerStatus from '@/views/container/docker-status/index.vue';
import { getAgentProviderDisplayName } from '@/utils/agent';
const items = ref<AI.AgentItem[]>([]); const items = ref<AI.AgentItem[]>([]);
const loading = ref(false); const loading = ref(false);
@@ -16,9 +16,25 @@
</el-form-item> </el-form-item>
<el-form-item :label="$t('aiTools.agents.apiKey')" prop="apiKey"> <el-form-item :label="$t('aiTools.agents.apiKey')" prop="apiKey">
<el-input v-model="form.apiKey" type="password" show-password /> <el-input v-model="form.apiKey" type="password" show-password />
<span class="input-help">{{ $t('aiTools.agents.customProviderHelper') }}</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('aiTools.agents.baseUrl')" prop="baseURL"> <el-form-item :label="$t('aiTools.agents.baseUrl')" prop="baseURL">
<el-input v-model="form.baseURL" :disabled="form.provider !== 'ollama'" /> <el-input v-model="form.baseURL" :disabled="form.provider !== 'ollama' && form.provider !== 'custom'" />
</el-form-item>
<el-form-item :label="$t('aiTools.model.model')" prop="model" v-if="form.provider === 'custom'">
<el-input v-model="form.model" placeholder="gpt-4o-mini" />
</el-form-item>
<el-form-item :label="'API ' + $t('commons.table.type')" prop="apiType" v-if="form.provider === 'custom'">
<el-select v-model="form.apiType">
<el-option label="openai-completions" value="openai-completions" />
<el-option label="openai-responses" value="openai-responses" />
</el-select>
</el-form-item>
<el-form-item label="Max Tokens" prop="maxTokens" v-if="form.provider === 'custom'">
<el-input-number v-model="form.maxTokens" :min="1" :max="2000000" />
</el-form-item>
<el-form-item label="Context Window" prop="contextWindow" v-if="form.provider === 'custom'">
<el-input-number v-model="form.contextWindow" :min="1" :max="2000000" />
</el-form-item> </el-form-item>
<el-form-item :label="$t('website.remark')" prop="remark"> <el-form-item :label="$t('website.remark')" prop="remark">
<el-input v-model="form.remark" /> <el-input v-model="form.remark" />
@@ -45,6 +61,7 @@ import { FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import { createAgentAccount, getAgentProviders, updateAgentAccount } from '@/api/modules/ai'; import { createAgentAccount, getAgentProviders, updateAgentAccount } from '@/api/modules/ai';
import i18n from '@/lang'; import i18n from '@/lang';
import { getAgentProviderDisplayName } from '@/utils/agent';
const emit = defineEmits(['search']); const emit = defineEmits(['search']);
@@ -59,6 +76,10 @@ const form = reactive({
provider: '', provider: '',
name: '', name: '',
baseURL: '', baseURL: '',
model: '',
apiType: 'openai-completions',
maxTokens: 8192,
contextWindow: 128000,
apiKey: '', apiKey: '',
remark: '', remark: '',
syncAgents: false, syncAgents: false,
@@ -73,6 +94,8 @@ const rules = reactive({
name: [Rules.requiredInput], name: [Rules.requiredInput],
apiKey: [Rules.requiredInput], apiKey: [Rules.requiredInput],
baseURL: [Rules.requiredInput], baseURL: [Rules.requiredInput],
model: [Rules.requiredInput],
apiType: [Rules.requiredSelect],
}); });
const submit = async () => { const submit = async () => {
@@ -88,6 +111,10 @@ const submit = async () => {
name: form.name, name: form.name,
baseURL: form.baseURL, baseURL: form.baseURL,
apiKey: form.apiKey, apiKey: form.apiKey,
model: form.model,
apiType: form.apiType,
maxTokens: form.maxTokens,
contextWindow: form.contextWindow,
remark: form.remark, remark: form.remark,
syncAgents: form.syncAgents, syncAgents: form.syncAgents,
}); });
@@ -97,6 +124,10 @@ const submit = async () => {
name: form.name, name: form.name,
baseURL: form.baseURL, baseURL: form.baseURL,
apiKey: form.apiKey, apiKey: form.apiKey,
model: form.model,
apiType: form.apiType,
maxTokens: form.maxTokens,
contextWindow: form.contextWindow,
remark: form.remark, remark: form.remark,
}); });
} }
@@ -111,6 +142,10 @@ const handleClose = () => {
formRef.value?.resetFields(); formRef.value?.resetFields();
loading.value = false; loading.value = false;
form.id = 0; form.id = 0;
form.model = '';
form.apiType = 'openai-completions';
form.maxTokens = 8192;
form.contextWindow = 128000;
form.syncAgents = false; form.syncAgents = false;
}; };
@@ -120,6 +155,10 @@ interface OpenParams {
name?: string; name?: string;
baseURL?: string; baseURL?: string;
apiKey?: string; apiKey?: string;
model?: string;
apiType?: string;
maxTokens?: number;
contextWindow?: number;
remark?: string; remark?: string;
} }
@@ -132,6 +171,10 @@ const openDrawer = async (params?: OpenParams) => {
form.name = params.name || ''; form.name = params.name || '';
form.baseURL = params.baseURL || ''; form.baseURL = params.baseURL || '';
form.apiKey = params.apiKey || ''; form.apiKey = params.apiKey || '';
form.model = params.model || '';
form.apiType = params.apiType || 'openai-completions';
form.maxTokens = params.maxTokens || 8192;
form.contextWindow = params.contextWindow || 128000;
form.remark = params.remark || ''; form.remark = params.remark || '';
form.syncAgents = false; form.syncAgents = false;
return; return;
@@ -140,6 +183,10 @@ const openDrawer = async (params?: OpenParams) => {
form.name = ''; form.name = '';
form.baseURL = ''; form.baseURL = '';
form.apiKey = ''; form.apiKey = '';
form.model = '';
form.apiType = 'openai-completions';
form.maxTokens = 8192;
form.contextWindow = 128000;
form.remark = ''; form.remark = '';
form.syncAgents = false; form.syncAgents = false;
if (providerOptions.value.length === 0) { if (providerOptions.value.length === 0) {
@@ -158,7 +205,7 @@ const loadProviders = async () => {
const data = res.data || []; const data = res.data || [];
providerOptions.value = data.map((item) => ({ providerOptions.value = data.map((item) => ({
value: item.provider, value: item.provider,
label: item.displayName || item.provider, label: getAgentProviderDisplayName(item.provider, item.displayName),
})); }));
providerBaseURL.value = data.reduce((acc, item) => { providerBaseURL.value = data.reduce((acc, item) => {
acc[item.provider] = item.baseUrl || ''; acc[item.provider] = item.baseUrl || '';
@@ -171,6 +218,14 @@ const loadProviders = async () => {
}; };
const handleProviderChange = () => { const handleProviderChange = () => {
if (form.provider === 'custom') {
form.baseURL = '';
form.apiType = form.apiType || 'openai-completions';
form.maxTokens = form.maxTokens || 8192;
form.contextWindow = form.contextWindow || 128000;
form.model = form.model || '';
return;
}
if (form.provider !== 'ollama') { if (form.provider !== 'ollama') {
form.baseURL = providerBaseURL.value[form.provider] || ''; form.baseURL = providerBaseURL.value[form.provider] || '';
} else { } else {
+6 -1
View File
@@ -14,7 +14,7 @@
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="200" /> <el-table-column :label="$t('commons.table.name')" prop="name" min-width="200" />
<el-table-column :label="$t('aiTools.agents.provider')" prop="provider" width="120"> <el-table-column :label="$t('aiTools.agents.provider')" prop="provider" width="120">
<template #default="{ row }"> <template #default="{ row }">
{{ row.providerName || row.provider }} {{ getAgentProviderDisplayName(row.provider, row.providerName) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('aiTools.agents.baseUrl')" prop="baseUrl" min-width="200" /> <el-table-column :label="$t('aiTools.agents.baseUrl')" prop="baseUrl" min-width="200" />
@@ -53,6 +53,7 @@ import AddDialog from '@/views/ai/agents/model/add/index.vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import i18n from '@/lang'; import i18n from '@/lang';
import { dateFormat } from '@/utils/util'; import { dateFormat } from '@/utils/util';
import { getAgentProviderDisplayName } from '@/utils/agent';
const items = ref<AI.AgentAccountItem[]>([]); const items = ref<AI.AgentAccountItem[]>([]);
const addRef = ref(); const addRef = ref();
@@ -103,6 +104,10 @@ const onEdit = (row: AI.AgentAccountItem) => {
name: row.name, name: row.name,
baseURL: row.baseUrl, baseURL: row.baseUrl,
apiKey: row.apiKey, apiKey: row.apiKey,
model: row.model,
apiType: row.apiType,
maxTokens: row.maxTokens,
contextWindow: row.contextWindow,
remark: row.remark, remark: row.remark,
}); });
} }