From 6b20ff0b13bbf6d18a111dc887b901e8a77ed9c8 Mon Sep 17 00:00:00 2001 From: CityFun <31820853+zhengkunwang223@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:36:24 +0800 Subject: [PATCH] feat: OpenClaw supports configuring model Max Tokens (#13818) --- agent/app/dto/agents.go | 23 ++- agent/app/service/agents.go | 5 +- agent/app/service/agents_channels.go | 4 + agent/app/service/agents_utils.go | 155 +++++++++++++++++- frontend/src/api/interface/ai.ts | 9 + frontend/src/lang/modules/en.ts | 8 + frontend/src/lang/modules/es-es.ts | 8 + frontend/src/lang/modules/fa.ts | 8 + frontend/src/lang/modules/ja.ts | 8 + frontend/src/lang/modules/ko.ts | 8 + frontend/src/lang/modules/lo.ts | 8 + frontend/src/lang/modules/ms.ts | 8 + frontend/src/lang/modules/pt-br.ts | 8 + frontend/src/lang/modules/ru.ts | 8 + frontend/src/lang/modules/tr.ts | 8 + frontend/src/lang/modules/zh-Hant.ts | 7 + frontend/src/lang/modules/zh.ts | 7 + .../ai/agents/agent/config/tabs/model.vue | 83 ++++++++++ 18 files changed, 359 insertions(+), 14 deletions(-) diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 5a3e69709..30305882e 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -162,16 +162,25 @@ type AgentWebsiteBindReq struct { } type AgentModelConfigUpdateReq struct { - AgentID uint `json:"agentId" validate:"required"` - AccountID uint `json:"accountId" validate:"required"` - Model string `json:"model" validate:"required"` - Fallbacks []string `json:"fallbacks"` + AgentID uint `json:"agentId" validate:"required"` + AccountID uint `json:"accountId" validate:"required"` + Model string `json:"model" validate:"required"` + Fallbacks []string `json:"fallbacks"` + Metadata []AgentModelMetadata `json:"metadata" validate:"dive"` } type AgentModelConfig struct { - AccountID uint `json:"accountId"` - Model string `json:"model"` - Fallbacks []string `json:"fallbacks"` + AccountID uint `json:"accountId"` + Model string `json:"model"` + Fallbacks []string `json:"fallbacks"` + Metadata []AgentModelMetadata `json:"metadata"` +} + +type AgentModelMetadata struct { + Model string `json:"model" validate:"required"` + InputMode string `json:"inputMode" validate:"required,oneof=auto text image"` + ContextWindow int `json:"contextWindow" validate:"min=0"` + MaxTokens int `json:"maxTokens" validate:"min=0"` } type AgentHermesChatSessionItem struct { diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index e6ada78f5..f4d3d2c3e 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -936,6 +936,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, AccountID: agent.AccountID, Model: model, Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model), + Metadata: extractOpenclawModelMetadata(conf, account, models), }, nil } @@ -967,7 +968,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error if agent.AgentType != constant.AppOpenclaw { return fmt.Errorf("%s does not support", agent.AgentType) } - if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil { + if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil { return err } } @@ -1684,7 +1685,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { return err } fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID) - if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil { + if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil { return err } case constant.AppHermesAgent: diff --git a/agent/app/service/agents_channels.go b/agent/app/service/agents_channels.go index a16e13dba..f898a328a 100644 --- a/agent/app/service/agents_channels.go +++ b/agent/app/service/agents_channels.go @@ -1354,6 +1354,10 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID } else if slices.Contains(options, "--dangerously-force-unsafe-install") { args = append(args, "--dangerously-force-unsafe-install") } + // Source confirmation does not grant the selected channel plugin's capabilities. + if slices.Contains(options, "--accept-capabilities") { + args = append(args, "--accept-capabilities") + } return mgr.Run("docker", args...) } diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index 068db9bd2..94951826f 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -10,6 +10,7 @@ import ( "net/url" "path" "regexp" + "slices" "strconv" "strings" "time" @@ -737,9 +738,11 @@ type modelProvider struct { } type modelEntry struct { - ID string `json:"id"` - Name string `json:"name"` - Input []string `json:"input,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Input []string `json:"input,omitempty"` + ContextWindow int `json:"contextWindow,omitempty"` + MaxTokens int `json:"maxTokens,omitempty"` } func requiresOpenclawProviderModels(provider string) bool { @@ -767,7 +770,7 @@ type browserConfig struct { DefaultProfile string `json:"defaultProfile"` } -func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error { +func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error { if strings.TrimSpace(confDir) == "" { return fmt.Errorf("config dir is required") } @@ -852,6 +855,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, } conf = initial } else { + preserveOpenclawModelMetadata(conf, cfg.Models) if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil { return err } @@ -906,6 +910,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, if allowedOrigins != nil { setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins}) } + if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil { + return err + } if err := writeOpenclawConfigRaw(configPath, conf); err != nil { return err } @@ -920,6 +927,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order) } +func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig { + raw, ok := conf["models"] + if !ok { + return nil + } + payload, err := json.Marshal(raw) + if err != nil { + return nil + } + var models modelsConfig + if err := json.Unmarshal(payload, &models); err != nil { + return nil + } + return &models +} + +func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) { + current := readOpenclawModelsConfig(conf) + if current == nil || next == nil { + return + } + for providerID, nextProvider := range next.Providers { + currentProvider, ok := current.Providers[providerID] + if !ok { + continue + } + byID := make(map[string]modelEntry, len(currentProvider.Models)) + for _, entry := range currentProvider.Models { + byID[entry.ID] = entry + } + for index := range nextProvider.Models { + currentEntry, ok := byID[nextProvider.Models[index].ID] + if !ok { + continue + } + nextProvider.Models[index].Input = currentEntry.Input + nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow + nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens + } + next.Providers[providerID] = nextProvider + } +} + +func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata { + result := make([]dto.AgentModelMetadata, 0, len(accountModels)) + configured := readOpenclawModelsConfig(conf) + for _, item := range accountModels { + _, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item) + if err != nil { + continue + } + metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"} + if configured != nil { + for _, entry := range configured.Providers[providerID].Models { + if entry.ID != inferred.ID { + continue + } + metadata.ContextWindow = entry.ContextWindow + metadata.MaxTokens = entry.MaxTokens + if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) { + if slices.Contains(entry.Input, "image") { + metadata.InputMode = "image" + } else { + metadata.InputMode = "text" + } + } + break + } + } + result = append(result, metadata) + } + return result +} + +func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error { + if len(requested) == 0 { + return nil + } + configured := readOpenclawModelsConfig(conf) + if configured == nil { + return fmt.Errorf("model metadata is not supported for provider %s", account.Provider) + } + accountModels, err := loadAgentAccountModels(account) + if err != nil { + return err + } + available := make(map[string]dto.AgentAccountModel, len(accountModels)) + for _, item := range accountModels { + available[item.ID] = item + } + seen := make(map[string]struct{}, len(requested)) + for _, metadata := range requested { + item, ok := available[metadata.Model] + if !ok { + return buserr.New("ErrAgentModelNotInAccount") + } + if _, ok := seen[metadata.Model]; ok { + return fmt.Errorf("duplicate model metadata: %s", metadata.Model) + } + seen[metadata.Model] = struct{}{} + _, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item) + if err != nil { + return err + } + provider := configured.Providers[providerID] + found := false + for index := range provider.Models { + if provider.Models[index].ID != inferred.ID { + continue + } + found = true + provider.Models[index].ContextWindow = metadata.ContextWindow + provider.Models[index].MaxTokens = metadata.MaxTokens + switch metadata.InputMode { + case "auto": + provider.Models[index].Input = inferred.Input + case "text": + provider.Models[index].Input = []string{"text"} + case "image": + provider.Models[index].Input = []string{"text", "image"} + default: + return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode) + } + break + } + if !found { + return buserr.New("ErrAgentModelNotInAccount") + } + configured.Providers[providerID] = provider + } + modelsMap, err := structToMap(configured) + if err != nil { + return err + } + conf["models"] = modelsMap + return nil +} + func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) { accountModels, err := loadAgentAccountModels(account) if err != nil { @@ -1041,7 +1186,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag return fmt.Errorf("app install is required") } confDir := path.Join(appInstall.GetPath(), "data", "conf") - if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil { + if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil { return err } dataDir := path.Join(appInstall.GetPath(), "data") diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index 54809915c..74d7449e6 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -426,12 +426,21 @@ export namespace AI { accountId: number; model: string; fallbacks: string[]; + metadata: AgentModelMetadata[]; } export interface AgentModelConfig { accountId: number; model: string; fallbacks: string[]; + metadata: AgentModelMetadata[]; + } + + export interface AgentModelMetadata { + model: string; + inputMode: 'auto' | 'text' | 'image'; + contextWindow: number; + maxTokens: number; } export interface AgentHermesChatSessionItem { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index db08922d5..cbe701598 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -758,6 +758,14 @@ const message = { addFallbackModel: 'Add Fallback Model', fallbackModelsEmpty: 'No fallback models configured', modelInputTypes: 'Input Types', + modelMetadata: 'Model Capabilities', + modelMetadataHelper: + 'These settings apply only to this OpenClaw instance. Leave values empty to use OpenClaw or provider defaults.', + inputModeAuto: 'Auto detect', + inputModeText: 'Text only', + inputModeImage: 'Text and images', + contextWindow: 'Context Window', + useModelDefault: 'Use model default', reasoning: 'Reasoning Model', verified: 'Verified', verifySkipped: 'No verification', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index d7f18eb58..efa27abbe 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -764,6 +764,14 @@ const message = { addFallbackModel: 'Agregar modelo de respaldo', fallbackModelsEmpty: 'No hay modelos de respaldo configurados', modelInputTypes: 'Tipos de entrada', + modelMetadata: 'Capacidades del modelo', + modelMetadataHelper: + 'Estos ajustes solo se aplican a esta instancia de OpenClaw. Déjelos vacíos para usar los valores predeterminados de OpenClaw o del proveedor.', + inputModeAuto: 'Detectar automáticamente', + inputModeText: 'Solo texto', + inputModeImage: 'Texto e imágenes', + contextWindow: 'Ventana de contexto', + useModelDefault: 'Usar valor predeterminado del modelo', reasoning: 'Modelo de razonamiento', verified: 'Verificado', verifySkipped: 'Sin verificación', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index 8f4afbcb4..1b13fbb74 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -755,6 +755,14 @@ const message = { addFallbackModel: 'افزودن مدل جایگزین', fallbackModelsEmpty: 'هیچ مدل جایگزینی پیکربندی نشده است', modelInputTypes: 'انواع ورودی', + modelMetadata: 'قابلیت‌های مدل', + modelMetadataHelper: + 'این تنظیمات فقط برای نمونه فعلی OpenClaw اعمال می‌شوند. برای استفاده از پیش‌فرض OpenClaw یا ارائه‌دهنده، فیلدها را خالی بگذارید.', + inputModeAuto: 'تشخیص خودکار', + inputModeText: 'فقط متن', + inputModeImage: 'متن و تصویر', + contextWindow: 'پنجره زمینه', + useModelDefault: 'استفاده از پیش‌فرض مدل', reasoning: 'مدل استدلالی', verified: 'تأیید شده', verifySkipped: 'بدون تأیید', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index cb16eda14..58bb66b15 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -759,6 +759,14 @@ const message = { addFallbackModel: 'フォールバックモデルを追加', fallbackModelsEmpty: 'フォールバックモデルは未設定です', modelInputTypes: '入力タイプ', + modelMetadata: 'モデル機能設定', + modelMetadataHelper: + 'この設定は現在の OpenClaw にのみ適用されます。空欄の場合は OpenClaw またはプロバイダーの既定値を使用します。', + inputModeAuto: '自動判定', + inputModeText: 'テキストのみ', + inputModeImage: 'テキストと画像', + contextWindow: 'コンテキストウィンドウ', + useModelDefault: 'モデルの既定値を使用', reasoning: '推論モデル', verified: '検証済み', verifySkipped: '検証なし', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index b56625f19..f5da2e7c3 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -749,6 +749,14 @@ const message = { addFallbackModel: '폴백 모델 추가', fallbackModelsEmpty: '구성된 폴백 모델이 없습니다', modelInputTypes: '입력 유형', + modelMetadata: '모델 기능 설정', + modelMetadataHelper: + '이 설정은 현재 OpenClaw에만 적용됩니다. 비워 두면 OpenClaw 또는 공급자의 기본값을 사용합니다.', + inputModeAuto: '자동 감지', + inputModeText: '텍스트만', + inputModeImage: '텍스트 및 이미지', + contextWindow: '컨텍스트 창', + useModelDefault: '모델 기본값 사용', reasoning: '추론 모델', verified: '검증됨', verifySkipped: '검증 안 함', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 34585bd6b..ee1ba5243 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -750,6 +750,14 @@ const message = { addFallbackModel: 'ເພີ່ມໂມເດວສຳຮອງ', fallbackModelsEmpty: 'ຍັງບໍ່ໄດ້ຕັ້ງຄ່າໂມເດວສຳຮອງ', modelInputTypes: 'ປະເພດຂໍ້ມູນຂາເຂົ້າ', + modelMetadata: 'ຄວາມສາມາດຂອງໂມເດວ', + modelMetadataHelper: + 'ການຕັ້ງຄ່ານີ້ໃຊ້ກັບ OpenClaw ປັດຈຸບັນເທົ່ານັ້ນ. ປ່ອຍຫວ່າງເພື່ອໃຊ້ຄ່າເລີ່ມຕົ້ນຂອງ OpenClaw ຫຼືຜູ້ໃຫ້ບໍລິການ.', + inputModeAuto: 'ກວດຫາອັດຕະໂນມັດ', + inputModeText: 'ຂໍ້ຄວາມເທົ່ານັ້ນ', + inputModeImage: 'ຂໍ້ຄວາມແລະຮູບພາບ', + contextWindow: 'ຂອບເຂດບໍລິບົດ', + useModelDefault: 'ໃຊ້ຄ່າເລີ່ມຕົ້ນຂອງໂມເດວ', reasoning: 'ໂມເດວການໃຊ້ເຫດຜົນ', verified: 'ກວດສອບແລ້ວ', verifySkipped: 'ບໍ່ມີການກວດສອບ', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 6df7d9a5d..958fa1f40 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -767,6 +767,14 @@ const message = { addFallbackModel: 'Tambah model fallback', fallbackModelsEmpty: 'Tiada model fallback dikonfigurasikan', modelInputTypes: 'Jenis input', + modelMetadata: 'Keupayaan model', + modelMetadataHelper: + 'Tetapan ini hanya digunakan pada tika OpenClaw semasa. Biarkan kosong untuk menggunakan nilai lalai OpenClaw atau penyedia.', + inputModeAuto: 'Kesan secara automatik', + inputModeText: 'Teks sahaja', + inputModeImage: 'Teks dan imej', + contextWindow: 'Tetingkap konteks', + useModelDefault: 'Gunakan lalai model', reasoning: 'Model penaakulan', verified: 'Disahkan', verifySkipped: 'Tanpa pengesahan', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 1f607c590..777174d8b 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -761,6 +761,14 @@ const message = { addFallbackModel: 'Adicionar modelo de fallback', fallbackModelsEmpty: 'Nenhum modelo de fallback configurado', modelInputTypes: 'Tipos de entrada', + modelMetadata: 'Recursos do modelo', + modelMetadataHelper: + 'Estas configurações se aplicam somente a esta instância do OpenClaw. Deixe em branco para usar os padrões do OpenClaw ou do provedor.', + inputModeAuto: 'Detectar automaticamente', + inputModeText: 'Somente texto', + inputModeImage: 'Texto e imagens', + contextWindow: 'Janela de contexto', + useModelDefault: 'Usar padrão do modelo', reasoning: 'Modelo de raciocínio', verified: 'Verificado', verifySkipped: 'Sem verificação', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 370c2e4ab..84c70e683 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -756,6 +756,14 @@ const message = { addFallbackModel: 'Добавить резервную модель', fallbackModelsEmpty: 'Резервные модели не настроены', modelInputTypes: 'Типы ввода', + modelMetadata: 'Возможности модели', + modelMetadataHelper: + 'Эти настройки применяются только к текущему экземпляру OpenClaw. Оставьте поля пустыми, чтобы использовать значения OpenClaw или поставщика.', + inputModeAuto: 'Определять автоматически', + inputModeText: 'Только текст', + inputModeImage: 'Текст и изображения', + contextWindow: 'Контекстное окно', + useModelDefault: 'Использовать значение модели', reasoning: 'Модель рассуждений', verified: 'Проверено', verifySkipped: 'Без проверки', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index fad6f2642..d2af72788 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -762,6 +762,14 @@ const message = { addFallbackModel: 'Yedek model ekle', fallbackModelsEmpty: 'Yapılandırılmış yedek model yok', modelInputTypes: 'Girdi türleri', + modelMetadata: 'Model yetenekleri', + modelMetadataHelper: + 'Bu ayarlar yalnızca mevcut OpenClaw örneğine uygulanır. OpenClaw veya sağlayıcı varsayılanlarını kullanmak için boş bırakın.', + inputModeAuto: 'Otomatik algıla', + inputModeText: 'Yalnızca metin', + inputModeImage: 'Metin ve görseller', + contextWindow: 'Bağlam penceresi', + useModelDefault: 'Model varsayılanını kullan', reasoning: 'Akıl yürütme modeli', verified: 'Doğrulandı', verifySkipped: 'Doğrulama yok', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 0c613406a..bb1d60f47 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -729,6 +729,13 @@ const message = { addFallbackModel: '新增備用模型', fallbackModelsEmpty: '尚未設定備用模型', modelInputTypes: '輸入類型', + modelMetadata: '模型能力設定', + modelMetadataHelper: '設定僅套用於目前的 OpenClaw。留空時使用 OpenClaw 或模型供應商的預設值。', + inputModeAuto: '自動識別', + inputModeText: '僅文字', + inputModeImage: '文字和圖片', + contextWindow: '上下文視窗', + useModelDefault: '使用模型預設值', reasoning: '推理模型', verified: '驗證狀態', verifySkipped: '略過驗證', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index fed9b3d61..e81316518 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -746,6 +746,13 @@ const message = { addFallbackModel: '新增备用模型', fallbackModelsEmpty: '尚未配置备用模型', modelInputTypes: '输入类型', + modelMetadata: '模型能力配置', + modelMetadataHelper: '配置仅作用于当前 OpenClaw。留空时使用 OpenClaw 或模型供应商的默认值。', + inputModeAuto: '自动识别', + inputModeText: '仅文本', + inputModeImage: '文本和图片', + contextWindow: '上下文窗口', + useModelDefault: '使用模型默认值', reasoning: '推理模型', verified: '验证状态', verifySkipped: '不验证', diff --git a/frontend/src/views/ai/agents/agent/config/tabs/model.vue b/frontend/src/views/ai/agents/agent/config/tabs/model.vue index 32f0cac81..45c0b39fe 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/model.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/model.vue @@ -55,6 +55,46 @@ + {{ t('commons.button.save') }} @@ -88,6 +128,16 @@ const agentType = ref('openclaw'); const accountOptions = ref([]); const modelOptions = ref([]); const showFallbacks = computed(() => agentType.value === 'openclaw'); +const metadataRows = ref< + Array< + Omit & { + contextWindow?: number; + maxTokens?: number; + } + > +>([]); +const selectedAccount = computed(() => accountOptions.value.find((item) => item.id === form.accountId)); +const showMetadata = computed(() => agentType.value === 'openclaw' && selectedAccount.value?.provider !== 'gemini'); interface AgentModelLoadParams { agentId: number; @@ -145,11 +195,25 @@ const syncFallbacks = (fallbacks?: string[]) => { } }; +const syncMetadata = (metadata: AI.AgentModelMetadata[] = []) => { + const configured = new Map(metadata.map((item) => [item.model, item])); + metadataRows.value = modelOptions.value.map((model) => { + const item = configured.get(model.id); + return { + model: model.id, + inputMode: item?.inputMode || 'auto', + contextWindow: item?.contextWindow || undefined, + maxTokens: item?.maxTokens || undefined, + }; + }); +}; + const handleAccountChange = () => { setModelOptionsByAccount(form.accountId); ensureSelectedModel(); form.fallbacks = []; fallbackCandidate.value = ''; + syncMetadata(); }; const handleModelChange = () => { @@ -199,6 +263,7 @@ const load = async (params: AgentModelLoadParams) => { form.model = ''; form.fallbacks = []; modelOptions.value = []; + metadataRows.value = []; return; } const config = configRes.data; @@ -208,6 +273,7 @@ const load = async (params: AgentModelLoadParams) => { setModelOptionsByAccount(currentAccount.id); ensureSelectedModel(config.model); syncFallbacks(showFallbacks.value ? config.fallbacks || [] : []); + syncMetadata(showMetadata.value ? config.metadata || [] : []); } finally { loading.value = false; } @@ -225,6 +291,14 @@ const saveModel = async () => { accountId: form.accountId, model: form.model, fallbacks: showFallbacks.value ? form.fallbacks : [], + metadata: showMetadata.value + ? metadataRows.value.map((item) => ({ + model: item.model, + inputMode: item.inputMode, + contextWindow: item.contextWindow || 0, + maxTokens: item.maxTokens || 0, + })) + : [], }); MsgSuccess(t('aiTools.agents.switchModelSuccess')); emit('updated'); @@ -257,4 +331,13 @@ defineExpose({ padding: 12px 0; color: var(--el-text-color-secondary); } + +.metadata-help { + display: block; + margin: -8px 0 12px; +} + +:deep(.el-input-number) { + width: 100%; +}