From 69a2f6f37f1ce276eeeaa1feead115b460a27abf Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <1paneldev@sina.com> Date: Fri, 17 Jul 2026 10:53:37 +0800 Subject: [PATCH] feat: Support fetching model lists for model accounts --- agent/app/api/v2/agents.go | 21 + agent/app/dto/agents.go | 56 +- agent/app/model/agent_account.go | 2 + agent/app/model/agent_account_model.go | 12 +- agent/app/provider/catalog.go | 567 ++++++++---------- agent/app/provider/discover.go | 64 ++ agent/app/provider/openclaw.go | 81 +-- agent/app/provider/verify.go | 186 +++--- agent/app/service/agents.go | 288 +++++---- agent/app/service/agents_hermes.go | 37 +- agent/app/service/agents_utils.go | 392 ++++-------- agent/app/service/agents_website.go | 6 +- agent/cmd/server/conf/app.yaml | 2 +- agent/i18n/lang/en.yaml | 2 + agent/i18n/lang/es-ES.yaml | 2 + agent/i18n/lang/fa.yaml | 2 + agent/i18n/lang/ja.yaml | 2 + agent/i18n/lang/ko.yaml | 2 + agent/i18n/lang/ms.yaml | 2 + agent/i18n/lang/pt-BR.yaml | 2 + agent/i18n/lang/ru.yaml | 2 + agent/i18n/lang/tr.yaml | 2 + agent/i18n/lang/zh-Hant.yaml | 2 + agent/i18n/lang/zh.yaml | 2 + agent/init/migration/migrate.go | 3 + agent/init/migration/migrations/init.go | 54 +- .../utils/agent_account_model_pool.go | 66 +- agent/router/ro_ai.go | 1 + agent/utils/files/ai_search_llm.go | 1 + agent/utils/terminal/ai/client.go | 72 +-- agent/utils/terminal/ai/command_generator.go | 1 + agent/utils/terminal/ai/config_runtime.go | 20 +- core/cmd/server/conf/app.yaml | 2 +- frontend/src/api/interface/ai.ts | 33 +- frontend/src/api/modules/ai.ts | 4 + frontend/src/lang/modules/en.ts | 20 +- frontend/src/lang/modules/es-es.ts | 19 +- frontend/src/lang/modules/fa.ts | 16 +- frontend/src/lang/modules/ja.ts | 17 +- frontend/src/lang/modules/ko.ts | 18 +- frontend/src/lang/modules/ms.ts | 18 +- frontend/src/lang/modules/pt-br.ts | 19 +- frontend/src/lang/modules/ru.ts | 19 +- frontend/src/lang/modules/tr.ts | 19 +- frontend/src/lang/modules/zh-Hant.ts | 17 +- frontend/src/lang/modules/zh.ts | 17 +- frontend/src/utils/agent.ts | 3 + .../agent/config/tabs/skills/hermes.vue | 1 + .../agent/config/tabs/skills/openclaw.vue | 1 + .../src/views/ai/agents/model/add/index.vue | 394 ++++++++---- frontend/src/views/ai/agents/model/index.vue | 18 +- .../src/views/ai/agents/model/pool/index.vue | 45 -- .../ai-search/file-ai-search-drawer.vue | 10 +- .../src/views/terminal/setting/ai/index.vue | 10 +- 54 files changed, 1420 insertions(+), 1254 deletions(-) create mode 100644 agent/app/provider/discover.go diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index 6ca12b9abd5c..264c073b06de 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -500,6 +500,27 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) { helper.SuccessWithData(c, list) } +// @Tags AI +// @Summary Discover custom provider models +// @Accept json +// @Param request body dto.AgentAccountModelDiscoverReq true "request" +// @Success 200 {array} dto.AgentAccountModel +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/accounts/models/discover [post] +func (b *BaseApi) DiscoverAgentAccountModels(c *gin.Context) { + var req dto.AgentAccountModelDiscoverReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + list, err := agentService.DiscoverAccountModels(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, list) +} + // @Tags AI // @Summary Create model account model // @Accept json diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 7fb8975bf163..e87d83ee5734 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -118,8 +118,6 @@ type AgentItem struct { ProviderName string `json:"providerName"` Model string `json:"model"` APIType string `json:"apiType"` - MaxTokens int `json:"maxTokens"` - ContextWindow int `json:"contextWindow"` BaseURL string `json:"baseUrl"` APIKey string `json:"apiKey"` Token string `json:"token"` @@ -292,19 +290,22 @@ type AgentOverviewSnapshot struct { } 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"` + RecordID uint `json:"recordId"` + ID string `json:"id"` + Name string `json:"name"` } type AgentAccountModelReq struct { AccountID uint `json:"accountId" validate:"required"` } +type AgentAccountModelDiscoverReq struct { + Provider string `json:"provider" validate:"required"` + BaseURL string `json:"baseURL" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + APIType string `json:"apiType" validate:"required"` +} + type AgentAccountModelCreateReq struct { AccountID uint `json:"accountId" validate:"required"` Model AgentAccountModel `json:"model" validate:"required"` @@ -328,6 +329,8 @@ type AgentAccountCreateReq struct { BaseURL string `json:"baseURL"` Models []AgentAccountModel `json:"models"` APIType string `json:"apiType" validate:"required"` + AuthMode string `json:"authMode"` + VerifyModel string `json:"verifyModel"` Remark string `json:"remark"` } @@ -338,6 +341,8 @@ type AgentAccountUpdateReq struct { RememberAPIKey bool `json:"rememberApiKey"` BaseURL string `json:"baseURL"` APIType string `json:"apiType" validate:"required"` + AuthMode string `json:"authMode"` + VerifyModel string `json:"verifyModel"` Remark string `json:"remark"` SyncAgents bool `json:"syncAgents"` } @@ -346,6 +351,9 @@ type AgentAccountVerifyReq struct { Provider string `json:"provider" validate:"required"` APIKey string `json:"apiKey" validate:"required"` BaseURL string `json:"baseURL"` + APIType string `json:"apiType" validate:"required"` + AuthMode string `json:"authMode"` + Model string `json:"model"` } type AgentAccountDeleteReq struct { @@ -373,26 +381,34 @@ type AgentAccountInfo struct { BaseURL string `json:"baseUrl"` Models []AgentAccountModel `json:"models"` APIType string `json:"apiType"` + AuthMode string `json:"authMode"` + VerifyModel string `json:"verifyModel"` Verified bool `json:"verified"` Remark string `json:"remark"` CreatedAt time.Time `json:"createdAt"` } type ProviderModelInfo struct { - ID string `json:"id"` - Name string `json:"name"` - ContextWindow int `json:"contextWindow"` - MaxTokens int `json:"maxTokens"` - Reasoning bool `json:"reasoning"` - Input []string `json:"input"` + ID string `json:"id"` + Name string `json:"name"` +} + +type ProviderAPIInfo struct { + APIType string `json:"apiType"` + BaseURL string `json:"baseUrl"` + EditableBaseURL bool `json:"editableBaseUrl"` + DefaultAuthMode string `json:"defaultAuthMode"` + AuthModes []string `json:"authModes"` } type ProviderInfo struct { - Sort uint `json:"-"` - Provider string `json:"provider"` - DisplayName string `json:"displayName"` - BaseURL string `json:"baseUrl"` - Models []ProviderModelInfo `json:"models"` + Sort uint `json:"-"` + Provider string `json:"provider"` + DisplayName string `json:"displayName"` + BaseURL string `json:"baseUrl"` + DefaultAPIType string `json:"defaultApiType"` + APITypes []ProviderAPIInfo `json:"apiTypes"` + Models []ProviderModelInfo `json:"models"` } type AgentFeishuConfigReq struct { diff --git a/agent/app/model/agent_account.go b/agent/app/model/agent_account.go index 2f499f7a4c4c..afae905ac043 100644 --- a/agent/app/model/agent_account.go +++ b/agent/app/model/agent_account.go @@ -7,6 +7,8 @@ type AgentAccount struct { APIKey string `json:"apiKey"` BaseURL string `json:"baseUrl"` APIType string `json:"apiType"` + AuthMode string `json:"authMode"` + VerifyModel string `json:"verifyModel"` RememberAPIKey bool `json:"rememberApiKey"` Verified bool `json:"verified"` Remark string `json:"remark"` diff --git a/agent/app/model/agent_account_model.go b/agent/app/model/agent_account_model.go index bd09e9225911..9b271fc3baed 100644 --- a/agent/app/model/agent_account_model.go +++ b/agent/app/model/agent_account_model.go @@ -2,14 +2,10 @@ 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"` + AccountID uint `json:"accountId" gorm:"index"` + Model string `json:"model" gorm:"index"` + Name string `json:"name"` + SortOrder int `json:"sortOrder" gorm:"index"` } func (AgentAccountModel) TableName() string { diff --git a/agent/app/provider/catalog.go b/agent/app/provider/catalog.go index 8ab705249459..92b5ffca0bcc 100644 --- a/agent/app/provider/catalog.go +++ b/agent/app/provider/catalog.go @@ -1,320 +1,181 @@ package provider import ( + "fmt" + "net/url" "strings" ) -type Model struct { - ID string - Name string - ContextWindow int - MaxTokens int - Reasoning bool - Input []string +type APIConfig struct { + APIType string + BaseURL string + EditableBaseURL bool + DefaultAuthMode string + AuthModes []string } -type RuntimeDefault struct { - APIType string - ContextWindow int - MaxTokens int - Input []string +const ( + AuthModeBearer = "bearer" + AuthModeXAPIKey = "x-api-key" +) + +type Model struct { + ID string + Name string } type Meta struct { Key string DisplayName string Sort uint - DefaultBaseURL string + DefaultAPIType string + APIConfigs []APIConfig EnvKey string - Default RuntimeDefault Models []Model } var catalog = map[string]Meta{ "custom": { - Key: "custom", - DisplayName: "Custom", - Sort: 10, - DefaultBaseURL: "", - EnvKey: "CUSTOM_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 128000, - MaxTokens: 8192, - Input: []string{"text"}, - }, - Models: []Model{}, + Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY", + APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"), }, "ollama": { - Key: "ollama", - DisplayName: "Ollama", - Sort: 15, - Default: RuntimeDefault{ - APIType: "openai-responses", - ContextWindow: 160000, - MaxTokens: 8192, - Input: []string{"text"}, - }, + Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses", + APIConfigs: editableAPIConfigs("openai-responses", "openai-completions"), }, "vllm": { - Key: "vllm", - DisplayName: "vLLM", - Sort: 20, - DefaultBaseURL: "", - EnvKey: "VLLM_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 128000, - MaxTokens: 8192, - Input: []string{"text"}, - }, - Models: []Model{}, + Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY", + APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"), }, "deepseek": { - Key: "deepseek", - DisplayName: "DeepSeek", - Sort: 25, - DefaultBaseURL: "https://api.deepseek.com/v1", - EnvKey: "DEEPSEEK_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 131072, - MaxTokens: 8192, - Input: []string{"text"}, - }, - Models: []Model{ - {ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"}, - {ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"}, + Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY", + APIConfigs: []APIConfig{ + {APIType: "openai-completions", BaseURL: "https://api.deepseek.com/v1"}, + anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey), }, + Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}}, }, "bailian-coding-plan": { - Key: "bailian-coding-plan", - DisplayName: "阿里云百炼 Coding Plan", - Sort: 30, - DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1", - EnvKey: "QWEN_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 256000, - MaxTokens: 8192, - Input: []string{"text"}, + Key: "bailian-coding-plan", DisplayName: "阿里云百炼 Coding Plan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY", + APIConfigs: []APIConfig{ + {APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"}, + anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer), }, Models: []Model{ - {ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true}, - {ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true}, - {ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true}, - {ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true}, - {ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true}, - {ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true}, - {ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true}, - {ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true}, + {ID: "qwen3-coder-plus", Name: "Qwen3-Coder-Plus"}, + {ID: "qwen3-max-2026-01-23", Name: "Qwen3-Max-2026-01-23"}, + {ID: "qwen3-coder-next", Name: "Qwen3-Coder-Next"}, + {ID: "glm-4.7", Name: "GLM-4.7"}, + {ID: "kimi-k2.5", Name: "Kimi K2.5"}, + {ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"}, + {ID: "glm-5", Name: "GLM-5"}, + {ID: "MiniMax-M2.5", Name: "MiniMax M2.5"}, + {ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"}, + {ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"}, }, }, "ark-coding-plan": { - Key: "ark-coding-plan", - DisplayName: "方舟 Coding Plan", - Sort: 35, - DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3", - EnvKey: "ARK_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 256000, - MaxTokens: 4096, - Input: []string{"text"}, + Key: "ark-coding-plan", DisplayName: "方舟 Coding Plan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY", + APIConfigs: []APIConfig{ + {APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"}, + anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer), }, Models: []Model{ - {ID: "ark-coding-plan/ark-code-latest", Name: "Ark Coding Plan", ContextWindow: 256000, MaxTokens: 4096}, - {ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao Seed Code", ContextWindow: 256000, MaxTokens: 4096}, - {ID: "ark-coding-plan/glm-4.7", Name: "GLM 4.7 Coding", ContextWindow: 200000, MaxTokens: 4096}, - {ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi K2 Thinking", ContextWindow: 256000, MaxTokens: 4096}, - {ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi K2.5 Coding", ContextWindow: 256000, MaxTokens: 4096}, - {ID: "ark-coding-plan/doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview", ContextWindow: 256000, MaxTokens: 4096}, + {ID: "ark-code-latest", Name: "Ark Coding Plan"}, {ID: "doubao-seed-code", Name: "Doubao Seed Code"}, + {ID: "glm-4.7", Name: "GLM 4.7 Coding"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}, + {ID: "kimi-k2.5", Name: "Kimi K2.5 Coding"}, {ID: "doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview"}, }, }, "zai": { - Key: "zai", - DisplayName: "Z.ai", - Sort: 40, - DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4", - EnvKey: "ZAI_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 204800, - MaxTokens: 131072, - Input: []string{"text"}, - }, - Models: []Model{ - {ID: "zai/glm-5", Name: "GLM-5", Reasoning: true}, - {ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true}, - {ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true}, - {ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true}, - }, + Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY", + APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}}, + Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}}, }, "minimax": { - Key: "minimax", - DisplayName: "MiniMax (CN)", - Sort: 45, - DefaultBaseURL: "https://api.minimaxi.com/anthropic", - EnvKey: "MINIMAX_API_KEY", - Default: RuntimeDefault{ - APIType: "anthropic-messages", - ContextWindow: 200000, - MaxTokens: 8192, - Input: []string{"text"}, - }, - Models: []Model{ - {ID: "minimax/MiniMax-M3", Name: "MiniMax M3", ContextWindow: 1000000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}}, - {ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"}, - {ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}, + Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY", + APIConfigs: []APIConfig{ + anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer), + {APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"}, }, + Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}}, }, "xiaomi": { - Key: "xiaomi", - DisplayName: "Xiaomi", - Sort: 46, - DefaultBaseURL: "https://api.xiaomimimo.com/v1", - EnvKey: "XIAOMI_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 262144, - MaxTokens: 8192, - Input: []string{"text"}, - }, - Models: []Model{ - {ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash", ContextWindow: 262144, MaxTokens: 8192, Input: []string{"text"}}, - {ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro", ContextWindow: 1048576, MaxTokens: 32000, Reasoning: true, Input: []string{"text"}}, - {ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni", ContextWindow: 262144, MaxTokens: 32000, Reasoning: true, Input: []string{"text", "image"}}, + Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY", + APIConfigs: []APIConfig{ + {APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"}, + anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer), }, + Models: []Model{{ID: "mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash"}, {ID: "mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro"}, {ID: "mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni"}}, }, "kimi": { - Key: "kimi", - DisplayName: "Kimi (CN)", - Sort: 50, - DefaultBaseURL: "https://api.moonshot.cn/v1", - EnvKey: "KIMI_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 256000, - MaxTokens: 8192, - Input: []string{"text", "image"}, - }, - Models: []Model{ - {ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true}, - {ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, - {ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true}, - }, + Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY", + APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.cn/v1"}}, + Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}}, }, "kimi-coding": { - Key: "kimi-coding", - DisplayName: "Kimi Coding", - Sort: 51, - DefaultBaseURL: "https://api.kimi.com/coding/", - EnvKey: "KIMI_API_KEY", - Default: RuntimeDefault{ - APIType: "anthropic-messages", - ContextWindow: 262144, - MaxTokens: 32768, - Input: []string{"text", "image"}, - }, - Models: []Model{ - {ID: "kimi-coding/kimi-code", Name: "Kimi Code", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}}, - {ID: "kimi-coding/k2p5", Name: "Kimi K2.5", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}}, - }, + Key: "kimi-coding", DisplayName: "Kimi Coding", Sort: 51, DefaultAPIType: "anthropic-messages", EnvKey: "KIMI_API_KEY", + APIConfigs: []APIConfig{anthropicAPIConfig("https://api.kimi.com/coding/", AuthModeXAPIKey)}, + Models: []Model{{ID: "kimi-code", Name: "Kimi Code"}, {ID: "k2p5", Name: "Kimi K2.5"}}, }, "openai": { - Key: "openai", - DisplayName: "OpenAI", - Sort: 55, - DefaultBaseURL: "https://api.openai.com/v1", - EnvKey: "OPENAI_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-responses", - ContextWindow: 272000, - MaxTokens: 128000, - Input: []string{"text", "image"}, - }, - Models: []Model{ - {ID: "openai/gpt-5.4", Name: "gpt-5.4", ContextWindow: 272000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}}, - {ID: "openai/gpt-5.4-pro", Name: "gpt-5.4-pro", ContextWindow: 1050000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}}, - {ID: "openai/gpt-5.4-mini", Name: "gpt-5.4-mini", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}}, - {ID: "openai/gpt-5.4-nano", Name: "gpt-5.4-nano", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}}, + Key: "openai", DisplayName: "OpenAI", Sort: 55, DefaultAPIType: "openai-responses", EnvKey: "OPENAI_API_KEY", + APIConfigs: []APIConfig{ + {APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"}, + {APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"}, }, + Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}}, }, "openrouter": { - Key: "openrouter", - DisplayName: "OpenRouter", - Sort: 56, - DefaultBaseURL: "https://openrouter.ai/api/v1", - EnvKey: "OPENROUTER_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 128000, - MaxTokens: 8192, - Input: []string{"text"}, - }, - Models: []Model{ - {ID: "openrouter/free", Name: "openrouter/free"}, - {ID: "openrouter/auto", Name: "openrouter/auto"}, - }, + Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY", + APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"}}, + Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}}, }, "anthropic": { - Key: "anthropic", - DisplayName: "Anthropic", - Sort: 60, - DefaultBaseURL: "https://api.anthropic.com", - EnvKey: "ANTHROPIC_API_KEY", - Default: RuntimeDefault{ - APIType: "anthropic-messages", - ContextWindow: 256000, - MaxTokens: 8192, - Input: []string{"text", "image"}, - }, - Models: []Model{ - {ID: "anthropic/claude-sonnet-4-6", Name: "Claude Sonnet 4.6", Reasoning: true}, - {ID: "anthropic/claude-opus-4-6", Name: "Claude Opus 4.6", Reasoning: true}, - {ID: "anthropic/claude-opus-4-5", Name: "Claude Opus 4.5"}, - {ID: "anthropic/claude-sonnet-4-5", Name: "Claude Sonnet 4.5"}, - {ID: "anthropic/claude-haiku-4-5", Name: "Claude Haiku 4.5"}, - }, + Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY", + APIConfigs: []APIConfig{anthropicAPIConfig("https://api.anthropic.com", AuthModeXAPIKey)}, + Models: []Model{{ID: "claude-sonnet-4-6", Name: "Claude Sonnet 4.6"}, {ID: "claude-opus-4-6", Name: "Claude Opus 4.6"}, {ID: "claude-opus-4-5", Name: "Claude Opus 4.5"}, {ID: "claude-sonnet-4-5", Name: "Claude Sonnet 4.5"}, {ID: "claude-haiku-4-5", Name: "Claude Haiku 4.5"}}, }, "gemini": { - Key: "gemini", - DisplayName: "Gemini", - Sort: 65, - DefaultBaseURL: "https://generativelanguage.googleapis.com", - EnvKey: "GEMINI_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 256000, - MaxTokens: 8192, - Input: []string{"text", "image"}, - }, - Models: []Model{ - {ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true}, - {ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"}, - {ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true}, - }, + Key: "gemini", DisplayName: "Gemini", Sort: 65, DefaultAPIType: "gemini-generate-content", EnvKey: "GEMINI_API_KEY", + APIConfigs: []APIConfig{{APIType: "gemini-generate-content", BaseURL: "https://generativelanguage.googleapis.com"}}, + Models: []Model{{ID: "gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"}, {ID: "gemini-flash-latest", Name: "Gemini Flash Latest"}, {ID: "gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"}}, }, "moonshot": { - Key: "moonshot", - DisplayName: "Moonshot (Global)", - Sort: 70, - DefaultBaseURL: "https://api.moonshot.ai/v1", - EnvKey: "MOONSHOT_API_KEY", - Default: RuntimeDefault{ - APIType: "openai-completions", - ContextWindow: 256000, - MaxTokens: 8192, - Input: []string{"text"}, - }, - Models: []Model{ - {ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true}, - {ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, - {ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true}, - }, + Key: "moonshot", DisplayName: "Moonshot (Global)", Sort: 70, DefaultAPIType: "openai-completions", EnvKey: "MOONSHOT_API_KEY", + APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}}, + Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}}, }, } +func editableAPIConfigs(apiTypes ...string) []APIConfig { + configs := make([]APIConfig, 0, len(apiTypes)) + for _, apiType := range apiTypes { + if apiType == "anthropic-messages" { + config := anthropicAPIConfig("", AuthModeXAPIKey, AuthModeBearer) + config.EditableBaseURL = true + configs = append(configs, config) + continue + } + configs = append(configs, APIConfig{APIType: apiType, EditableBaseURL: true}) + } + return configs +} + +func anthropicAPIConfig(baseURL, defaultAuthMode string, additionalAuthModes ...string) APIConfig { + authModes := []string{defaultAuthMode} + for _, authMode := range additionalAuthModes { + if authMode != defaultAuthMode { + authModes = append(authModes, authMode) + } + } + return APIConfig{ + APIType: "anthropic-messages", + BaseURL: baseURL, + DefaultAuthMode: defaultAuthMode, + AuthModes: authModes, + } +} + func Get(key string) (Meta, bool) { meta, ok := catalog[key] if !ok { @@ -331,107 +192,159 @@ func All() map[string]Meta { return result } -func DefaultBaseURL(key string) (string, bool) { +func FindAPIConfig(key, apiType string) (APIConfig, bool) { meta, ok := catalog[key] - if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" { - return "", false + if !ok { + return APIConfig{}, false + } + target := strings.TrimSpace(apiType) + if target == "" { + target = meta.DefaultAPIType + } + for _, config := range meta.APIConfigs { + if config.APIType == target { + return config, true + } } - return meta.DefaultBaseURL, true + return APIConfig{}, false } -func EnvKey(key string) string { - meta, ok := catalog[key] +func ResolveAuthMode(provider, apiType, requested string) (string, error) { + config, ok := FindAPIConfig(provider, apiType) if !ok { - return "" + return "", fmt.Errorf("provider %s does not support api type %s", provider, apiType) } - return meta.EnvKey + if config.APIType != "anthropic-messages" { + return "", nil + } + authMode := strings.TrimSpace(requested) + if authMode == "" { + authMode = config.DefaultAuthMode + } + for _, allowed := range config.AuthModes { + if authMode == allowed { + return authMode, nil + } + } + return "", fmt.Errorf("provider %s does not support auth mode %s", provider, authMode) } -func DisplayName(key string) string { - meta, ok := catalog[key] - if !ok { - return key +func DefaultBaseURL(key string) (string, bool) { + config, ok := FindAPIConfig(key, "") + if !ok || strings.TrimSpace(config.BaseURL) == "" { + return "", false } - if strings.TrimSpace(meta.DisplayName) == "" { - return key + return config.BaseURL, true +} + +func DefaultAPIType(key string) string { + meta, ok := catalog[key] + if !ok || strings.TrimSpace(meta.DefaultAPIType) == "" { + return "openai-completions" } - return meta.DisplayName + return meta.DefaultAPIType } -func FindModel(key, modelID string) (Model, bool) { - meta, ok := Get(key) +func ResolveBaseURL(key, apiType, requested string) (string, error) { + config, ok := FindAPIConfig(key, apiType) if !ok { - return Model{}, false + return "", fmt.Errorf("provider %s does not support api type %s", key, apiType) } - for _, item := range meta.Models { - if item.ID == modelID { - return item, true - } + if !config.EditableBaseURL { + return strings.TrimRight(config.BaseURL, "/"), nil + } + baseURL := strings.TrimSpace(requested) + if baseURL == "" { + baseURL = config.BaseURL + } + if baseURL == "" { + return "", fmt.Errorf("base url is required") } - return Model{}, false + parsed, err := url.Parse(baseURL) + if err != nil || parsed.Scheme == "" || parsed.Host == "" { + return "", fmt.Errorf("invalid base url") + } + parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path) + parsed.RawQuery = "" + parsed.Fragment = "" + return strings.TrimRight(parsed.String(), "/"), nil } -func cloneMeta(meta Meta) Meta { - clone := meta - if len(meta.Default.Input) > 0 { - clone.Default.Input = make([]string, len(meta.Default.Input)) - copy(clone.Default.Input, meta.Default.Input) +func normalizeEndpointPath(apiType, value string) string { + path := strings.TrimRight(value, "/") + suffixes := []string{} + switch apiType { + case "openai-completions": + suffixes = []string{"/chat/completions"} + case "openai-responses": + suffixes = []string{"/responses"} + case "anthropic-messages": + suffixes = []string{"/v1/messages", "/messages"} } - if len(meta.Models) > 0 { - clone.Models = make([]Model, len(meta.Models)) - for i, item := range meta.Models { - clone.Models[i] = normalizeModel(meta, item) + for _, suffix := range suffixes { + if strings.HasSuffix(strings.ToLower(path), suffix) { + return path[:len(path)-len(suffix)] } } - return clone + return path } -func normalizeModel(meta Meta, 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.MaxTokens = meta.Default.MaxTokens - } - if clone.ContextWindow <= 0 { - clone.ContextWindow = meta.Default.ContextWindow +func EnvKey(key string) string { + meta, ok := catalog[key] + if !ok { + return "" } - if len(clone.Input) == 0 && len(meta.Default.Input) > 0 { - clone.Input = make([]string, len(meta.Default.Input)) - copy(clone.Input, meta.Default.Input) + return meta.EnvKey +} + +func DisplayName(key string) string { + meta, ok := catalog[key] + if !ok || strings.TrimSpace(meta.DisplayName) == "" { + return key } - return clone + return meta.DisplayName } -func ResolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) { - defaultAPIType := "openai-completions" - defaultMaxTokens := 8192 - defaultContextWindow := 256000 - if meta, ok := Get(provider); ok { - if meta.Default.APIType != "" { - defaultAPIType = meta.Default.APIType - } - if meta.Default.MaxTokens > 0 { - defaultMaxTokens = meta.Default.MaxTokens +func NormalizeModelID(provider, modelID string) string { + target := strings.TrimLeft(strings.TrimSpace(modelID), "/") + for _, prefix := range legacyModelPrefixes[provider] { + legacyPrefix := prefix + "/" + if !strings.HasPrefix(target, legacyPrefix) { + continue } - if meta.Default.ContextWindow > 0 { - defaultContextWindow = meta.Default.ContextWindow + candidate := strings.TrimSpace(strings.TrimPrefix(target, legacyPrefix)) + if candidate != "" { + return candidate } } - resolvedAPI := apiType - if strings.TrimSpace(apiType) == "" { - resolvedAPI = defaultAPIType - } - resolvedMaxTokens := defaultMaxTokens - resolvedContextWindow := defaultContextWindow - if maxTokens > 0 { - resolvedMaxTokens = maxTokens - } - if contextWindow > 0 { - resolvedContextWindow = contextWindow + return target +} + +var legacyModelPrefixes = map[string][]string{ + "custom": {"custom"}, + "vllm": {"custom"}, + "ollama": {"ollama"}, + "deepseek": {"deepseek"}, + "bailian-coding-plan": {"bailian-coding-plan"}, + "ark-coding-plan": {"ark-coding-plan"}, + "zai": {"zai"}, + "minimax": {"minimax"}, + "xiaomi": {"xiaomi"}, + "kimi": {"kimi", "moonshot"}, + "kimi-coding": {"kimi-coding"}, + "openai": {"openai"}, + "anthropic": {"anthropic"}, + "gemini": {"google", "gemini"}, + "moonshot": {"moonshot"}, +} + +func cloneMeta(meta Meta) Meta { + clone := meta + clone.APIConfigs = make([]APIConfig, len(meta.APIConfigs)) + for index, config := range meta.APIConfigs { + clone.APIConfigs[index] = config + clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...) } - return resolvedAPI, resolvedMaxTokens, resolvedContextWindow + clone.Models = append([]Model(nil), meta.Models...) + return clone } diff --git a/agent/app/provider/discover.go b/agent/app/provider/discover.go new file mode 100644 index 000000000000..549c760b4e57 --- /dev/null +++ b/agent/app/provider/discover.go @@ -0,0 +1,64 @@ +package provider + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" +) + +func DiscoverModels(baseURL, apiKey string) ([]string, error) { + req, err := http.NewRequest(http.MethodGet, buildModelDiscoveryURL(baseURL), nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+apiKey) + resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode >= http.StatusBadRequest { + return nil, fmt.Errorf("request failed: %s", resp.Status) + } + var payload struct { + Data []struct { + ID string `json:"id"` + } `json:"data"` + } + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return nil, err + } + models := make([]string, 0, len(payload.Data)) + seen := make(map[string]struct{}, len(payload.Data)) + for _, item := range payload.Data { + id := strings.TrimSpace(item.ID) + if id == "" { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + models = append(models, id) + } + if len(models) == 0 { + return nil, fmt.Errorf("no models found") + } + return models, nil +} + +func buildModelDiscoveryURL(baseURL string) string { + base := strings.TrimRight(strings.TrimSpace(baseURL), "/") + for _, apiType := range []string{"openai-completions", "openai-responses", "anthropic-messages"} { + base = normalizeEndpointPath(apiType, base) + } + switch { + case strings.HasSuffix(base, "/v1/models"): + return base + case strings.HasSuffix(base, "/v1"): + return base + "/models" + default: + return base + "/v1/models" + } +} diff --git a/agent/app/provider/openclaw.go b/agent/app/provider/openclaw.go index 60cd7f85a4f5..39ee5435fd58 100644 --- a/agent/app/provider/openclaw.go +++ b/agent/app/provider/openclaw.go @@ -15,72 +15,49 @@ type OpenClawProviderPatch struct { AuthHeader bool } -func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) { +func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL, apiKey string) (*OpenClawProviderPatch, error) { if modelName == "" { return nil, fmt.Errorf("model is required") } - resolvedAPIType, _, _ := ResolveRuntimeParams(provider, apiType, 0, 0) - modelID := resolveOpenClawModelID(provider, modelName) + resolvedAPIType := apiType + if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok { + resolvedAPIType = DefaultAPIType(provider) + } + resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode) + if err != nil { + return nil, err + } + usesBearer := resolvedAuthMode == AuthModeBearer + modelID := NormalizeModelID(provider, modelName) + providerKey := provider + preserveQualifiedModel := false switch provider { - case "deepseek": - return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil case "gemini": - return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil + providerKey = "google" + resolvedAPIType = "google-generative-ai" + usesBearer = false case "moonshot", "kimi": - return buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey), nil - case "bailian-coding-plan": - return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil - case "ark-coding-plan": - return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil - case "minimax": - return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, apiKey, baseURL, "anthropic-messages", true), nil - case "xiaomi": - return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, apiKey, baseURL, "openai-completions", false), nil - case "custom", "vllm": - return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil + providerKey = "moonshot" + resolvedAPIType = "openai-completions" + usesBearer = false case "ollama": - return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", baseURL, resolvedAPIType, false), nil - case "kimi-coding": - return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, apiKey, baseURL, "anthropic-messages", false), nil - case "zai": - return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, apiKey, baseURL, "openai-completions", false), nil - default: - return newOpenClawProviderPatch(modelName, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil + apiKey = "ollama" + usesBearer = false + case "openai", "openrouter", "anthropic": + preserveQualifiedModel = strings.Contains(modelName, "/") } -} -func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawProviderPatch { - providerKey := provider - primaryModel := modelName - if provider == "kimi" { - providerKey = "moonshot" - primaryModel = "moonshot/" + modelID + primaryModel := providerKey + "/" + modelID + if preserveQualifiedModel { + primaryModel = modelName } - return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false) -} - -func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch { return &OpenClawProviderPatch{ PrimaryModel: primaryModel, ProviderKey: providerKey, ModelID: modelID, APIKey: apiKey, BaseURL: baseURL, - APIType: apiType, - AuthHeader: authHeader, - } -} - -func resolveOpenClawModelID(provider, modelName string) string { - if provider == "custom" || provider == "vllm" { - target := strings.TrimLeft(modelName, "/") - if parts := strings.SplitN(target, "/", 2); len(parts) == 2 && parts[0] == "custom" { - return strings.TrimLeft(parts[1], "/") - } - return target - } - if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 { - return parts[1] - } - return modelName + APIType: resolvedAPIType, + AuthHeader: usesBearer, + }, nil } diff --git a/agent/app/provider/verify.go b/agent/app/provider/verify.go index f2d98159ed22..80894c06eb63 100644 --- a/agent/app/provider/verify.go +++ b/agent/app/provider/verify.go @@ -3,7 +3,9 @@ package provider import ( "bytes" "encoding/json" + "errors" "fmt" + "io" "net/http" "strings" "time" @@ -18,160 +20,108 @@ type VerifyRequest struct { Body []byte } -const ( - defaultVerifyTimeout = 30 * time.Second -) +type verifyErrorResponse struct { + Error struct { + Message string `json:"message"` + } `json:"error"` + Message string `json:"message"` +} + +const defaultVerifyTimeout = 30 * time.Second -func SkipVerification(key string) bool { - switch key { - case "custom", "vllm", "ollama", "kimi-coding": +func SkipVerification(provider string) bool { + switch provider { + case "vllm", "ollama", "kimi-coding": return true default: return false } } -func VerifyAccount(provider, baseURL, apiKey string) error { - req := BuildVerifyRequest(provider, baseURL, apiKey) - var body *bytes.Buffer - if len(req.Body) > 0 { - body = bytes.NewBuffer(req.Body) - } else { - body = bytes.NewBuffer(nil) - } - httpReq, err := http.NewRequest(req.Method, req.URL, body) +func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error { + req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model) + httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body)) if err != nil { return err } for key, value := range req.Headers { httpReq.Header.Set(key, value) } - resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq) + httpReq.Header.Set("Accept", "application/json") + resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(httpReq) if err != nil { return buserr.WithErr("ErrAgentAccountUnavailable", err) } defer resp.Body.Close() - if resp.StatusCode >= 400 { - return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status)) + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) + if readErr != nil { + return buserr.WithErr("ErrAgentAccountUnavailable", readErr) + } + return buserr.WithErr("ErrAgentAccountUnavailable", errors.New(verifyHTTPError(resp.StatusCode, body))) } return nil } -func verifyTimeout() time.Duration { - return defaultVerifyTimeout -} - -func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest { - base := strings.TrimRight(strings.TrimSpace(baseURL), "/") - headers := map[string]string{} - request := VerifyRequest{Method: http.MethodGet, Headers: headers} +func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest { + baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/") + headers := map[string]string{"Content-Type": "application/json"} + request := VerifyRequest{Method: http.MethodPost, Headers: headers} - switch provider { - case "anthropic", "kimi-coding": - headers["x-api-key"] = apiKey - headers["anthropic-version"] = "2023-06-01" - if strings.Contains(base, "/v1") { - request.URL = base + "/models" - } else { - request.URL = base + "/v1/models" - } - case "gemini": - request.Method = http.MethodPost - if strings.Contains(base, "/v1beta") { - request.URL = base + "/models/gemini-3-flash-preview:generateContent" - } else { - request.URL = base + "/v1beta/models/gemini-3-flash-preview:generateContent" - } + if provider == "gemini" { + request.URL = baseURL + "/v1beta/models/" + strings.TrimSpace(model) + ":generateContent" headers["x-goog-api-key"] = apiKey - headers["Content-Type"] = "application/json" request.Body = mustJSON(map[string]interface{}{ - "contents": []map[string]interface{}{{ - "parts": []map[string]string{{ - "text": "Explain how AI works in a few words", - }}, - }}, + "contents": []map[string]interface{}{{"parts": []map[string]string{{"text": "test"}}}}, }) - case "zai": - headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) - request.URL = base + "/models" - case "bailian-coding-plan": - request.Method = http.MethodPost - if !strings.Contains(base, "/v1") { - base = base + "/v1" - } - request.URL = base + "/chat/completions" - headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) - headers["Content-Type"] = "application/json" - request.Body = mustJSON(map[string]interface{}{ - "model": "qwen3.5-plus", - "messages": []map[string]string{{"role": "user", "content": "test"}}, - "max_tokens": 1, - }) - case "ark-coding-plan": - request.Method = http.MethodPost - if !strings.Contains(base, "/api/coding/v3") { - base = "https://ark.cn-beijing.volces.com/api/coding/v3" - } - request.URL = base + "/chat/completions" - headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) - headers["Content-Type"] = "application/json" - request.Body = mustJSON(map[string]interface{}{ - "model": "ark-code-latest", - "messages": []map[string]string{{"role": "user", "content": "test"}}, - "max_tokens": 1, - }) - case "minimax": - request.Method = http.MethodPost - headers["x-api-key"] = apiKey - headers["anthropic-version"] = "2023-06-01" - headers["Content-Type"] = "application/json" - if strings.Contains(base, "/v1") { - request.URL = base + "/messages" + return request + } + + switch apiType { + case "anthropic-messages": + request.URL = baseURL + "/v1/messages" + if authMode == AuthModeBearer { + headers["Authorization"] = "Bearer " + apiKey } else { - request.URL = base + "/v1/messages" + headers["x-api-key"] = apiKey } + headers["anthropic-version"] = "2023-06-01" request.Body = mustJSON(map[string]interface{}{ - "model": "MiniMax-M3", - "max_tokens": 1, - "messages": []map[string]interface{}{{ - "role": "user", - "content": []map[string]string{{ - "type": "text", - "text": "test", - }}, - }}, + "model": model, "max_tokens": 1, "stream": false, + "messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}}, }) - case "xiaomi": - request.Method = http.MethodPost - headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) - headers["Content-Type"] = "application/json" - if !strings.Contains(base, "/v1") { - base = base + "/v1" + case "openai-responses": + request.URL = baseURL + "/responses" + headers["Authorization"] = "Bearer " + apiKey + request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false}) + default: + request.URL = baseURL + "/chat/completions" + if provider != "ollama" || strings.TrimSpace(apiKey) != "" { + headers["Authorization"] = "Bearer " + apiKey } - request.URL = base + "/chat/completions" request.Body = mustJSON(map[string]interface{}{ - "model": "mimo-v2-flash", - "max_tokens": 1, - "messages": []map[string]string{{"role": "user", "content": "test"}}, + "model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false, }) - case "openrouter": - headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) - if strings.Contains(base, "/v1") { - request.URL = base + "/key" - } else { - request.URL = base + "/v1/key" - } - default: - headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) - if strings.Contains(base, "/v1") { - request.URL = base + "/models" - } else { - request.URL = base + "/v1/models" - } } return request } +func verifyHTTPError(statusCode int, body []byte) string { + message := strings.TrimSpace(string(body)) + var payload verifyErrorResponse + if err := json.Unmarshal(body, &payload); err == nil { + if value := strings.TrimSpace(payload.Error.Message); value != "" { + message = value + } else if value := strings.TrimSpace(payload.Message); value != "" { + message = value + } + } + if message == "" { + return fmt.Sprintf("validation request returned status %d", statusCode) + } + return message +} + func mustJSON(value interface{}) []byte { payload, err := json.Marshal(value) if err != nil { diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index f4aa8bb6c5d6..8533b14a81d8 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -8,8 +8,8 @@ import ( "os" "path" "sort" - "strconv" "strings" + "sync" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" @@ -30,6 +30,8 @@ import ( "gorm.io/gorm" ) +var agentAccountMutationMu sync.Mutex + type IAgentService interface { Create(req dto.AgentCreateReq) (*dto.AgentItem, error) BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) @@ -77,6 +79,7 @@ type IAgentService interface { PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) + DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) CreateAccountModel(req dto.AgentAccountModelCreateReq) error UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error @@ -165,8 +168,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { provider := "" baseURL := "" apiType := "" - maxTokens := 0 - contextWindow := 0 apiKey := "" runtimeModel := "" accountID := uint(0) @@ -197,8 +198,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { } storedModel = resolvedRuntime.StoredModel apiType = resolvedRuntime.APIType - maxTokens = resolvedRuntime.MaxTokens - contextWindow = resolvedRuntime.ContextWindow runtimeModel = resolvedRuntime.PrimaryModel apiKey = account.APIKey accountID = account.ID @@ -247,8 +246,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { params["PROVIDER"] = provider params["MODEL"] = runtimeModel params["API_TYPE"] = apiType - params["MAX_TOKENS"] = maxTokens - params["CONTEXT_WINDOW"] = contextWindow params["BASE_URL"] = baseURL params["API_KEY"] = apiKey params["OPENCLAW_GATEWAY_TOKEN"] = token @@ -291,22 +288,20 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { configPath = path.Join(appInstall.GetPath(), "data", "config.yaml") } agent := &model.Agent{ - Name: req.Name, - Remark: req.Remark, - AgentType: agentType, - Provider: provider, - Model: storedModel, - APIType: apiType, - MaxTokens: maxTokens, - ContextWindow: contextWindow, - BaseURL: baseURL, - APIKey: apiKey, - Token: token, - Status: appInstall.Status, - Message: appInstall.Message, - AppInstallID: appInstall.ID, - AccountID: accountID, - ConfigPath: configPath, + Name: req.Name, + Remark: req.Remark, + AgentType: agentType, + Provider: provider, + Model: storedModel, + APIType: apiType, + BaseURL: baseURL, + APIKey: apiKey, + Token: token, + Status: appInstall.Status, + Message: appInstall.Message, + AppInstallID: appInstall.ID, + AccountID: accountID, + ConfigPath: configPath, } if err := agentRepo.Create(agent); err != nil { return nil, err @@ -404,7 +399,7 @@ func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dt results = append(results, result) continue } - installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchSkillInstallTaskID(req.TaskID, agent.ID), agent.ID) + installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchTaskID(req.TaskID, "batch-skill-install", agent.ID), agent.ID) if err != nil { result.Message = err.Error() results = append(results, result) @@ -446,7 +441,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat if operate == constant.Delete { if err := a.Delete(dto.AgentDeleteReq{ ID: agent.ID, - TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID), + TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID), ForceDelete: req.ForceDelete, }); err != nil { result.Message = err.Error() @@ -474,7 +469,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{ InstallId: install.ID, Operate: operate, - TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID), + TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID), }); err != nil { result.Message = err.Error() } else { @@ -558,35 +553,19 @@ func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, [ DetailID: detail.ID, Backup: req.Backup, PullImage: req.PullImage, - TaskID: buildBatchUpgradeTaskID(req.TaskID, install.ID), + TaskID: buildBatchTaskID(req.TaskID, "batch-upgrade", install.ID), }, }) } return plans, results, nil } -func buildBatchUpgradeTaskID(taskID string, appInstallID uint) string { - taskID = strings.TrimSpace(taskID) - if taskID == "" { - taskID = fmt.Sprintf("batch-upgrade-%d-%d", appInstallID, time.Now().UnixNano()) - } - return fmt.Sprintf("%s-%d", taskID, appInstallID) -} - -func buildBatchSkillInstallTaskID(taskID string, agentID uint) string { - taskID = strings.TrimSpace(taskID) - if taskID == "" { - taskID = fmt.Sprintf("batch-skill-install-%d-%d", agentID, time.Now().UnixNano()) - } - return fmt.Sprintf("%s-%d", taskID, agentID) -} - -func buildBatchOperateTaskID(taskID string, agentID uint) string { +func buildBatchTaskID(taskID, prefix string, id uint) string { taskID = strings.TrimSpace(taskID) if taskID == "" { - taskID = fmt.Sprintf("batch-operate-%d-%d", agentID, time.Now().UnixNano()) + taskID = fmt.Sprintf("%s-%d-%d", prefix, id, time.Now().UnixNano()) } - return fmt.Sprintf("%s-%d", taskID, agentID) + return fmt.Sprintf("%s-%d", taskID, id) } func batchOperateSkipMessage(operate constant.AppOperate, status string) string { @@ -715,6 +694,7 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u account.RememberAPIKey = snapshot.RememberAPIKey account.BaseURL = snapshot.BaseURL account.APIType = snapshot.APIType + account.AuthMode = snapshot.AuthMode account.Remark = snapshot.Remark account.Verified = true @@ -722,6 +702,11 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u if err != nil { return 0, err } + verifyModel, err := resolveAgentAccountVerifyModel(account.Provider, snapshot.VerifyModel, initialModels) + if err != nil { + return 0, err + } + account.VerifyModel = verifyModel if err := global.DB.Transaction(func(tx *gorm.DB) error { if account.ID == 0 { if err := tx.Create(account).Error; err != nil { @@ -959,7 +944,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error return err } modelName := resolvedRuntime.StoredModel - apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow + apiType := resolvedRuntime.APIType confDir := path.Dir(agent.ConfigPath) if agent.AgentType == constant.AppHermesAgent { cfg, err := readHermesConfig(agent.ConfigPath) @@ -980,8 +965,6 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error agent.Provider = account.Provider agent.Model = modelName agent.APIType = apiType - agent.MaxTokens = maxTokens - agent.ContextWindow = contextWindow agent.BaseURL = account.BaseURL agent.APIKey = account.APIKey agent.AccountID = account.ID @@ -995,20 +978,29 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) { models := make([]dto.ProviderModelInfo, 0, len(def.Models)) for _, item := range def.Models { models = append(models, dto.ProviderModelInfo{ - ID: item.ID, - Name: item.Name, - ContextWindow: item.ContextWindow, - MaxTokens: item.MaxTokens, - Reasoning: item.Reasoning, - Input: append([]string(nil), item.Input...), + ID: item.ID, + Name: item.Name, + }) + } + apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs)) + for _, item := range def.APIConfigs { + apiTypes = append(apiTypes, dto.ProviderAPIInfo{ + APIType: item.APIType, + BaseURL: item.BaseURL, + EditableBaseURL: item.EditableBaseURL, + DefaultAuthMode: item.DefaultAuthMode, + AuthModes: item.AuthModes, }) } + baseURL, _ := providercatalog.DefaultBaseURL(key) providers = append(providers, dto.ProviderInfo{ - Sort: def.Sort, - Provider: key, - DisplayName: def.DisplayName, - BaseURL: def.DefaultBaseURL, - Models: models, + Sort: def.Sort, + Provider: key, + DisplayName: def.DisplayName, + BaseURL: baseURL, + DefaultAPIType: def.DefaultAPIType, + APITypes: apiTypes, + Models: models, }) } sort.Slice(providers, func(i, j int) bool { @@ -1018,11 +1010,21 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) { } func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { + agentAccountMutationMu.Lock() + defer agentAccountMutationMu.Unlock() provider := req.Provider - if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 { - return buserr.New("ErrRecordExist") + if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil { + return err + } + initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider}, req.Models) + if err != nil { + return err } - resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL) + verifyModel, err := resolveAgentAccountVerifyModel(provider, req.VerifyModel, initialModels) + if err != nil { + return err + } + resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel) if err != nil { return err } @@ -1032,14 +1034,12 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { APIKey: resolvedInput.APIKey, RememberAPIKey: req.RememberAPIKey, BaseURL: resolvedInput.BaseURL, - APIType: req.APIType, + APIType: resolvedInput.APIType, + AuthMode: resolvedInput.AuthMode, + VerifyModel: verifyModel, Verified: true, Remark: req.Remark, } - initialModels, err := buildInitialAgentAccountModels(account, req.Models) - if err != nil { - return err - } if err := global.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Create(account).Error; err != nil { return err @@ -1056,12 +1056,29 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { } func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { + agentAccountMutationMu.Lock() + defer agentAccountMutationMu.Unlock() account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID)) if err != nil { return err } provider := account.Provider - resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL) + if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil { + return err + } + models, err := loadAgentAccountModels(account) + if err != nil { + return err + } + requestedVerifyModel := req.VerifyModel + if strings.TrimSpace(requestedVerifyModel) == "" { + requestedVerifyModel = account.VerifyModel + } + verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models) + if err != nil { + return err + } + resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel) if err != nil { return err } @@ -1069,7 +1086,9 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { account.APIKey = resolvedInput.APIKey account.RememberAPIKey = req.RememberAPIKey account.BaseURL = resolvedInput.BaseURL - account.APIType = req.APIType + account.APIType = resolvedInput.APIType + account.AuthMode = resolvedInput.AuthMode + account.VerifyModel = verifyModel account.Remark = req.Remark account.Verified = true @@ -1115,17 +1134,33 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age BaseURL: item.BaseURL, Models: nil, APIType: item.APIType, + AuthMode: item.AuthMode, + VerifyModel: item.VerifyModel, Verified: item.Verified, Remark: item.Remark, CreatedAt: item.CreatedAt, }) } - for i := range items { - models, err := loadAgentAccountModels(&list[i]) - if err != nil { + if len(list) > 0 { + accountIDs := make([]uint, 0, len(list)) + for _, account := range list { + accountIDs = append(accountIDs, account.ID) + } + var rows []model.AgentAccountModel + if err := global.DB.Where("account_id IN ?", accountIDs).Order("account_id ASC, sort_order ASC, id ASC").Find(&rows).Error; err != nil { return 0, nil, err } - items[i].Models = models + modelsByAccount := make(map[uint][]dto.AgentAccountModel, len(list)) + for _, row := range rows { + modelsByAccount[row.AccountID] = append(modelsByAccount[row.AccountID], dto.AgentAccountModel{ + RecordID: row.ID, + ID: strings.TrimSpace(row.Model), + Name: strings.TrimSpace(row.Name), + }) + } + for index, account := range list { + items[index].Models = modelsByAccount[account.ID] + } } return count, items, nil } @@ -1142,6 +1177,21 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen return loadAgentAccountModels(account) } +func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) { + if req.APIType != "openai-completions" && req.APIType != "openai-responses" { + return nil, buserr.New("ErrAgentAccountModelsRequired") + } + baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL) + if err != nil { + return nil, buserr.WithErr("ErrAgentAccountUnavailable", err) + } + models, err := providercatalog.DiscoverModels(baseURL, req.APIKey) + if err != nil { + return nil, buserr.WithErr("ErrAgentAccountUnavailable", err) + } + return buildDiscoveredAgentAccountModels(models), nil +} + func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error { account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) if err != nil { @@ -1151,24 +1201,19 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err if err != nil { return err } - nextModel := cloneAgentAccountModel(req.Model) - if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok { - return buserr.New("ErrRecordExist") - } - inputPayload, err := json.Marshal(nextModel.Input) + nextModel, err := normalizeAgentAccountModel(account, req.Model) if err != nil { return err } + if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok { + return buserr.New("ErrRecordExist") + } sortOrder := len(models) + 1 record := &model.AgentAccountModel{ - AccountID: account.ID, - Model: nextModel.ID, - Name: nextModel.Name, - ContextWindow: nextModel.ContextWindow, - MaxTokens: nextModel.MaxTokens, - Reasoning: nextModel.Reasoning, - Input: string(inputPayload), - SortOrder: sortOrder, + AccountID: account.ID, + Model: nextModel.ID, + Name: nextModel.Name, + SortOrder: sortOrder, } if err := agentAccountModelRepo.Create(record); err != nil { return err @@ -1189,7 +1234,11 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err if err != nil { return err } - nextModel := cloneAgentAccountModel(req.Model) + nextModel, err := normalizeAgentAccountModel(account, req.Model) + if err != nil { + return err + } + nextModel.RecordID = req.Model.RecordID for _, item := range models { if item.RecordID == req.Model.RecordID { continue @@ -1209,19 +1258,18 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err if err := ensureAccountModelsNotBound(account, nextModels); err != nil { return err } - inputPayload, err := json.Marshal(nextModel.Input) - if err != nil { - return err - } + previousModelID := record.Model record.Model = nextModel.ID record.Name = nextModel.Name - record.ContextWindow = nextModel.ContextWindow - record.MaxTokens = nextModel.MaxTokens - record.Reasoning = nextModel.Reasoning - record.Input = string(inputPayload) if err := agentAccountModelRepo.Save(record); err != nil { return err } + if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) { + account.VerifyModel = nextModel.ID + if err := agentAccountRepo.Save(account); err != nil { + return err + } + } terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateFileAIRuntimeCache() return a.syncAgentsByAccount(account) @@ -1232,9 +1280,13 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err if err != nil { return err } - if _, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)); err != nil { + record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)) + if err != nil { return err } + if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) { + return buserr.New("ErrAgentVerifyModelInUse") + } models, err := loadAgentAccountModels(account) if err != nil { return err @@ -1268,25 +1320,45 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error { } func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error { - _, err := resolveAgentAccountInput(req.Provider, req.APIKey, req.BaseURL) + _, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model) return err } 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 aiStatus, _ := settingRepo.GetValueByKey("AIStatus"); strings.EqualFold(strings.TrimSpace(aiStatus), constant.StatusEnable) { - if aiAccountID, _ := settingRepo.GetValueByKey("AIAccountID"); strings.TrimSpace(aiAccountID) == strconv.FormatUint(uint64(req.ID), 10) { + agentAccountMutationMu.Lock() + defer agentAccountMutationMu.Unlock() + if err := global.DB.Transaction(func(tx *gorm.DB) error { + var agentCount int64 + if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil { + return err + } + if agentCount > 0 { + return buserr.New("ErrAgentAccountBound") + } + used, err := agentAccountUsedBySetting(tx, req.ID, "AIStatus", "AIAccountID") + if err != nil { + return err + } + if used { return buserr.New("ErrTerminalAIAccountInUse") } - } - if err := agentAccountModelRepo.Delete(repo.WithByAccountID(req.ID)); err != nil { + used, err = agentAccountUsedBySetting(tx, req.ID, "FileAIStatus", "FileAIAccountID") + if err != nil { + return err + } + if used { + return buserr.New("ErrFileAIAccountInUse") + } + if err := tx.Where("account_id = ?", req.ID).Delete(&model.AgentAccountModel{}).Error; err != nil { + return err + } + return tx.Delete(&model.AgentAccount{}, req.ID).Error + }); err != nil { return err } terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateFileAIRuntimeCache() - return agentAccountRepo.DeleteByID(req.ID) + return nil } func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) { @@ -1557,7 +1629,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { return err } modelName := resolvedRuntime.StoredModel - apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow + apiType := resolvedRuntime.APIType confDir := path.Dir(agent.ConfigPath) switch agent.AgentType { case constant.AppOpenclaw: @@ -1585,8 +1657,6 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { agent.Provider = account.Provider agent.Model = modelName agent.APIType = apiType - agent.MaxTokens = maxTokens - agent.ContextWindow = contextWindow _ = agentRepo.Save(&agent) } return nil diff --git a/agent/app/service/agents_hermes.go b/agent/app/service/agents_hermes.go index f9e676a3d822..e07bc7e2cf45 100644 --- a/agent/app/service/agents_hermes.go +++ b/agent/app/service/agents_hermes.go @@ -76,7 +76,7 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st } } - provider := resolveHermesProvider(account.Provider) + provider := resolveHermesProvider(account.Provider, account.APIType) configPath := path.Join(confDir, "config.yaml") cfg, err := readHermesConfigMap(configPath) if err != nil { @@ -458,11 +458,18 @@ func normalizeHermesTimezone(timezone string) string { return timezone } -func resolveHermesProvider(provider string) string { +func resolveHermesProvider(provider, apiType string) string { + if apiType == "anthropic-messages" && (provider == "deepseek" || provider == "bailian-coding-plan" || provider == "ark-coding-plan" || provider == "xiaomi") { + return "anthropic" + } switch provider { case "": return "custom" - case "openrouter", "anthropic", "gemini", "zai", "kimi-coding", "xiaomi": + case "moonshot": + return "kimi-coding" + case "kimi": + return "kimi-coding-cn" + case "openrouter", "anthropic", "deepseek", "gemini", "zai", "kimi-coding", "xiaomi": return provider case "minimax": return "minimax-cn" @@ -479,17 +486,7 @@ func resolveHermesModel(sourceProvider, targetProvider, modelName string) string if targetProvider != "custom" { return target } - if sourceProvider == "custom" || sourceProvider == "vllm" { - return normalizeCustomModel(target) - } - if strings.Contains(target, "/") { - parts := strings.SplitN(target, "/", 2) - model := strings.TrimSpace(parts[1]) - if model != "" { - return model - } - } - return target + return providercatalog.NormalizeModelID(sourceProvider, target) } func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) { @@ -500,7 +497,7 @@ func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels [ if configuredModel == "" { return "", buserr.New("ErrAgentModelNotInAccount") } - provider := resolveHermesProvider(account.Provider) + provider := resolveHermesProvider(account.Provider, account.APIType) for _, item := range accountModels { if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel { return item.ID, nil @@ -513,7 +510,8 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry { if account == nil { return nil } - if resolveHermesProvider(account.Provider) == "custom" { + resolvedProvider := resolveHermesProvider(account.Provider, account.APIType) + if resolvedProvider == "custom" { if account.APIKey == "" { return nil } @@ -531,6 +529,11 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry { } entries = append(entries, hermesEnvEntry{Key: key, Value: value}) } + if resolvedProvider == "anthropic" { + appendEntry("ANTHROPIC_API_KEY", apiKey) + appendEntry("ANTHROPIC_BASE_URL", baseURL) + return entries + } switch account.Provider { case "openrouter": @@ -538,6 +541,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry { appendEntry("OPENROUTER_BASE_URL", baseURL) case "anthropic": appendEntry("ANTHROPIC_API_KEY", apiKey) + appendEntry("ANTHROPIC_BASE_URL", baseURL) case "gemini": appendEntry("GOOGLE_API_KEY", apiKey) appendEntry("GEMINI_API_KEY", apiKey) @@ -607,6 +611,7 @@ func hermesManagedModelEnvKeys() []string { "OPENROUTER_API_KEY", "OPENROUTER_BASE_URL", "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", "GOOGLE_API_KEY", "GEMINI_API_KEY", "GEMINI_BASE_URL", diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index 4e78e9fd3231..74997258dc10 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -4,10 +4,12 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" + "errors" "fmt" "net/http" "net/url" "path" + "regexp" "strconv" "strings" "time" @@ -37,6 +39,39 @@ type resolvedAgentAccountInput struct { Provider string APIKey string BaseURL string + APIType string + AuthMode string +} + +func ensureAgentAccountNameAvailable(provider, name string, excludeID uint) error { + opts := []repo.DBOption{repo.WithByProvider(provider), repo.WithByName(name)} + if excludeID > 0 { + opts = append(opts, repo.WithByNOTID(excludeID)) + } + account, err := agentAccountRepo.GetFirst(opts...) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + if err != nil { + return err + } + if account != nil && account.ID > 0 { + return buserr.New("ErrRecordExist") + } + return nil +} + +func agentAccountUsedBySetting(tx *gorm.DB, accountID uint, statusKey, accountIDKey string) (bool, error) { + var settings []model.Setting + if err := tx.Where("key IN ?", []string{statusKey, accountIDKey}).Find(&settings).Error; err != nil { + return false, err + } + values := make(map[string]string, len(settings)) + for _, setting := range settings { + values[setting.Key] = setting.Value + } + return strings.EqualFold(strings.TrimSpace(values[statusKey]), constant.StatusEnable) && + strings.TrimSpace(values[accountIDKey]) == strconv.FormatUint(uint64(accountID), 10), nil } func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) { @@ -61,19 +96,26 @@ func ensureContainerRunning(containerName string) error { return nil } -func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) { +func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string) (resolvedAgentAccountInput, error) { resolvedAPIKey := strings.TrimSpace(apiKey) - resolvedBaseURL := strings.TrimSpace(baseURL) - if resolvedBaseURL == "" { - if requiresInitialAgentAccountModels(provider) { + resolvedAPIType := strings.TrimSpace(apiType) + resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode) + if err != nil { + return resolvedAgentAccountInput{}, err + } + resolvedBaseURL, err := providercatalog.ResolveBaseURL(provider, resolvedAPIType, baseURL) + if err != nil { + if strings.Contains(err.Error(), "base url is required") { return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired") } - if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok { - resolvedBaseURL = defaultURL - } + return resolvedAgentAccountInput{}, err + } + modelID = strings.TrimSpace(modelID) + if modelID == "" { + return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired") } if !providercatalog.SkipVerification(provider) { - if err := providercatalog.VerifyAccount(provider, resolvedBaseURL, resolvedAPIKey); err != nil { + if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil { return resolvedAgentAccountInput{}, err } } @@ -81,6 +123,8 @@ func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAc Provider: provider, APIKey: resolvedAPIKey, BaseURL: resolvedBaseURL, + APIType: resolvedAPIType, + AuthMode: resolvedAuthMode, }, nil } @@ -337,26 +381,24 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) { func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem { agentType := agent.AgentType item := dto.AgentItem{ - ID: agent.ID, - Name: agent.Name, - Remark: agent.Remark, - AgentType: agentType, - Provider: agent.Provider, - ProviderName: providercatalog.DisplayName(agent.Provider), - Model: agent.Model, - APIType: agent.APIType, - MaxTokens: agent.MaxTokens, - ContextWindow: agent.ContextWindow, - BaseURL: agent.BaseURL, - APIKey: maskKey(agent.APIKey), - Token: agent.Token, - Status: agent.Status, - Message: agent.Message, - AppInstallID: agent.AppInstallID, - WebsiteID: agent.WebsiteID, - AccountID: agent.AccountID, - ConfigPath: agent.ConfigPath, - CreatedAt: agent.CreatedAt, + ID: agent.ID, + Name: agent.Name, + Remark: agent.Remark, + AgentType: agentType, + Provider: agent.Provider, + ProviderName: providercatalog.DisplayName(agent.Provider), + Model: agent.Model, + APIType: agent.APIType, + BaseURL: agent.BaseURL, + APIKey: maskKey(agent.APIKey), + Token: agent.Token, + Status: agent.Status, + Message: agent.Message, + AppInstallID: agent.AppInstallID, + WebsiteID: agent.WebsiteID, + AccountID: agent.AccountID, + ConfigPath: agent.ConfigPath, + CreatedAt: agent.CreatedAt, } if appInstall != nil && appInstall.ID > 0 { item.Container = appInstall.ContainerName @@ -684,13 +726,9 @@ type modelProvider struct { } type modelEntry struct { - ID string `json:"id"` - Name string `json:"name"` - Reasoning bool `json:"reasoning"` - Input []string `json:"input"` - ContextWindow int `json:"contextWindow"` - MaxTokens int `json:"maxTokens"` - Cost modelCost `json:"cost"` + ID string `json:"id"` + Name string `json:"name"` + Input []string `json:"input,omitempty"` } func requiresOpenclawProviderModels(provider string) bool { @@ -710,13 +748,6 @@ func applyOpenclawModelsConfig(conf map[string]interface{}, models *modelsConfig return nil } -type modelCost struct { - Input float64 `json:"input"` - Output float64 `json:"output"` - CacheRead float64 `json:"cacheRead"` - CacheWrite float64 `json:"cacheWrite"` -} - type browserConfig struct { Enabled bool `json:"enabled"` ExecutablePath string `json:"executablePath"` @@ -1062,7 +1093,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s } func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) { - providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.BaseURL, account.APIKey) + providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.AuthMode, account.BaseURL, account.APIKey) if err != nil { return "", modelEntry{}, "", modelProvider{}, err } @@ -1074,28 +1105,24 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen }, nil } +var openclawVisionModelPattern = regexp.MustCompile(`(?i)(\b(gpt-4o|gpt-4\.1|gpt-[5-9]|o[134])\b|\bclaude-(3|4|sonnet|opus|haiku)\b|\bgemini\b|\b(qwen[\w.-]*-?vl|qwen-vl|qwen3\.[5-9]-plus)\b|\b(kimi-k2\.(5|6)|kimi-k2\.7-code|minimax-m3)\b|\b(vision|llava|pixtral|internvl|mllama|minicpm-v|glm-4v|omni)\b|(^|[-_/])vl([-_/]|$))`) + func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry { name := strings.TrimSpace(model.Name) if name == "" { name = strings.TrimSpace(modelID) } - return modelEntry{ - ID: strings.TrimSpace(modelID), - Name: name, - Reasoning: model.Reasoning, - Input: sanitizeAgentAccountModelInputs(model.Input), - ContextWindow: model.ContextWindow, - MaxTokens: model.MaxTokens, - Cost: modelCost{}, + entry := modelEntry{ID: strings.TrimSpace(modelID), Name: name} + if openclawVisionModelPattern.MatchString(modelID) { + entry.Input = []string{"text", "image"} } + return entry } type openclawAccountModelRuntime struct { - StoredModel string - PrimaryModel string - APIType string - MaxTokens int - ContextWindow int + StoredModel string + PrimaryModel string + APIType string } func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) { @@ -1104,11 +1131,9 @@ func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.Age return openclawAccountModelRuntime{}, err } return openclawAccountModelRuntime{ - StoredModel: model.ID, - PrimaryModel: primaryModel, - APIType: account.APIType, - MaxTokens: model.MaxTokens, - ContextWindow: model.ContextWindow, + StoredModel: model.ID, + PrimaryModel: primaryModel, + APIType: account.APIType, }, nil } @@ -1128,15 +1153,11 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto if account == nil { return nil, fmt.Errorf("account is required") } - if requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 { + if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 { return nil, buserr.New("ErrAgentAccountSingleInitialModel") } if len(requested) > 0 { - models := make([]dto.AgentAccountModel, 0, len(requested)) - for _, item := range requested { - models = append(models, cloneAgentAccountModel(item)) - } - return models, nil + return normalizeAgentAccountModels(account, requested) } meta, ok := providercatalog.Get(account.Provider) if !ok || len(meta.Models) == 0 { @@ -1148,15 +1169,22 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto 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...), + ID: item.ID, + Name: item.Name, }) } - return requested, nil + return normalizeAgentAccountModels(account, requested) +} + +func buildDiscoveredAgentAccountModels(modelIDs []string) []dto.AgentAccountModel { + models := make([]dto.AgentAccountModel, 0, len(modelIDs)) + for _, modelID := range modelIDs { + models = append(models, dto.AgentAccountModel{ + ID: modelID, + Name: modelID, + }) + } + return models } func compactPersistedAgentAccountModelSortOrder(accountID uint) error { @@ -1184,18 +1212,6 @@ func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountMode return listPersistedAgentAccountModels(account.ID) } -func cloneAgentAccountModel(model dto.AgentAccountModel) dto.AgentAccountModel { - return dto.AgentAccountModel{ - RecordID: model.RecordID, - ID: model.ID, - Name: model.Name, - ContextWindow: model.ContextWindow, - MaxTokens: model.MaxTokens, - Reasoning: model.Reasoning, - Input: append([]string(nil), model.Input...), - } -} - func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) { if account == nil { return nil, fmt.Errorf("account is required") @@ -1219,12 +1235,8 @@ func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, exi 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...), + ID: item.ID, + Name: item.Name, }) } if len(requested) == len(existing) { @@ -1243,18 +1255,10 @@ func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, e } 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), + RecordID: row.ID, + ID: strings.TrimSpace(row.Model), + Name: strings.TrimSpace(row.Name), }) } return result, nil @@ -1265,19 +1269,11 @@ func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, model 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, + AccountID: accountID, + Model: strings.TrimSpace(item.ID), + Name: strings.TrimSpace(item.Name), + SortOrder: index + 1, } if err := tx.Create(record).Error; err != nil { return err @@ -1318,86 +1314,17 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco if modelID == "" { return dto.AgentAccountModel{}, fmt.Errorf("model is required") } + modelID = providercatalog.NormalizeModelID(account.Provider, modelID) name := strings.TrimSpace(model.Name) if name == "" { name = modelID } - inputs := sanitizeAgentAccountModelInputs(model.Input) return dto.AgentAccountModel{ - ID: normalizeAgentAccountModelID(account.Provider, modelID), - Name: name, - ContextWindow: model.ContextWindow, - MaxTokens: model.MaxTokens, - Reasoning: model.Reasoning, - Input: inputs, + ID: modelID, + Name: name, }, nil } -func normalizeAgentAccountModelID(provider, requestedID string) string { - switch provider { - case "custom", "vllm": - return normalizeCustomModel(requestedID) - case "ollama": - target := strings.TrimSpace(requestedID) - if strings.HasPrefix(target, "ollama/") { - return target - } - target = strings.TrimLeft(strings.TrimSpace(target), "/") - if target == "" { - return "" - } - return "ollama/" + target - default: - target := strings.TrimSpace(requestedID) - if target == "" { - return "" - } - prefix := poolModelPrefix(provider) - if strings.Contains(target, "/") { - parts := strings.SplitN(target, "/", 2) - targetPrefix := 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) - } - target = strings.TrimLeft(strings.TrimSpace(target), "/") - if prefix == "" { - return target - } - return prefix + "/" + target - } -} - -func sanitizeAgentAccountModelInputs(values []string) []string { - result := make([]string, 0, len(values)) - seen := make(map[string]struct{}, len(values)) - for _, value := range values { - normalized := 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 requiresInitialAgentAccountModels(provider string) bool { switch provider { case "custom", "vllm", "ollama": @@ -1407,36 +1334,14 @@ func requiresInitialAgentAccountModels(provider string) bool { } } -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 := 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) + leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed) + rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed) return leftComparable != "" && leftComparable == rightComparable } @@ -1457,6 +1362,20 @@ func requireAgentAccountModelForProvider(provider string, models []dto.AgentAcco return selectedAccountModel, nil } +func resolveAgentAccountVerifyModel(provider, requested string, models []dto.AgentAccountModel) (string, error) { + if len(models) == 0 { + return "", buserr.New("ErrAgentAccountModelsRequired") + } + if strings.TrimSpace(requested) == "" { + return models[0].ID, nil + } + selected, ok := findAgentAccountModelForProvider(provider, models, requested) + if !ok { + return "", buserr.New("ErrAgentModelNotInAccount") + } + return selected.ID, nil +} + func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error { if account == nil || account.ID == 0 { return nil @@ -1595,57 +1514,6 @@ func toInt(value interface{}) int { } } -func normalizeCustomModel(modelName string) string { - trim := strings.TrimSpace(modelName) - trim = strings.TrimLeft(trim, "/") - if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 { - if strings.EqualFold(parts[0], "custom") { - return strings.TrimLeft(strings.TrimSpace(parts[1]), "/") - } - } - return trim -} - -func runtimeProviderModelPrefix(provider string) string { - switch provider { - case "gemini": - return "google" - case "kimi": - return "moonshot" - default: - return provider - } -} - -func poolModelPrefix(provider string) string { - meta, ok := providercatalog.Get(provider) - if ok && len(meta.Models) > 0 { - parts := strings.SplitN(strings.TrimSpace(meta.Models[0].ID), "/", 2) - if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" { - return parts[0] - } - } - return provider -} - -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 := value - if target == "" { - continue - } - if _, ok := seen[target]; ok { - continue - } - seen[target] = struct{}{} - result = append(result, target) - } - return result -} - func generateToken() string { bytes := make([]byte, 24) if _, err := rand.Read(bytes); err != nil { diff --git a/agent/app/service/agents_website.go b/agent/app/service/agents_website.go index 5e5e98240168..be31242ebccf 100644 --- a/agent/app/service/agents_website.go +++ b/agent/app/service/agents_website.go @@ -162,7 +162,7 @@ func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]mo } } -func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website { +func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website { websiteMap := make(map[uint]model.Website) duplicateAppInstallIDs := make(map[uint]struct{}) for _, website := range websites { @@ -182,10 +182,6 @@ func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]m return websiteMap } -func UniqueDeploymentWebsiteMapForMigration(websites []model.Website) map[uint]model.Website { - return uniqueDeploymentWebsiteMapByAppInstall(websites) -} - func uniqueUintList(items []uint) []uint { itemMap := make(map[uint]struct{}, len(items)) uniq := make([]uint, 0, len(items)) diff --git a/agent/cmd/server/conf/app.yaml b/agent/cmd/server/conf/app.yaml index 3cddcfca35ea..a4f52b67be92 100644 --- a/agent/cmd/server/conf/app.yaml +++ b/agent/cmd/server/conf/app.yaml @@ -4,7 +4,7 @@ base: is_demo: false is_offline: false is_fxplay: false - is_enterprise: false + is_enterprise: true log: level: debug diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 38ec4c41c490..728991b195a3 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -70,6 +70,7 @@ Decrypt: "Decrypt" # agent ErrAgentAccountBound: 'Account is bound to an agent and cannot be deleted' ErrTerminalAIAccountInUse: 'This model account is currently used by Terminal AI. Switch the account in Terminal AI settings or disable Terminal AI and try again.' +ErrFileAIAccountInUse: 'This model account is currently used by File Management AI. Switch the account in File Management AI settings or disable File Management AI and try again.' ErrAgentAccountUnavailable: 'Account connection unavailable: {{ .err }}' ErrAgentProviderNotSupported: 'Unsupported agent provider' ErrAgentAccountRequired: 'Select an agent account first' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: 'Agent account not verified' ErrAgentAccountModelsRequired: 'Please configure at least one model' ErrAgentAccountSingleInitialModel: 'Only one initial model can be configured during account creation' ErrAgentModelNotInAccount: 'Model is not configured in the selected account' +ErrAgentVerifyModelInUse: 'This model is used to verify the account. Select another verification model before deleting it' ErrAgentFallbackModelDuplicate: 'Fallback models cannot be duplicated' ErrAgentFallbackModelPrimary: 'Fallback model cannot match the primary model' ErrAgentModelInUse: 'A bound agent is still using this model: {{ .name }}' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 01ed41b312a9..029115fe6f28 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -70,6 +70,7 @@ Decrypt: 'Descifrar' # agente ErrAgentAccountBound: 'Cuenta vinculada a agente' ErrTerminalAIAccountInUse: 'Esta cuenta de modelo esta siendo usada por Terminal AI. Cambia la cuenta en la configuracion de Terminal AI o desactiva Terminal AI y vuelve a intentarlo.' +ErrFileAIAccountInUse: 'Esta cuenta de modelo está siendo utilizada por la IA del gestor de archivos. Cambia la cuenta en su configuración o desactívala y vuelve a intentarlo.' ErrAgentAccountUnavailable: 'Conexión de cuenta no disponible: {{ .err }}' ErrAgentProviderNotSupported: 'Proveedor de agente no soportado' ErrAgentAccountRequired: 'Elige una cuenta de agente primero' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: 'Cuenta de agente no verificada' ErrAgentAccountModelsRequired: 'Configure al menos un modelo' ErrAgentAccountSingleInitialModel: 'Solo se puede configurar un modelo inicial al crear la cuenta' ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada' +ErrAgentVerifyModelInUse: 'Este modelo se usa para verificar la cuenta. Seleccione otro modelo de verificación antes de eliminarlo' ErrAgentFallbackModelDuplicate: 'Los modelos de respaldo no pueden duplicarse' ErrAgentFallbackModelPrimary: 'El modelo de respaldo no puede ser igual al modelo principal' ErrAgentModelInUse: 'Un agente vinculado todavía usa este modelo: {{ .name }}' diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index 731cd48039c1..9efeeeb47a34 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -70,6 +70,7 @@ Decrypt: "رمزگشایی" # عامل ErrAgentAccountBound: 'حساب به یک عامل متصل است و قابل حذف نیست' ErrTerminalAIAccountInUse: 'این حساب مدل در حال حاضر توسط ترمینال هوش مصنوعی استفاده می‌شود. حساب را در تنظیمات ترمینال هوش مصنوعی تغییر دهید یا ترمینال هوش مصنوعی را غیرفعال کرده و دوباره تلاش کنید.' +ErrFileAIAccountInUse: 'این حساب مدل توسط هوش مصنوعی مدیریت فایل استفاده می‌شود. حساب را در تنظیمات آن تغییر دهید یا هوش مصنوعی مدیریت فایل را غیرفعال کرده و دوباره تلاش کنید.' ErrAgentAccountUnavailable: 'اتصال حساب در دسترس نیست: {{ .err }}' ErrAgentProviderNotSupported: 'ارائه‌دهنده عامل پشتیبانی نمی‌شود' ErrAgentAccountRequired: 'ابتدا یک حساب عامل انتخاب کنید' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: 'حساب عامل تأیید نشده است' ErrAgentAccountModelsRequired: 'لطفاً حداقل یک مدل پیکربندی کنید' ErrAgentAccountSingleInitialModel: 'فقط یک مدل اولیه می‌تواند در هنگام ایجاد حساب پیکربندی شود' ErrAgentModelNotInAccount: 'مدل در حساب انتخاب شده پیکربندی نشده است' +ErrAgentVerifyModelInUse: 'این مدل برای بررسی حساب استفاده می‌شود. پیش از حذف، مدل بررسی دیگری انتخاب کنید' ErrAgentFallbackModelDuplicate: 'مدل‌های جایگزین نمی‌توانند تکراری باشند' ErrAgentFallbackModelPrimary: 'مدل جایگزین نمی‌تواند با مدل اصلی یکی باشد' ErrAgentModelInUse: 'یک عامل متصل هنوز از این مدل استفاده می‌کند: {{ .name }}' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 613ae99edab1..7245884bf154 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -70,6 +70,7 @@ Decrypt: '復号化' # エージェント ErrAgentAccountBound: 'アカウントはエージェントに紐づいています' ErrTerminalAIAccountInUse: 'このモデルアカウントは Terminal AI で使用中です。Terminal AI の設定で別のアカウントへ切り替えるか、Terminal AI を無効にしてから再試行してください。' +ErrFileAIAccountInUse: 'このモデルアカウントはファイル管理 AI で使用中です。ファイル管理 AI の設定で別のアカウントへ切り替えるか、ファイル管理 AI を無効にしてから再試行してください。' ErrAgentAccountUnavailable: 'アカウント接続不可: {{ .err }}' ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応' ErrAgentAccountRequired: 'まずエージェントアカウントを選択' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: 'アカウント未検証です' ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください' ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです' ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません' +ErrAgentVerifyModelInUse: 'このモデルはアカウント検証に使用されています。削除する前に別の検証モデルを選択してください' ErrAgentFallbackModelDuplicate: 'フォールバックモデルは重複できません' ErrAgentFallbackModelPrimary: 'フォールバックモデルは主モデルと同じにできません' ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 09ab7516f669..af0dfd4842f9 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -70,6 +70,7 @@ Decrypt: '복호화' # 에이전트 ErrAgentAccountBound: '계정이 에이전트에 묶여 있습니다' ErrTerminalAIAccountInUse: '이 모델 계정은 현재 터미널 AI에서 사용 중입니다. 터미널 AI 설정에서 다른 계정으로 변경하거나 터미널 AI를 비활성화한 뒤 다시 시도하세요.' +ErrFileAIAccountInUse: '이 모델 계정은 현재 파일 관리 AI에서 사용 중입니다. 파일 관리 AI 설정에서 다른 계정으로 변경하거나 파일 관리 AI를 비활성화한 뒤 다시 시도하세요.' ErrAgentAccountUnavailable: '계정 연결 불가: {{ .err }}' ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자' ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: '에이전트 계정 미확인' ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요' ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다' ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다' +ErrAgentVerifyModelInUse: '이 모델은 계정 검증에 사용 중입니다. 삭제하기 전에 다른 검증 모델을 선택하세요' ErrAgentFallbackModelDuplicate: '폴백 모델은 중복될 수 없습니다' ErrAgentFallbackModelPrimary: '폴백 모델은 기본 모델과 같을 수 없습니다' ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index c5606dd3921c..82fd1130fe6f 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -70,6 +70,7 @@ Decrypt: 'Dekripsi' # ejen ErrAgentAccountBound: 'Akaun terikat kepada ejen' ErrTerminalAIAccountInUse: 'Akaun model ini sedang digunakan oleh Terminal AI. Tukar akaun dalam tetapan Terminal AI atau nyahaktifkan Terminal AI, kemudian cuba lagi.' +ErrFileAIAccountInUse: 'Akaun model ini sedang digunakan oleh AI pengurusan fail. Tukar akaun dalam tetapannya atau nyahaktifkan AI tersebut, kemudian cuba lagi.' ErrAgentAccountUnavailable: 'Sambungan akaun tidak tersedia: {{ .err }}' ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong' ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan' ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model' ErrAgentAccountSingleInitialModel: 'Hanya satu model awal boleh dikonfigurasi semasa mencipta akaun' ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih' +ErrAgentVerifyModelInUse: 'Model ini digunakan untuk mengesahkan akaun. Pilih model pengesahan lain sebelum memadamkannya' ErrAgentFallbackModelDuplicate: 'Model fallback tidak boleh berulang' ErrAgentFallbackModelPrimary: 'Model fallback tidak boleh sama dengan model utama' ErrAgentModelInUse: 'Masih ada ejen terikat yang menggunakan model ini: {{ .name }}' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index c5a933385dd4..414da2e96ce7 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -70,6 +70,7 @@ Decrypt: 'Descriptografar' # agente ErrAgentAccountBound: 'Conta vinculada a agente' ErrTerminalAIAccountInUse: 'Esta conta de modelo esta sendo usada pelo Terminal AI. Troque a conta nas configuracoes do Terminal AI ou desative o Terminal AI e tente novamente.' +ErrFileAIAccountInUse: 'Esta conta de modelo está sendo usada pela IA do gerenciador de arquivos. Troque a conta nas configurações ou desative a IA e tente novamente.' ErrAgentAccountUnavailable: 'Conexão da conta indisponível: {{ .err }}' ErrAgentProviderNotSupported: 'Provedor de agente não suportado' ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: 'Conta de agente não verificada' ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo' ErrAgentAccountSingleInitialModel: 'Apenas um modelo inicial pode ser configurado ao criar a conta' ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada' +ErrAgentVerifyModelInUse: 'Este modelo é usado para verificar a conta. Selecione outro modelo de verificação antes de excluí-lo' ErrAgentFallbackModelDuplicate: 'Os modelos de fallback não podem ser duplicados' ErrAgentFallbackModelPrimary: 'O modelo de fallback não pode ser igual ao modelo principal' ErrAgentModelInUse: 'Ainda há um agente vinculado usando este modelo: {{ .name }}' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 29f660deea29..b68b40061d8a 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -70,6 +70,7 @@ Decrypt: 'Расшифровать' # агент ErrAgentAccountBound: 'Акаунт привязан к агенту' ErrTerminalAIAccountInUse: 'Эта учетная запись модели сейчас используется Terminal AI. Смените учетную запись в настройках Terminal AI или отключите Terminal AI и повторите попытку.' +ErrFileAIAccountInUse: 'Эта учетная запись модели используется ИИ файлового менеджера. Смените учетную запись в его настройках или отключите ИИ и повторите попытку.' ErrAgentAccountUnavailable: 'Связь с аккаунтом недоступна: {{ .err }}' ErrAgentProviderNotSupported: 'Провайдер агента не поддерживается' ErrAgentAccountRequired: 'Выберите аккаунт агента' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: 'Аккаунт не подтверждён' ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель' ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель' ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте' +ErrAgentVerifyModelInUse: 'Эта модель используется для проверки аккаунта. Перед удалением выберите другую модель проверки' ErrAgentFallbackModelDuplicate: 'Резервные модели не могут дублироваться' ErrAgentFallbackModelPrimary: 'Резервная модель не может совпадать с основной' ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 2961522e84c5..2320dccad12b 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -70,6 +70,7 @@ Decrypt: 'Şifre Çöz' # ajan ErrAgentAccountBound: 'Hesap bir ajana bağlı' ErrTerminalAIAccountInUse: 'Bu model hesabi su anda Terminal AI tarafindan kullaniliyor. Terminal AI ayarlarinda baska bir hesaba gecin veya Terminal AI yi devre disi birakip tekrar deneyin.' +ErrFileAIAccountInUse: 'Bu model hesabı dosya yönetimi yapay zekası tarafından kullanılıyor. Ayarlardan hesabı değiştirin veya yapay zekayı devre dışı bırakıp yeniden deneyin.' ErrAgentAccountUnavailable: 'Hesap bağlantısı yok: {{ .err }}' ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor' ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı' ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın' ErrAgentAccountSingleInitialModel: 'Hesap oluştururken yalnızca bir başlangıç modeli yapılandırılabilir' ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı' +ErrAgentVerifyModelInUse: 'Bu model hesap doğrulaması için kullanılıyor. Silmeden önce başka bir doğrulama modeli seçin' ErrAgentFallbackModelDuplicate: 'Yedek modeller tekrar edemez' ErrAgentFallbackModelPrimary: 'Yedek model birincil modelle aynı olamaz' ErrAgentModelInUse: 'Bu modeli hâlâ kullanan bağlı bir ajan var: {{ .name }}' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 3581033da07d..d45b1f63ab65 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -70,6 +70,7 @@ Decrypt: '解密' # 智慧體 ErrAgentAccountBound: '該帳號已綁定到智能體,無法刪除,請重試。' ErrTerminalAIAccountInUse: '該模型帳號正被終端 AI 使用,請在終端 AI 設定中更換帳號或關閉終端 AI 後再試。' +ErrFileAIAccountInUse: '該模型帳號正被檔案管理 AI 使用,請在檔案管理 AI 設定中更換帳號或關閉檔案管理 AI 後再試。' ErrAgentAccountUnavailable: '帳號連線資訊不可用,錯誤:{{ .err }},請重試' ErrAgentProviderNotSupported: '暫不支援該智能體提供商,請重試' ErrAgentAccountRequired: '請選擇智能體帳號後重試' @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: '帳號未驗證通過,請重試' ErrAgentAccountModelsRequired: '請至少配置一個模型' ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型' ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試' +ErrAgentVerifyModelInUse: '該模型正用於驗證帳號,請先選擇其他驗證模型後再刪除' ErrAgentFallbackModelDuplicate: '備用模型不能重複' ErrAgentFallbackModelPrimary: '備用模型不能與主模型相同' ErrAgentModelInUse: '仍有智能體正在使用該模型:{{ .name }}' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index c414c29affc5..641c036f72ec 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -70,6 +70,7 @@ Decrypt: "解密" # 智能体 ErrAgentAccountBound: "该账号已绑定智能体,无法删除" ErrTerminalAIAccountInUse: "该模型账号正被终端 AI 使用,请在终端 AI 设置中更换账号或关闭终端 AI 后重试" +ErrFileAIAccountInUse: "该模型账号正被文件管理 AI 使用,请在文件管理 AI 设置中更换账号或关闭文件管理 AI 后重试" ErrAgentAccountUnavailable: "账号连接信息不可用: {{ .err }}" ErrAgentProviderNotSupported: "不支持该智能体提供商" ErrAgentAccountRequired: "请选择智能体账号后重试" @@ -77,6 +78,7 @@ ErrAgentAccountNotVerified: "账号未通过验证" ErrAgentAccountModelsRequired: "请至少配置一个模型" ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型" ErrAgentModelNotInAccount: "所选模型未配置在当前账号中" +ErrAgentVerifyModelInUse: "该模型正用于验证账号,请先选择其他验证模型后再删除" ErrAgentFallbackModelDuplicate: "备用模型不能重复" ErrAgentFallbackModelPrimary: "备用模型不能与主模型相同" ErrAgentModelInUse: "仍有智能体正在使用该模型: {{ .name }}" diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index eba2ea7f51d8..e6d934dc6ea6 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -77,6 +77,9 @@ func InitAgentDB() { migrations.NormalizeOllamaAccountAPIType, migrations.InitAgentAccountModelPool, migrations.AddAgentAccountMasterID, + migrations.NormalizeAgentAccountModelIDs, + migrations.AddAgentAccountVerifyModel, + migrations.AddAgentAccountAuthMode, migrations.AddHostTable, migrations.AddAITerminalSettings, migrations.UpdateAgentQuickJumpTitle, diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 199f4461acb2..600cfae84a30 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -14,6 +14,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/model" + providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider" "github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" @@ -1212,6 +1213,57 @@ var AddAgentAccountMasterID = &gormigrate.Migration{ }, } +var NormalizeAgentAccountModelIDs = &gormigrate.Migration{ + ID: "20260716-normalize-agent-account-model-ids", + Migrate: func(tx *gorm.DB) error { + return migrationutils.NormalizeAgentAccountModelIDs(tx) + }, +} + +var AddAgentAccountVerifyModel = &gormigrate.Migration{ + ID: "20260716-add-agent-account-verify-model", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.AgentAccount{}); err != nil { + return err + } + var accounts []model.AgentAccount + if err := tx.Where("verify_model = '' OR verify_model IS NULL").Find(&accounts).Error; err != nil { + return err + } + for _, account := range accounts { + var accountModel model.AgentAccountModel + err := tx.Where("account_id = ?", account.ID).Order("sort_order ASC, id ASC").First(&accountModel).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + if err != nil { + return err + } + if err := tx.Model(&model.AgentAccount{}).Where("id = ?", account.ID).Update("verify_model", accountModel.Model).Error; err != nil { + return err + } + } + return nil + }, +} + +var AddAgentAccountAuthMode = &gormigrate.Migration{ + ID: "20260716-add-agent-account-auth-mode", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.AgentAccount{}); err != nil { + return err + } + if err := tx.Model(&model.AgentAccount{}). + Where("api_type = ? AND (auth_mode IS NULL OR auth_mode = '') AND provider IN ?", "anthropic-messages", []string{"bailian-coding-plan", "ark-coding-plan", "xiaomi"}). + Update("auth_mode", providercatalog.AuthModeBearer).Error; err != nil { + return err + } + return tx.Model(&model.AgentAccount{}). + Where("api_type = ? AND (auth_mode IS NULL OR auth_mode = '')", "anthropic-messages"). + Update("auth_mode", providercatalog.AuthModeXAPIKey).Error + }, +} + var AddHostTable = &gormigrate.Migration{ ID: "20260318-add-host-table", Migrate: func(tx *gorm.DB) error { @@ -1391,7 +1443,7 @@ var AddAgentWebsiteBinding = &gormigrate.Migration{ if err := tx.Where("type = ? AND app_install_id > 0", constant.Deployment).Find(&websites).Error; err != nil { return err } - websiteMap := service.UniqueDeploymentWebsiteMapForMigration(websites) + websiteMap := service.UniqueDeploymentWebsiteMapByAppInstall(websites) for _, agent := range agents { if agent.WebsiteID != 0 || agent.AppInstallID == 0 { continue diff --git a/agent/init/migration/migrations/utils/agent_account_model_pool.go b/agent/init/migration/migrations/utils/agent_account_model_pool.go index 9c752c5c0590..2e94bf0b3637 100644 --- a/agent/init/migration/migrations/utils/agent_account_model_pool.go +++ b/agent/init/migration/migrations/utils/agent_account_model_pool.go @@ -39,19 +39,11 @@ func MigrateAgentAccountModelPool(tx *gorm.DB) error { 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, + AccountID: account.ID, + Model: item.ID, + Name: item.Name, + SortOrder: index + 1, } if err := tx.Create(record).Error; err != nil { return err @@ -108,27 +100,47 @@ func buildMigratedAgentAccountModels(tx *gorm.DB, account *legacyAgentAccountMod } return nil, err } - applyLegacyAgentAccountModelDefaults(account.Provider, models) return models, nil } -func applyLegacyAgentAccountModelDefaults(provider string, models []dto.AgentAccountModel) { - if provider != "custom" && provider != "ollama" && provider != "vllm" { - return - } - meta, ok := providercatalog.Get(provider) - if !ok { - return +func NormalizeAgentAccountModelIDs(tx *gorm.DB) error { + var accounts []model.AgentAccount + if err := tx.Find(&accounts).Error; err != nil { + return err } - for i := range models { - if strings.TrimSpace(models[i].Name) == "" { - models[i].Name = strings.TrimSpace(models[i].ID) + for _, account := range accounts { + var accountModels []model.AgentAccountModel + if err := tx.Where("account_id = ?", account.ID).Find(&accountModels).Error; err != nil { + return err + } + for _, item := range accountModels { + normalized := providercatalog.NormalizeModelID(account.Provider, item.Model) + if normalized != item.Model { + if err := tx.Model(&model.AgentAccountModel{}).Where("id = ?", item.ID).Update("model", normalized).Error; err != nil { + return err + } + } } - if models[i].ContextWindow <= 0 { - models[i].ContextWindow = meta.Default.ContextWindow + + var agents []model.Agent + if err := tx.Where("account_id = ?", account.ID).Find(&agents).Error; err != nil { + return err } - if models[i].MaxTokens <= 0 { - models[i].MaxTokens = meta.Default.MaxTokens + for _, agent := range agents { + normalized := providercatalog.NormalizeModelID(account.Provider, agent.Model) + if normalized != agent.Model { + if err := tx.Model(&model.Agent{}).Where("id = ?", agent.ID).Update("model", normalized).Error; err != nil { + return err + } + } } } + if err := tx.Model(&model.AgentAccount{}). + Where("provider = ? AND api_type = ? AND (base_url = '' OR base_url = ? OR base_url = ?)", "deepseek", "anthropic-messages", "https://api.deepseek.com", "https://api.deepseek.com/v1"). + Update("base_url", "https://api.deepseek.com/anthropic").Error; err != nil { + return err + } + return tx.Model(&model.AgentAccount{}). + Where("provider = ?", "gemini"). + Update("api_type", "gemini-generate-content").Error } diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index efb4f89ddc19..d7934a17e570 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -52,6 +52,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/accounts/search", baseApi.PageAgentAccounts) aiToolsRouter.POST("/accounts/counts", baseApi.CountAgentAccountsByProviders) aiToolsRouter.POST("/accounts/models", baseApi.GetAgentAccountModels) + aiToolsRouter.POST("/accounts/models/discover", baseApi.DiscoverAgentAccountModels) aiToolsRouter.POST("/accounts/models/create", baseApi.CreateAgentAccountModel) aiToolsRouter.POST("/accounts/models/update", baseApi.UpdateAgentAccountModel) aiToolsRouter.POST("/accounts/models/delete", baseApi.DeleteAgentAccountModel) diff --git a/agent/utils/files/ai_search_llm.go b/agent/utils/files/ai_search_llm.go index 65ea99f07b72..d3fe0a922831 100644 --- a/agent/utils/files/ai_search_llm.go +++ b/agent/utils/files/ai_search_llm.go @@ -100,6 +100,7 @@ func RunFileAISearchLLM(ctx context.Context, cfg terminalai.GeneratorConfig, cli APIKey: cfg.APIKey, Model: cfg.Model, APIType: cfg.APIType, + AuthMode: cfg.AuthMode, MaxTokens: cfg.MaxTokens, Timeout: timeout, }) diff --git a/agent/utils/terminal/ai/client.go b/agent/utils/terminal/ai/client.go index 0dc17ac86691..9af2a0cb9b42 100644 --- a/agent/utils/terminal/ai/client.go +++ b/agent/utils/terminal/ai/client.go @@ -20,6 +20,7 @@ const ( defaultTimeout = 30 * time.Second defaultUserAgent = "1panel-terminal-ai/1.0" defaultAnthropicToken = 1024 + defaultTerminalTokens = 8192 ) type Client interface { @@ -32,6 +33,7 @@ type ClientConfig struct { APIKey string Model string APIType string + AuthMode string MaxTokens int Timeout time.Duration HTTPClient *http.Client @@ -43,6 +45,7 @@ type GeneratorConfig struct { APIKey string Model string APIType string + AuthMode string MaxTokens int } @@ -99,6 +102,11 @@ func NewClient(cfg ClientConfig) (Client, error) { cfg.Provider = providerKey cfg.BaseURL = baseURL cfg.APIType = normalizeClientAPIType(providerKey, cfg.APIType) + authMode, err := providercatalog.ResolveAuthMode(providerKey, cfg.APIType, cfg.AuthMode) + if err != nil { + return nil, err + } + cfg.AuthMode = authMode if cfg.Timeout <= 0 { cfg.Timeout = defaultTimeout } @@ -112,26 +120,6 @@ func NewClient(cfg ClientConfig) (Client, error) { }, nil } -type ClientOption func(*ClientConfig) - -func WithBaseURL(baseURL string) ClientOption { - return func(cfg *ClientConfig) { - cfg.BaseURL = baseURL - } -} - -func WithTimeout(timeout time.Duration) ClientOption { - return func(cfg *ClientConfig) { - cfg.Timeout = timeout - } -} - -func WithHTTPClient(client *http.Client) ClientOption { - return func(cfg *ClientConfig) { - cfg.HTTPClient = client - } -} - func (c *terminalAIClient) ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) { if len(req.Messages) == 0 { return nil, fmt.Errorf("messages are required") @@ -150,7 +138,7 @@ func (c *terminalAIClient) ChatCompletion(ctx context.Context, req ChatCompletio func (c *terminalAIClient) chatCompletionOpenAI(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) { payload := openAIChatCompletionRequest{ - Model: normalizeModelID(c.config.Model), + Model: strings.TrimSpace(c.config.Model), Messages: req.Messages, MaxTokens: firstPositive(req.MaxTokens, c.config.MaxTokens), Temperature: req.Temperature, @@ -195,7 +183,7 @@ func (c *terminalAIClient) chatCompletionOpenAI(ctx context.Context, req ChatCom func (c *terminalAIClient) chatCompletionOpenAIResponses(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) { payload := openAIResponsesRequest{ - Model: normalizeModelID(c.config.Model), + Model: strings.TrimSpace(c.config.Model), Input: toResponsesInput(req.Messages), MaxOutputTokens: firstPositive(req.MaxTokens, c.config.MaxTokens), Temperature: req.Temperature, @@ -242,7 +230,7 @@ func (c *terminalAIClient) chatCompletionOpenAIResponses(ctx context.Context, re func (c *terminalAIClient) chatCompletionAnthropic(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) { system, messages := toAnthropicMessages(req.Messages) payload := anthropicMessagesRequest{ - Model: normalizeModelID(c.config.Model), + Model: strings.TrimSpace(c.config.Model), System: system, Messages: messages, MaxTokens: firstPositive(req.MaxTokens, c.config.MaxTokens, defaultAnthropicToken), @@ -257,7 +245,11 @@ func (c *terminalAIClient) chatCompletionAnthropic(ctx context.Context, req Chat if err != nil { return nil, fmt.Errorf("create request: %w", err) } - httpReq.Header.Set("x-api-key", strings.TrimSpace(c.config.APIKey)) + if c.config.AuthMode == providercatalog.AuthModeBearer { + httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.config.APIKey)) + } else { + httpReq.Header.Set("x-api-key", strings.TrimSpace(c.config.APIKey)) + } httpReq.Header.Set("anthropic-version", "2023-06-01") httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Accept", "application/json") @@ -299,7 +291,7 @@ func (c *terminalAIClient) chatCompletionGemini(ctx context.Context, req ChatCom if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } - httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildGeminiGenerateContentURL(c.config.BaseURL, normalizeModelID(c.config.Model)), bytes.NewReader(body)) + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildGeminiGenerateContentURL(c.config.BaseURL, strings.TrimSpace(c.config.Model)), bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } @@ -592,40 +584,18 @@ func buildGeminiGenerateContentURL(baseURL, model string) string { return parsed.String() } -func normalizeModelID(model string) string { - model = strings.TrimSpace(model) - if parts := strings.SplitN(model, "/", 2); len(parts) == 2 { - return parts[1] - } - return model -} - func thinkingModeForModel(model string) *ThinkingMode { - if strings.EqualFold(normalizeModelID(model), "kimi-k2.5") { + if strings.EqualFold(strings.TrimSpace(model), "kimi-k2.5") { return &ThinkingMode{Type: "disabled"} } return nil } func normalizeClientAPIType(provider, apiType string) string { - switch strings.ToLower(strings.TrimSpace(provider)) { - case "anthropic", "kimi-coding", "minimax": - return "anthropic-messages" - case "gemini": - return "gemini-generate-content" - case "ollama": - trim := strings.ToLower(strings.TrimSpace(apiType)) - if trim == "openai-completions" { - return trim - } - return "openai-responses" - default: - trim := strings.ToLower(strings.TrimSpace(apiType)) - if trim == "" { - return "openai-completions" - } - return trim + if value := strings.ToLower(strings.TrimSpace(apiType)); value != "" { + return value } + return providercatalog.DefaultAPIType(strings.ToLower(strings.TrimSpace(provider))) } func extractAssistantContent(resp openAIChatCompletionResponse) string { diff --git a/agent/utils/terminal/ai/command_generator.go b/agent/utils/terminal/ai/command_generator.go index 00f19f9d4965..4fd41e90d7bf 100644 --- a/agent/utils/terminal/ai/command_generator.go +++ b/agent/utils/terminal/ai/command_generator.go @@ -35,6 +35,7 @@ func NewCommandGeneratorFromConfig(cfg GeneratorConfig) (*CommandGenerator, erro APIKey: cfg.APIKey, Model: cfg.Model, APIType: cfg.APIType, + AuthMode: cfg.AuthMode, MaxTokens: cfg.MaxTokens, }) if err != nil { diff --git a/agent/utils/terminal/ai/config_runtime.go b/agent/utils/terminal/ai/config_runtime.go index 5ace6cb6516e..e6d839abf695 100644 --- a/agent/utils/terminal/ai/config_runtime.go +++ b/agent/utils/terminal/ai/config_runtime.go @@ -50,7 +50,7 @@ func ResolveGeneratorConfig(accountID uint) (GeneratorConfig, time.Duration, err if provider == "" { return GeneratorConfig{}, 0, fmt.Errorf("agent account provider is required") } - model, maxTokens, err := resolveAccountModelConfig(account.ID, provider) + model, err := resolveAccountModelConfig(account.ID, provider) if err != nil { return GeneratorConfig{}, 0, err } @@ -73,7 +73,8 @@ func ResolveGeneratorConfig(accountID uint) (GeneratorConfig, time.Duration, err APIKey: strings.TrimSpace(apiKey), Model: model, APIType: strings.TrimSpace(account.APIType), - MaxTokens: maxTokens, + AuthMode: strings.TrimSpace(account.AuthMode), + MaxTokens: defaultTerminalTokens, }, 30 * time.Second, nil } @@ -85,26 +86,25 @@ func lookupProviderAPIKey(provider string) string { return strings.TrimSpace(os.Getenv(envKey)) } -func defaultModelForProvider(provider string) (string, int) { +func defaultModelForProvider(provider string) string { meta, ok := providercatalog.Get(provider) if !ok || len(meta.Models) == 0 { - return "", 0 + return "" } - return meta.Models[0].ID, meta.Models[0].MaxTokens + return meta.Models[0].ID } -func resolveAccountModelConfig(accountID uint, provider string) (string, int, error) { +func resolveAccountModelConfig(accountID uint, provider string) (string, error) { if accountID > 0 { rows, err := agentAccountModelRepo.List(repo.WithByAccountID(accountID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id")) if err != nil { - return "", 0, err + return "", err } if len(rows) > 0 { - return strings.TrimSpace(rows[0].Model), rows[0].MaxTokens, nil + return strings.TrimSpace(rows[0].Model), nil } } - model, maxTokens := defaultModelForProvider(provider) - return model, maxTokens, nil + return defaultModelForProvider(provider), nil } func ResolveGeneratorConfigFromAgentSettings() (GeneratorConfig, uint, time.Duration, error) { diff --git a/core/cmd/server/conf/app.yaml b/core/cmd/server/conf/app.yaml index a1a166a29faa..c114bfea251a 100644 --- a/core/cmd/server/conf/app.yaml +++ b/core/cmd/server/conf/app.yaml @@ -4,7 +4,7 @@ base: is_demo: false is_offline: false is_fxplay: false - is_enterprise: false + is_enterprise: true port: 9999 username: admin password: admin123 diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index 57f638ada0aa..e8a4feb3ef9c 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -344,8 +344,6 @@ export namespace AI { providerName: string; model: string; apiType: string; - maxTokens: number; - contextWindow: number; baseUrl: string; apiKey: string; token: string; @@ -521,16 +519,19 @@ export namespace AI { recordId: number; id: string; name: string; - contextWindow: number; - maxTokens: number; - reasoning: boolean; - input: string[]; } export interface AgentAccountModelReq { accountId: number; } + export interface AgentAccountModelDiscoverReq { + provider: string; + baseURL: string; + apiKey: string; + apiType: string; + } + export interface AgentAccountModelCreateReq { accountId: number; model: AgentAccountModel; @@ -549,16 +550,22 @@ export namespace AI { export interface ProviderModelInfo { id: string; name: string; - contextWindow: number; - maxTokens: number; - reasoning: boolean; - input: string[]; + } + + export interface ProviderAPIInfo { + apiType: string; + baseUrl: string; + editableBaseUrl: boolean; + defaultAuthMode: string; + authModes: string[]; } export interface ProviderInfo { provider: string; displayName: string; baseUrl: string; + defaultApiType: string; + apiTypes: ProviderAPIInfo[]; models: ProviderModelInfo[]; } @@ -569,7 +576,9 @@ export namespace AI { rememberApiKey: boolean; baseURL: string; apiType: string; + authMode: string; models?: AgentAccountModel[]; + verifyModel: string; remark: string; } @@ -580,6 +589,8 @@ export namespace AI { rememberApiKey: boolean; baseURL: string; apiType: string; + authMode: string; + verifyModel: string; remark: string; syncAgents: boolean; } @@ -605,6 +616,8 @@ export namespace AI { baseUrl: string; models: AgentAccountModel[]; apiType: string; + authMode: string; + verifyModel: string; verified: boolean; remark: string; createdAt: string; diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index d158d2dd61ab..e631b021cbf0 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -234,6 +234,10 @@ export const getAgentAccountModels = (req: AI.AgentAccountModelReq) => { return http.post(`/ai/accounts/models`, req); }; +export const discoverAgentAccountModels = (req: AI.AgentAccountModelDiscoverReq) => { + return http.post(`/ai/accounts/models/discover`, req); +}; + export const createAgentAccountModel = (req: AI.AgentAccountModelCreateReq) => { return http.post(`/ai/accounts/models/create`, req); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 99a7fad8bd79..30cff381e8cd 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -722,6 +722,15 @@ const message = { accountModelsHelper: 'Choose which models this account makes available to OpenClaw for selection and configuration', accountModelsRequired: 'Select at least one model', + verifyModel: 'Verification Model', + verifyModelHelper: + 'Select a model this account can access. It is used to send a minimal request when saving to verify connectivity.', + verifyModelRequired: 'Select a verification model', + discoverModels: 'Get Models', + automaticModelDiscovery: 'Auto fetch', + manualModelConfiguration: 'Manual setup', + discoverModelsHelper: 'Get models from the provider /v1/models API and add all of them to the model pool', + discoverModelsFailedFallback: 'Failed to get models. Configure a model manually.', accountModelsDuplicate: 'Duplicate models were found in the catalog', modelPool: 'Model Pool', fallbackModels: 'Fallback Models', @@ -873,7 +882,6 @@ const message = { dashboardAuthAutoRebuildHelper: 'Saving the username or password will automatically rebuild the container so the changes take effect.', channelDeleteConfirm: 'Delete the {0} channel configuration?', - customProviderHelper: 'Custom model providers do not validate whether the account is available.', apiTypeBaseURLHelper: 'This type sends requests to {0}. Recommended Base URL: {1}', apiTypeBaseURLMismatch: 'The current Base URL looks like a {0} path, but {1} is selected. Recommended URL: {2}', @@ -1054,8 +1062,6 @@ const message = { healthUnknown: 'Unknown', validationModelMapEmpty: 'Model mapping names cannot be empty', validationModelMapDuplicate: 'Request model {0} is duplicated', - validateAvailability: 'Validate Account Availability', - validateAvailabilityHelper: 'Send a minimal message before saving to validate API availability', modelGroup: 'Model Group', availableModels: 'Available Models', modelGroupModels: 'Request Models', @@ -4930,8 +4936,10 @@ const message = { noBlackIp: 'IP is already blocked, no need to block again', noWhiteUrl: 'URL is already in the white list, no need to add again', spiderIpHelper: - 'Includes Baidu, Bing, Google, 360, Shenma, Sogou, ByteDance, DuckDuckGo, Yandex. Closing this will block all spider access.', + 'When enabled, only selected spiders are allowed. When disabled, all spider access is blocked.', spiderIp: 'Spider IP Pool', + spiderAllowlist: 'Allowed spiders', + spiderAllowlistEmpty: 'No spiders selected. Saving will block all spider access.', geoIp: 'IP Address Library', geoIpHelper: 'Used to confirm the geolocation of the IP', stat: 'Attack Report', @@ -5067,6 +5075,7 @@ const message = { statusErr: 'Invalid status code format', shenma: 'Shenma Search', duckduckgo: 'DuckDuckGo', + yandex: 'Yandex', excludeUri: 'Exclude URIs', top100Helper: 'Show the top 100 data', logSaveDay: 'Log Retention Period (days)', @@ -5782,7 +5791,8 @@ const message = { storagePool: 'Storage Pool', network: 'Network', bridgeName: 'Bridge Name', - natNetworkHelper: 'VMs can access the network through the host, but external devices usually cannot access them directly.', + natNetworkHelper: + 'VMs can access the network through the host, but external devices usually cannot access them directly.', bridgeNetworkHelper: 'Connect VMs to an existing host bridge (such as br0). If the host has an unused additional NIC, create a bridge for it and use it.', bridgeNameHelper: diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index f0fe82e2647f..213dd4d46918 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -721,6 +721,16 @@ const message = { accountModelsHelper: 'Configure los modelos que esta cuenta expone a OpenClaw para el cambio y la configuración', accountModelsRequired: 'Configure al menos un modelo', + verifyModel: 'Modelo de verificación', + verifyModelHelper: + 'Seleccione un modelo al que esta cuenta tenga acceso. Se usará para enviar una solicitud mínima al guardar y verificar la conexión.', + verifyModelRequired: 'Seleccione un modelo de verificación', + discoverModels: 'Obtener modelos', + automaticModelDiscovery: 'Obtención automática', + manualModelConfiguration: 'Configuración manual', + discoverModelsHelper: + 'Obtenga los modelos mediante la API /v1/models del proveedor y añádalos todos al conjunto de modelos', + discoverModelsFailedFallback: 'No se pudieron obtener los modelos. Configure un modelo manualmente.', accountModelsDuplicate: 'Existen modelos duplicados en el catálogo', modelPool: 'Pool de modelos', fallbackModels: 'Modelos de respaldo', @@ -877,7 +887,6 @@ const message = { dashboardAuthAutoRebuildHelper: 'Al guardar el usuario o la contraseña, el contenedor se reconstruirá automáticamente para que los cambios surtan efecto.', channelDeleteConfirm: '¿Eliminar la configuración del canal {0}?', - customProviderHelper: 'En el proveedor de modelo personalizado no se valida si la cuenta está disponible', apiTypeBaseURLHelper: 'Este tipo envía solicitudes a {0}. Base URL recomendada: {1}', apiTypeBaseURLMismatch: 'La Base URL actual parece una ruta {0}, pero está seleccionado {1}. URL recomendada: {2}', @@ -1062,9 +1071,6 @@ const message = { healthUnknown: 'Desconocido', validationModelMapEmpty: 'Los nombres del mapeo de modelos no pueden estar vacíos', validationModelMapDuplicate: 'El modelo solicitado {0} está duplicado', - validateAvailability: 'Validar disponibilidad de la cuenta', - validateAvailabilityHelper: - 'Enviar un mensaje mínimo antes de guardar para validar la disponibilidad de la API', modelGroup: 'Grupo de modelos', availableModels: 'Modelos disponibles', modelGroupModels: 'Modelos solicitados', @@ -4975,8 +4981,10 @@ const message = { noBlackIp: 'IP ya bloqueada', noWhiteUrl: 'URL ya en whitelist', spiderIpHelper: - 'Incluye Baidu, Bing, Google, 360, Shenma, Sogou, ByteDance, DuckDuckGo, Yandex. Al cerrar se bloquea todo acceso de crawlers', + 'Al activarlo, solo se permiten los crawlers seleccionados; al desactivarlo, se bloquean todos.', spiderIp: 'Pool de IPs de crawlers', + spiderAllowlist: 'Crawlers permitidos', + spiderAllowlistEmpty: 'No hay crawlers seleccionados. Al guardar, se bloquearán todos.', geoIp: 'Base de datos IP', geoIpHelper: 'Usada para geolocalizar IPs', stat: 'Reporte de ataques', @@ -5108,6 +5116,7 @@ const message = { statusErr: 'Formato de código de estado inválido', shenma: 'Shenma', duckduckgo: 'DuckDuckGo', + yandex: 'Yandex', excludeUri: 'Excluir URIs', top100Helper: 'Mostrar top 100', logSaveDay: 'Retención de logs (días)', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index ecb5d2d17e34..2f1fbe45daf2 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -708,6 +708,16 @@ const message = { accountModelsHelper: 'انتخاب کنید که این حساب چه مدل‌هایی را برای OpenClaw جهت انتخاب و پیکربندی در دسترس قرار دهد', accountModelsRequired: 'حداقل یک مدل انتخاب کنید', + verifyModel: 'مدل بررسی', + verifyModelHelper: + 'مدلی را انتخاب کنید که این حساب به آن دسترسی دارد. هنگام ذخیره، یک درخواست حداقلی برای بررسی اتصال ارسال می‌شود.', + verifyModelRequired: 'یک مدل بررسی انتخاب کنید', + discoverModels: 'دریافت مدل‌ها', + automaticModelDiscovery: 'دریافت خودکار', + manualModelConfiguration: 'پیکربندی دستی', + discoverModelsHelper: + 'مدل‌ها را از API ‏/v1/models ارائه‌دهنده دریافت و همه را به مجموعه مدل‌ها اضافه کنید', + discoverModelsFailedFallback: 'دریافت مدل‌ها ناموفق بود. مدل را به‌صورت دستی پیکربندی کنید.', accountModelsDuplicate: 'مدل‌های تکراری در کاتالوگ یافت شد', modelPool: 'مجموعه مدل‌ها', fallbackModels: 'مدل‌های جایگزین', @@ -856,7 +866,6 @@ const message = { 'برای شروع کار ورود با QR کلیک کنید. کد QR در لاگ وظیفه ظاهر می‌شود و پس از اسکان موفق، کانتینر به طور خودکار مجدداً راه‌اندازی می‌شود.', channelAutoRestartHelper: 'ذخیره به طور خودکار کانتینر را مجدداً راه‌اندازی می‌کند تا تغییرات اعمال شوند.', channelDeleteConfirm: 'پیکربندی کانال {0} حذف شود؟', - customProviderHelper: 'ارائه‌دهندگان مدل سفارشی در دسترس بودن حساب را تأیید نمی‌کنند.', }, model: { model: 'مدل‌ها', @@ -4887,8 +4896,10 @@ const message = { noBlackIp: 'IP از قبل مسدود شده است، نیازی به مسدودسازی مجدد نیست', noWhiteUrl: 'URL از قبل در لیست سفید است، نیازی به افزودن مجدد نیست', spiderIpHelper: - 'شامل Baidu، Bing، Google، 360، Shenma، Sogou، ByteDance، DuckDuckGo، Yandex. بستن این گزینه همه دسترسی‌های عنکبوتی را مسدود می‌کند.', + 'در صورت فعال بودن، فقط خزنده‌های انتخاب‌شده مجازند؛ با غیرفعال کردن، دسترسی همه خزنده‌ها مسدود می‌شود.', spiderIp: 'مجموعه IP عنکبوت', + spiderAllowlist: 'خزنده‌های مجاز', + spiderAllowlistEmpty: 'هیچ خزنده‌ای انتخاب نشده است. با ذخیره، دسترسی همه خزنده‌ها مسدود می‌شود.', geoIp: 'کتابخانه آدرس IP', geoIpHelper: 'برای تأیید موقعیت جغرافیایی IP استفاده می‌شود', stat: 'گزارش حمله', @@ -5012,6 +5023,7 @@ const message = { statusErr: 'فرمت کد وضعیت نامعتبر است', shenma: 'جستجوی شنما', duckduckgo: 'داک‌داک‌گو', + yandex: 'Yandex', excludeUri: 'رد کردن URI‌ها', top100Helper: 'نمایش ۱۰۰ داده برتر', logSaveDay: 'دوره نگهداری لاگ (روز)', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 55c5ed4e82eb..679721632e8d 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -716,6 +716,15 @@ const message = { accountModelsHelper: 'このアカウントが OpenClaw に提供するモデルを設定します。モデルの切り替えや設定に使用されます', accountModelsRequired: '少なくとも 1 つのモデルを設定してください', + verifyModel: '検証モデル', + verifyModelHelper: + 'このアカウントで利用できるモデルを選択してください。保存時に最小リクエストを送信して接続を検証します。', + verifyModelRequired: '検証モデルを選択してください', + discoverModels: 'モデルを取得', + automaticModelDiscovery: '自動取得', + manualModelConfiguration: '手動設定', + discoverModelsHelper: 'プロバイダーの /v1/models API からモデルを取得し、すべてモデルプールに追加します', + discoverModelsFailedFallback: 'モデルを取得できませんでした。手動でモデルを設定してください。', accountModelsDuplicate: 'カタログに重複したモデルがあります', modelPool: 'モデルプール', fallbackModels: 'フォールバックモデル', @@ -869,7 +878,6 @@ const message = { dashboardAuthAutoRebuildHelper: 'ユーザー名またはパスワードを保存すると、変更を反映するためにコンテナが自動で再作成されます。', channelDeleteConfirm: '{0} チャンネルの設定を削除しますか?', - customProviderHelper: 'カスタムモデルプロバイダーでは、アカウントの有効性を検証しません', apiTypeBaseURLHelper: 'このタイプは最終的に {0} にリクエストします。推奨 Base URL: {1}', apiTypeBaseURLMismatch: '現在の Base URL は {0} のパスに見えますが、{1} が選択されています。推奨 URL: {2}', }, @@ -1051,8 +1059,6 @@ const message = { healthUnknown: '不明', validationModelMapEmpty: 'モデルマッピングのモデル名は空にできません', validationModelMapDuplicate: 'リクエストモデル {0} が重複しています', - validateAvailability: 'アカウントの可用性を検証', - validateAvailabilityHelper: '保存前に最小メッセージを送信して API の可用性を検証します', modelGroup: 'モデルグループ', availableModels: '利用可能なモデル', modelGroupModels: 'リクエストモデル', @@ -4945,8 +4951,10 @@ const message = { noBlackIp: 'IP は既にブロックされているため、再度ブロックする必要はありません', noWhiteUrl: 'URL は既にホワイトリストに含まれているため、再度追加する必要はありません', spiderIpHelper: - '百度、Bing、Google、360、神马、搜狗、字节、DuckDuckGo、Yandexを含みます。これを閉じると、すべてのクローラーのアクセスがブロックされます。', + '有効にすると選択したクローラーのみ許可し、無効にするとすべてのクローラーをブロックします。', spiderIp: 'スパイダー IP プール', + spiderAllowlist: '許可するクローラー', + spiderAllowlistEmpty: 'クローラーが選択されていません。保存するとすべてのクローラーがブロックされます。', geoIp: 'IP アドレスライブラリ', geoIpHelper: 'IP の地理的位置を確認するために使用されます', stat: '攻撃レポート', @@ -5080,6 +5088,7 @@ const message = { statusErr: '無効なステータスコード形式', shenma: '神馬検索', duckduckgo: 'DuckDuckGo', + yandex: 'Yandex', excludeUri: 'URIを除外', top100Helper: '上位100データを表示', logSaveDay: 'ログ保持期間(日数)', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index d5e16bf8fc5d..b123d1a5ff2c 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -707,6 +707,15 @@ const message = { provider: '모델 제공자', accountModelsHelper: '이 계정이 OpenClaw에 제공할 모델을 설정합니다. 모델 전환 및 설정에 사용됩니다.', accountModelsRequired: '모델을 하나 이상 설정하세요.', + verifyModel: '검증 모델', + verifyModelHelper: + '이 계정으로 사용할 수 있는 모델을 선택하세요. 저장할 때 최소 요청을 보내 연결을 검증합니다.', + verifyModelRequired: '검증 모델을 선택하세요', + discoverModels: '모델 가져오기', + automaticModelDiscovery: '자동 가져오기', + manualModelConfiguration: '수동 설정', + discoverModelsHelper: '공급자의 /v1/models API에서 모델을 가져와 모두 모델 풀에 추가합니다.', + discoverModelsFailedFallback: '모델을 가져오지 못했습니다. 모델을 직접 설정하세요.', accountModelsDuplicate: '카탈로그에 중복된 모델이 있습니다', modelPool: '모델 풀', fallbackModels: '폴백 모델', @@ -854,7 +863,6 @@ const message = { dashboardAuthAutoRebuildHelper: '사용자 이름 또는 비밀번호를 저장하면 변경 사항 적용을 위해 컨테이너가 자동으로 재생성됩니다.', channelDeleteConfirm: '{0} 채널 구성을 삭제하시겠습니까?', - customProviderHelper: '사용자 정의 모델 공급자는 계정 사용 가능 여부를 검증하지 않습니다', apiTypeBaseURLHelper: '이 유형은 최종적으로 {0}에 요청합니다. 권장 Base URL: {1}', apiTypeBaseURLMismatch: '현재 Base URL은 {0} 경로처럼 보이지만 {1}이 선택되었습니다. 권장 URL: {2}', }, @@ -1036,8 +1044,6 @@ const message = { healthUnknown: '알 수 없음', validationModelMapEmpty: '모델 매핑의 모델 이름은 비워 둘 수 없습니다', validationModelMapDuplicate: '요청 모델 {0}이(가) 중복되었습니다', - validateAvailability: '계정 사용 가능 여부 검증', - validateAvailabilityHelper: '저장 전에 최소 메시지를 보내 API 사용 가능 여부를 검증합니다', modelGroup: '모델 그룹', availableModels: '사용 가능한 모델', modelGroupModels: '요청 모델', @@ -4845,9 +4851,10 @@ const message = { addUrlsToWhite: 'URL을 일괄 허용 목록에 추가', noBlackIp: 'IP가 이미 차단되어 있으므로 다시 차단할 필요가 없습니다', noWhiteUrl: 'URL이 이미 허용 목록에 포함되어 있으므로 다시 추가할 필요가 없습니다', - spiderIpHelper: - '바이두, 빙, 구글, 360, 신마, 소구, 바이트댄스, DuckDuckGo, Yandex가 포함됩니다. 이를 끄면 모든 스파이더의 접근이 차단됩니다.', + spiderIpHelper: '활성화하면 선택한 크롤러만 허용하고, 비활성화하면 모든 크롤러 접근을 차단합니다.', spiderIp: '스파이더 IP 풀', + spiderAllowlist: '허용할 크롤러', + spiderAllowlistEmpty: '선택한 크롤러가 없습니다. 저장하면 모든 크롤러 접근이 차단됩니다.', geoIp: 'IP 주소 라이브러리', geoIpHelper: 'IP의 지리적 위치를 확인하는 데 사용됩니다', stat: '공격 보고서', @@ -4983,6 +4990,7 @@ const message = { statusErr: '잘못된 상태 코드 형식', shenma: '션마 검색', duckduckgo: '덕덕고', + yandex: 'Yandex', excludeUri: 'URI 제외', top100Helper: '상위 100 개의 데이터를 표시합니다', logSaveDay: '로그 보관 기간 (일)', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 520a7ee26f53..44c98ed51d90 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -723,6 +723,16 @@ const message = { accountModelsHelper: 'Konfigurasikan model yang ditawarkan oleh akaun ini kepada OpenClaw untuk pemilihan dan tetapan', accountModelsRequired: 'Pilih sekurang-kurangnya satu model', + verifyModel: 'Model Pengesahan', + verifyModelHelper: + 'Pilih model yang boleh dicapai oleh akaun ini. Permintaan minimum akan dihantar semasa menyimpan untuk mengesahkan sambungan.', + verifyModelRequired: 'Pilih model pengesahan', + discoverModels: 'Dapatkan model', + automaticModelDiscovery: 'Dapatkan automatik', + manualModelConfiguration: 'Konfigurasi manual', + discoverModelsHelper: + 'Dapatkan model daripada API /v1/models penyedia dan tambahkan semuanya ke katalog model', + discoverModelsFailedFallback: 'Gagal mendapatkan model. Konfigurasikan model secara manual.', accountModelsDuplicate: 'Terdapat model pendua dalam katalog', modelPool: 'Katalog model', fallbackModels: 'Model fallback', @@ -876,7 +886,6 @@ const message = { dashboardAuthAutoRebuildHelper: 'Menyimpan nama pengguna atau kata laluan akan membina semula bekas secara automatik supaya perubahan berkuat kuasa.', channelDeleteConfirm: 'Padam konfigurasi saluran {0}?', - customProviderHelper: 'Penyedia model tersuai tidak mengesahkan sama ada akaun boleh digunakan', apiTypeBaseURLHelper: 'Jenis ini menghantar permintaan ke {0}. Base URL yang disyorkan: {1}', apiTypeBaseURLMismatch: 'Base URL semasa kelihatan seperti laluan {0}, tetapi {1} dipilih. URL yang disyorkan: {2}', @@ -1060,8 +1069,6 @@ const message = { healthUnknown: 'Tidak diketahui', validationModelMapEmpty: 'Nama model dalam pemetaan model tidak boleh kosong', validationModelMapDuplicate: 'Model permintaan {0} berulang', - validateAvailability: 'Sahkan Ketersediaan Akaun', - validateAvailabilityHelper: 'Hantar mesej minimum sebelum menyimpan untuk mengesahkan ketersediaan API', modelGroup: 'Kumpulan Model', availableModels: 'Model Tersedia', modelGroupModels: 'Model Permintaan', @@ -5006,8 +5013,10 @@ const message = { noBlackIp: 'IP telah disekat, tidak perlu disekat semula', noWhiteUrl: 'URL telah dimasukkan ke senarai putih, tidak perlu ditambah semula', spiderIpHelper: - 'Termasuk Baidu, Bing, Google, 360, Shenma, Sogou, ByteDance, DuckDuckGo, Yandex. Menutup ini akan menyekat semua akses labah-labah.', + 'Apabila didayakan, hanya perangkak yang dipilih dibenarkan; apabila dilumpuhkan, semua akses perangkak disekat.', spiderIp: 'Kolam IP labah-labah', + spiderAllowlist: 'Perangkak dibenarkan', + spiderAllowlistEmpty: 'Tiada perangkak dipilih. Menyimpan akan menyekat semua akses perangkak.', geoIp: 'Pustaka Alamat IP', geoIpHelper: 'Digunakan untuk mengesahkan lokasi geografi IP', stat: 'Laporan Serangan', @@ -5143,6 +5152,7 @@ const message = { statusErr: 'Format kod status tidak sah', shenma: 'Shenma Search', duckduckgo: 'DuckDuckGo', + yandex: 'Yandex', excludeUri: 'Kecualikan URI', top100Helper: 'Tunjukkan data 100 teratas', logSaveDay: 'Tempoh Penahanan Log (hari)', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index e5061618fcdc..438a86a1161a 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -717,6 +717,16 @@ const message = { accountModelsHelper: 'Configure os modelos que esta conta disponibiliza ao OpenClaw para seleção e configuração', accountModelsRequired: 'Selecione pelo menos um modelo', + verifyModel: 'Modelo de verificação', + verifyModelHelper: + 'Selecione um modelo acessível por esta conta. Uma solicitação mínima será enviada ao salvar para verificar a conexão.', + verifyModelRequired: 'Selecione um modelo de verificação', + discoverModels: 'Obter modelos', + automaticModelDiscovery: 'Obter automaticamente', + manualModelConfiguration: 'Configuração manual', + discoverModelsHelper: + 'Obtenha os modelos pela API /v1/models do provedor e adicione todos ao catálogo de modelos', + discoverModelsFailedFallback: 'Não foi possível obter os modelos. Configure um modelo manualmente.', accountModelsDuplicate: 'Há modelos duplicados no catálogo', modelPool: 'Catálogo de modelos', fallbackModels: 'Modelos de fallback', @@ -872,7 +882,6 @@ const message = { dashboardAuthAutoRebuildHelper: 'Ao salvar o usuário ou a senha, o contêiner será reconstruído automaticamente para que as alterações entrem em vigor.', channelDeleteConfirm: 'Excluir a configuração do canal {0}?', - customProviderHelper: 'Provedores de modelo personalizados não validam se a conta está disponível', apiTypeBaseURLHelper: 'Este tipo envia requisições para {0}. Base URL recomendada: {1}', apiTypeBaseURLMismatch: 'A Base URL atual parece um caminho {0}, mas {1} está selecionado. URL recomendada: {2}', @@ -1056,9 +1065,6 @@ const message = { healthUnknown: 'Desconhecido', validationModelMapEmpty: 'Os nomes no mapeamento de modelos não podem ficar vazios', validationModelMapDuplicate: 'O modelo solicitado {0} está duplicado', - validateAvailability: 'Validar disponibilidade da conta', - validateAvailabilityHelper: - 'Envie uma mensagem mínima antes de salvar para validar a disponibilidade da API', modelGroup: 'Grupo de modelos', availableModels: 'Modelos disponíveis', modelGroupModels: 'Modelos solicitados', @@ -5151,8 +5157,10 @@ const message = { noBlackIp: 'O IP já está bloqueado, não é necessário bloquear novamente', noWhiteUrl: 'O URL já está na lista branca, não é necessário adicionar novamente', spiderIpHelper: - 'Inclui Baidu, Bing, Google, 360, Shenma, Sogou, ByteDance, DuckDuckGo, Yandex. Fechar isso bloqueará todos os acessos de spiders.', + 'Quando ativado, apenas os spiders selecionados são permitidos; quando desativado, todos são bloqueados.', spiderIp: 'Pool de IPs de spiders', + spiderAllowlist: 'Spiders permitidos', + spiderAllowlistEmpty: 'Nenhum spider selecionado. Ao salvar, todos os spiders serão bloqueados.', geoIp: 'Biblioteca de endereços IP', geoIpHelper: 'Usado para determinar a localização geográfica do IP', stat: 'Relatório de ataques', @@ -5289,6 +5297,7 @@ const message = { statusErr: 'Formato de código de status inválido', shenma: 'Busca Shenma', duckduckgo: 'DuckDuckGo', + yandex: 'Yandex', excludeUri: 'Excluir URIs', top100Helper: 'Mostrar os 100 principais dados', logSaveDay: 'Período de Retenção de Logs (dias)', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 8996589f8f16..e0ba6aec1c77 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -714,6 +714,15 @@ const message = { provider: 'Поставщик моделей', accountModelsHelper: 'Настройте модели, которые этот аккаунт предоставляет OpenClaw для выбора и настройки', accountModelsRequired: 'Выберите хотя бы одну модель', + verifyModel: 'Модель проверки', + verifyModelHelper: + 'Выберите модель, доступную этому аккаунту. При сохранении будет отправлен минимальный запрос для проверки подключения.', + verifyModelRequired: 'Выберите модель проверки', + discoverModels: 'Получить модели', + automaticModelDiscovery: 'Получить автоматически', + manualModelConfiguration: 'Настроить вручную', + discoverModelsHelper: 'Получить модели через API /v1/models поставщика и добавить их все в каталог моделей', + discoverModelsFailedFallback: 'Не удалось получить модели. Настройте модель вручную.', accountModelsDuplicate: 'В каталоге обнаружены повторяющиеся модели', modelPool: 'Каталог моделей', fallbackModels: 'Резервные модели', @@ -867,7 +876,6 @@ const message = { dashboardAuthAutoRebuildHelper: 'После сохранения имени пользователя или пароля контейнер будет автоматически пересобран, чтобы изменения вступили в силу.', channelDeleteConfirm: 'Удалить конфигурацию канала {0}?', - customProviderHelper: 'Для пользовательского провайдера модели доступность учетной записи не проверяется', apiTypeBaseURLHelper: 'Этот тип отправляет запросы в {0}. Рекомендуемый Base URL: {1}', apiTypeBaseURLMismatch: 'Текущий Base URL похож на путь {0}, но выбран {1}. Рекомендуемый URL: {2}', }, @@ -1050,9 +1058,6 @@ const message = { healthUnknown: 'Неизвестно', validationModelMapEmpty: 'Имена моделей в сопоставлении не могут быть пустыми', validationModelMapDuplicate: 'Запрошенная модель {0} дублируется', - validateAvailability: 'Проверить доступность аккаунта', - validateAvailabilityHelper: - 'Отправить минимальное сообщение перед сохранением для проверки доступности API', modelGroup: 'Группа моделей', availableModels: 'Доступные модели', modelGroupModels: 'Запрошенные модели', @@ -4999,8 +5004,11 @@ const message = { noBlackIp: 'IP уже заблокирован, не нужно блокировать снова', noWhiteUrl: 'URL уже в белом списке, не нужно добавлять снова', spiderIpHelper: - 'Включает Baidu, Bing, Google, 360, Shenma, Sogou, ByteDance, DuckDuckGo, Yandex. Закрытие этого параметра заблокирует доступ всех пауков.', + 'Если включено, разрешены только выбранные поисковые роботы; если выключено, доступ всех роботов блокируется.', spiderIp: 'Пул IP-адресов поисковых роботов', + spiderAllowlist: 'Разрешенные поисковые роботы', + spiderAllowlistEmpty: + 'Поисковые роботы не выбраны. После сохранения доступ всех роботов будет заблокирован.', geoIp: 'Библиотека IP-адресов', geoIpHelper: 'Используется для определения географического положения IP', stat: 'Отчет об атаках', @@ -5135,6 +5143,7 @@ const message = { statusErr: 'Неверный формат статус-кода', shenma: 'Shenma Search', duckduckgo: 'DuckDuckGo', + yandex: 'Yandex', excludeUri: 'Исключить URI', top100Helper: 'Показать топ 100 данных', logSaveDay: 'Период хранения логов (дни)', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 6852a1dab66a..905188ebad97 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -719,6 +719,16 @@ const message = { accountModelsHelper: "Bu hesabın OpenClaw'a sunacağı modelleri yapılandırın; seçim ve ayarlarda kullanılacaktır.", accountModelsRequired: 'En az bir model seçin', + verifyModel: 'Doğrulama Modeli', + verifyModelHelper: + 'Bu hesabın erişebildiği bir model seçin. Kaydederken bağlantıyı doğrulamak için en küçük istek gönderilir.', + verifyModelRequired: 'Bir doğrulama modeli seçin', + discoverModels: 'Modelleri getir', + automaticModelDiscovery: 'Otomatik getir', + manualModelConfiguration: 'Manuel yapılandır', + discoverModelsHelper: + 'Sağlayıcının /v1/models API’sinden modelleri alın ve tümünü model kataloğuna ekleyin', + discoverModelsFailedFallback: 'Modeller alınamadı. Bir modeli elle yapılandırın.', accountModelsDuplicate: 'Katalogda yinelenen modeller bulundu', modelPool: 'Model kataloğu', fallbackModels: 'Yedek modeller', @@ -875,7 +885,6 @@ const message = { dashboardAuthAutoRebuildHelper: 'Kullanıcı adı veya parola kaydedildiğinde değişikliklerin etkili olması için konteyner otomatik olarak yeniden oluşturulur.', channelDeleteConfirm: '{0} kanal yapılandırması silinsin mi?', - customProviderHelper: 'Özel model sağlayıcısında hesabın kullanılabilirliği doğrulanmaz', apiTypeBaseURLHelper: 'Bu tür istekleri {0} yoluna gönderir. Önerilen Base URL: {1}', apiTypeBaseURLMismatch: 'Geçerli Base URL bir {0} yolu gibi görünüyor, ancak {1} seçildi. Önerilen URL: {2}', @@ -1059,9 +1068,6 @@ const message = { healthUnknown: 'Bilinmiyor', validationModelMapEmpty: 'Model eşlemesindeki model adları boş olamaz', validationModelMapDuplicate: 'İstek modeli {0} yineleniyor', - validateAvailability: 'Hesap Kullanılabilirliğini Doğrula', - validateAvailabilityHelper: - 'Kaydetmeden önce API kullanılabilirliğini doğrulamak için en küçük iletiyi gönder', modelGroup: 'Model Grubu', availableModels: 'Kullanılabilir Modeller', modelGroupModels: 'İstek Modelleri', @@ -4996,8 +5002,10 @@ const message = { noBlackIp: 'IP zaten engellenmiş, tekrar engellemeye gerek yok', noWhiteUrl: 'URL zaten beyaz listede, tekrar eklemeye gerek yok', spiderIpHelper: - "Baidu, Bing, Google, 360, Shenma, Sogou, ByteDance, DuckDuckGo, Yandex'i içerir. Bunu kapatmak tüm örümcek erişimlerini engeller.", + 'Etkinleştirildiğinde yalnızca seçilen tarayıcılara izin verilir; devre dışı bırakıldığında tüm tarayıcılar engellenir.', spiderIp: 'Örümcek IP Havuzu', + spiderAllowlist: 'İzin verilen tarayıcılar', + spiderAllowlistEmpty: 'Hiçbir tarayıcı seçilmedi. Kaydedildiğinde tüm tarayıcılar engellenecek.', geoIp: 'IP Adres Kütüphanesi', geoIpHelper: 'IP’nin coğrafi konumunu doğrulamak için kullanılır', stat: 'Saldırı Raporu', @@ -5132,6 +5140,7 @@ const message = { statusErr: 'Geçersiz durum kodu formatı', shenma: 'Shenma Arama', duckduckgo: 'DuckDuckGo', + yandex: 'Yandex', excludeUri: 'URI’leri Hariç Tut', top100Helper: 'İlk 100 veriyi göster', logSaveDay: 'Günlük Saklama Süresi (gün)', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 0ab5449fbbec..e802cb55de8c 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -684,6 +684,14 @@ const message = { provider: '模型供應商', accountModelsHelper: '配置該帳號可提供給 OpenClaw 使用與切換的模型列表', accountModelsRequired: '請至少配置一個模型', + verifyModel: '驗證模型', + verifyModelHelper: '請選擇該帳號有權存取的模型,儲存時將使用它發送最小請求以驗證連線。', + verifyModelRequired: '請選擇驗證模型', + discoverModels: '取得模型', + automaticModelDiscovery: '自動取得', + manualModelConfiguration: '手動配置', + discoverModelsHelper: '從供應商的 /v1/models 介面取得模型,取得後將全部加入模型池', + discoverModelsFailedFallback: '取得模型失敗,請手動配置模型', accountModelsDuplicate: '模型池中存在重複模型,請檢查後重試', modelPool: '模型池', fallbackModels: '備用模型', @@ -819,7 +827,6 @@ const message = { channelAutoRestartHelper: '保存後將自動重新啟動容器以使設定生效。', dashboardAuthAutoRebuildHelper: '保存使用者名稱或密碼後將自動重建容器以使設定生效。', channelDeleteConfirm: '確認刪除 {0} 頻道設定?', - customProviderHelper: '自訂模型供應商不驗證帳號是否可用', apiTypeBaseURLHelper: '目前類型最終請求 {0},Base URL 建議填寫 {1}', apiTypeBaseURLMismatch: '目前 Base URL 看起來是 {0} 路徑,和已選擇的 {1} 不一致,建議改為 {2}', }, @@ -995,8 +1002,6 @@ const message = { healthUnknown: '未知', validationModelMapEmpty: '模型映射的模型名稱不能為空', validationModelMapDuplicate: '請求模型 {0} 重複', - validateAvailability: '驗證帳號可用性', - validateAvailabilityHelper: '儲存前發送最小訊息驗證介面可用性', modelGroup: '模型組', availableModels: '可用模型', modelGroupModels: '請求模型', @@ -4588,9 +4593,10 @@ const message = { addUrlsToWhite: '批次加白 URL', noBlackIp: 'IP 已封鎖,無需再次封鎖', noWhiteUrl: 'URL 已加白,無需再次加白', - spiderIpHelper: - '包含百度、Bing、Google、360、神馬、搜狗、位元組、DuckDuckGo、Yandex,關閉之後會攔截所有蜘蛛存取', + spiderIpHelper: '啟用後僅放行已選取的蜘蛛,停用後會攔截所有蜘蛛存取', spiderIp: '蜘蛛 IP 池', + spiderAllowlist: '蜘蛛放行範圍', + spiderAllowlistEmpty: '未選取任何蜘蛛,儲存後將攔截所有蜘蛛存取', geoIp: 'IP 位址庫', geoIpHelper: '用來確認 IP 的地理位置', stat: '攻擊報表', @@ -4722,6 +4728,7 @@ const message = { statusErr: '狀態碼格式錯誤', shenma: '神馬搜尋', duckduckgo: 'DuckDuckGo', + yandex: 'Yandex', exceptUri: '排除 Uri', top100Helper: '顯示 Top 100 的資料', logSaveDay: '日誌儲存天數', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index a114bc3bbc76..0930a93aada3 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -687,6 +687,14 @@ const message = { provider: '模型供应商', accountModelsHelper: '配置账号可提供给 OpenClaw 使用和切换的模型列表', accountModelsRequired: '请至少配置一个模型', + verifyModel: '验证模型', + verifyModelHelper: '请选择该账号有权限调用的模型,保存时将使用它发送最小请求验证连接。', + verifyModelRequired: '请选择验证模型', + discoverModels: '获取模型', + automaticModelDiscovery: '自动获取', + manualModelConfiguration: '手动配置', + discoverModelsHelper: '从供应商的 /v1/models 接口获取模型,获取后将全部加入模型池', + discoverModelsFailedFallback: '获取模型失败,请手动配置模型', accountModelsDuplicate: '模型池中存在重复模型,请检查后重试', modelPool: '模型池', fallbackModels: '备用模型', @@ -818,7 +826,6 @@ const message = { channelAutoRestartHelper: '保存后将自动重启容器以使配置生效。', dashboardAuthAutoRebuildHelper: '保存用户名密码后将自动重建容器以使配置生效。', channelDeleteConfirm: '确认删除 {0} 频道配置?', - customProviderHelper: '自定义模型供应商不验证账号是否可用', apiTypeBaseURLHelper: '当前类型最终请求 {0},Base URL 建议填写 {1}', apiTypeBaseURLMismatch: '当前 Base URL 看起来是 {0} 路径,和已选择的 {1} 不一致,建议改为 {2}', }, @@ -992,8 +999,6 @@ const message = { healthUnknown: '未知', validationModelMapEmpty: '模型映射的模型名不能为空', validationModelMapDuplicate: '请求模型 {0} 重复', - validateAvailability: '验证账号可用性', - validateAvailabilityHelper: '保存前发送最小消息验证接口可用性', modelGroup: '模型组', availableModels: '可用模型', modelGroupModels: '请求模型', @@ -4046,9 +4051,10 @@ const message = { addUrlsToWhite: '批量加白 URL', noBlackIp: 'IP 已拉黑,无需再次拉黑', noWhiteUrl: 'URL 已加白,无需再次加白', - spiderIpHelper: - '包含百度、Bing、谷歌、360、神马、搜狗、字节、DuckDuckGo、Yandex,关闭之后会拦截所有蜘蛛访问', + spiderIpHelper: '开启后仅放行已选择的蜘蛛,关闭后会拦截所有蜘蛛访问', spiderIp: '蜘蛛 IP 池', + spiderAllowlist: '蜘蛛放行范围', + spiderAllowlistEmpty: '未选择任何蜘蛛,保存后将拦截所有蜘蛛访问', geoIp: 'IP 地址库', geoIpHelper: '用来确认 IP 的地理位置', stat: '攻击报表', @@ -4174,6 +4180,7 @@ const message = { statusErr: '状态码格式错误', shenma: '神马搜索', duckduckgo: 'DuckDuckGo', + yandex: 'Yandex', '360': '360 搜索', excludeUri: '排除 Uri', top100Helper: '显示 Top 100 的数据', diff --git a/frontend/src/utils/agent.ts b/frontend/src/utils/agent.ts index 0870409b9032..f5a971b62370 100644 --- a/frontend/src/utils/agent.ts +++ b/frontend/src/utils/agent.ts @@ -15,6 +15,9 @@ export const getAgentProviderDisplayName = (provider: string, displayName?: stri return displayName || provider; }; +export const isAgentAccountVerificationSkipped = (provider?: string): boolean => + ['vllm', 'ollama', 'kimi-coding'].includes((provider || '').toLowerCase()); + export const supportsAgentModelConfig = (agentType: string): boolean => { return agentType === 'openclaw' || agentType === 'hermes-agent'; }; diff --git a/frontend/src/views/ai/agents/agent/config/tabs/skills/hermes.vue b/frontend/src/views/ai/agents/agent/config/tabs/skills/hermes.vue index f53a71503f8d..a0dc91b64f4e 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/skills/hermes.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/skills/hermes.vue @@ -371,6 +371,7 @@ defineExpose({ diff --git a/frontend/src/views/ai/agents/model/index.vue b/frontend/src/views/ai/agents/model/index.vue index 3921d3f6c0c0..f459523229bb 100644 --- a/frontend/src/views/ai/agents/model/index.vue +++ b/frontend/src/views/ai/agents/model/index.vue @@ -67,7 +67,7 @@ import ProviderLogo from '@/components/agent-provider-logo/index.vue'; import { ElMessageBox } from 'element-plus'; import i18n from '@/lang'; import { dateFormat } from '@/utils/date'; -import { getAgentProviderDisplayName } from '@/utils/agent'; +import { getAgentProviderDisplayName, isAgentAccountVerificationSkipped } from '@/utils/agent'; const items = ref([]); const addRef = ref(); @@ -111,9 +111,7 @@ const search = async () => { const openCreate = () => { if (addRef.value?.open) { - addRef.value.open({ - provider: 'deepseek', - }); + addRef.value.open(); } }; @@ -127,6 +125,9 @@ const onEdit = (row: AI.AgentAccountItem) => { apiKey: row.apiKey, rememberApiKey: row.rememberApiKey, apiType: row.apiType, + authMode: row.authMode, + verifyModel: row.verifyModel, + models: row.models, remark: row.remark, }); } @@ -138,20 +139,15 @@ const onManageModelPool = (row: AI.AgentAccountItem) => { } }; -const isVerificationSkipped = (provider: string) => { - const key = (provider || '').toLowerCase(); - return key === 'custom' || key === 'vllm' || key === 'ollama' || key === 'kimi-coding'; -}; - const verificationLabel = (row: AI.AgentAccountItem) => { - if (isVerificationSkipped(row.provider)) { + if (isAgentAccountVerificationSkipped(row.provider)) { return i18n.global.t('aiTools.agents.verifySkipped'); } return row.verified ? 'OK' : 'N/A'; }; const verificationTagType = (row: AI.AgentAccountItem) => { - if (isVerificationSkipped(row.provider)) { + if (isAgentAccountVerificationSkipped(row.provider)) { return 'info'; } return row.verified ? 'success' : 'info'; diff --git a/frontend/src/views/ai/agents/model/pool/index.vue b/frontend/src/views/ai/agents/model/pool/index.vue index ce934c1d46af..46b43ce7a308 100644 --- a/frontend/src/views/ai/agents/model/pool/index.vue +++ b/frontend/src/views/ai/agents/model/pool/index.vue @@ -10,20 +10,6 @@ - - - - - - - - @@ -43,21 +29,6 @@ - - - - - - - - - Text - Image - - - - -