feat(agent): re-enable per-Agent model override

This commit is contained in:
matevip 2026-05-02 15:40:24 +08:00
parent b0f5dadbe6
commit 92bd8e9b6e
6 changed files with 61 additions and 4 deletions

View File

@ -155,9 +155,13 @@ public class AgentGraphBuilder {
// the agent's bound-skill requires-model. Falls back to the
// global default when no preferred provider satisfies, so the
// existing "no default model" error path stays intact.
// RFC-03 Lane G1 honor per-Agent model override when set.
// resolveModel() looks up entity.modelName in enabled-only models;
// null / blank / unmatched silently fall back to getDefaultModel(),
// preserving the legacy behavior for Agents without an override.
ModelConfigEntity globalDefault;
try {
globalDefault = modelConfigService.getDefaultModel();
globalDefault = modelConfigService.resolveModel(entity.getModelName());
} catch (Exception e) {
throw new MateClawException("err.agent.no_default_model", "无法构建 Agent请先在「设置 → 模型」中配置并启用默认模型");
}

View File

@ -31,10 +31,22 @@ public class AgentEntity {
private String systemPrompt;
/**
* 保留但不再生效运行时统一使用全局默认模型ModelConfigService.getDefaultModel()
* 该字段为历史残留仅保留以避免数据库迁移
* Per-Agent model override.
*
* <p>When non-blank, the runtime resolves this value via
* {@code ModelConfigService.resolveModel(...)} a case-sensitive,
* enabled-only lookup against {@code mate_model_config.model_name}.
* On match, the resolved entity is used as the primary model in
* place of {@code getDefaultModel()}.
*
* <p>Null / blank fall back to the global default (preserves the
* original behavior). Stale rows whose named model has been removed
* or disabled also fall back, since {@code resolveModel} returns the
* default when no enabled match is found.
*
* <p>RFC-03 Lane G1 re-enables this field after it was silently
* deprecated in earlier work; the database column is unchanged.
*/
@Deprecated
private String modelName;
/** 最大迭代次数 */

View File

@ -233,6 +233,7 @@ err.llm.pkce_failed=PKCE \u751f\u6210\u5931\u8d25
err.llm.chatgpt_stream_failed=ChatGPT \u6d41\u5f0f\u8c03\u7528\u5931\u8d25
err.llm.chatgpt_error=ChatGPT \u8fd4\u56de\u9519\u8bef
err.llm.chatgpt_account_missing=chatgpt-account-id \u7f3a\u5931
err.llm.model_not_supported=\u6a21\u578b ID \u4e0d\u652f\u6301\u5728 DashScope \u539f\u751f\u534f\u8bae\u4e2d\u4f7f\u7528\u3002\u70b9\u7248\u672c\u683c\u5f0f\u7684\u7cfb\u5217\uff08\u4f8b\u5982 qwen3.5-*\u3001qwen3.6-*\uff09\u53ea\u80fd\u901a\u8fc7\u517c\u5bb9\u6a21\u5f0f\u4f7f\u7528\u3002\u8bf7\u4f7f\u7528\u5141\u8bb8\u7684 ID\uff0c\u5982 qwen-max / qwen-plus / qwen3-max\u3002
# datasource
err.datasource.not_found=\u6570\u636e\u6e90\u4e0d\u5b58\u5728
err.datasource.sql_empty=SQL \u4e0d\u80fd\u4e3a\u7a7a

View File

@ -773,6 +773,9 @@ export default {
systemPrompt: 'System Prompt',
maxIterations: 'Max Iterations',
defaultThinkingLevel: 'Default Thinking Level',
modelName: 'Model',
modelGlobalDefault: 'Use global default',
modelHint: 'Override the global default model for this Agent. Leave blank to follow Settings → Models.',
tags: 'Tags',
enabled: 'Enabled',
},

View File

@ -775,6 +775,9 @@ export default {
systemPrompt: '系统提示词',
maxIterations: '最大迭代次数',
defaultThinkingLevel: '默认思考深度',
modelName: '模型',
modelGlobalDefault: '使用全局默认模型',
modelHint: '为此 Agent 单独指定模型,留空则跟随「设置 → 模型」中的全局默认。',
tags: '标签',
enabled: '启用',
},

View File

@ -204,6 +204,18 @@
<label class="form-label">{{ t('agents.fields.maxIterations') }}</label>
<input v-model.number="form.maxIterations" type="number" min="1" max="50" class="form-input" />
</div>
<!-- RFC-03 Lane G1: per-Agent model override. Empty value falls
back to the global default in ModelConfigService.resolveModel. -->
<div class="form-group">
<label class="form-label">{{ t('agents.fields.modelName') }}</label>
<select v-model="form.modelName" class="form-input">
<option value="">{{ t('agents.fields.modelGlobalDefault') }}</option>
<option v-for="m in availableModels" :key="m.id" :value="m.modelName">
{{ m.name }} ({{ m.provider }}/{{ m.modelName }})
</option>
</select>
<p class="form-hint">{{ t('agents.fields.modelHint') }}</p>
</div>
<div class="form-group">
<label class="form-label">{{ t('agents.fields.defaultThinkingLevel') }}</label>
<select v-model="form.defaultThinkingLevel" class="form-input">
@ -368,6 +380,9 @@ const selectedToolNames = ref<string[]>([])
// RFC-009 PR-3: per-agent provider preference order
const availableProviders = ref<{ id: string; name: string }[]>([])
const selectedProviderIds = ref<string[]>([])
// RFC-03 Lane G1: per-Agent model override picker populated from the
// global enabled-models list, blank value means "fall back to default".
const availableModels = ref<Array<{ id: number; name: string; provider: string; modelName: string }>>([])
// Template selector state
const showTemplateSelector = ref(false)
@ -387,6 +402,7 @@ const defaultForm = (): Partial<Agent> & { name: string; defaultThinkingLevel: s
description: '',
agentType: 'react',
systemPrompt: '',
modelName: '', // RFC-03 G1 empty means "use global default"
maxIterations: 10,
// Empty so SkillIcon's fallback (🤖) shows instead of pinning a literal
// emoji into form.icon otherwise the picker thinks the user picked
@ -419,6 +435,9 @@ const filteredAgents = computed(() => {
onMounted(() => {
loadAgents()
// RFC-03 G1: load models once for the per-Agent override dropdown.
// Failure is non-fatal the dropdown just shows only "global default".
loadAvailableModels()
})
async function loadAgents() {
@ -430,6 +449,20 @@ async function loadAgents() {
}
}
async function loadAvailableModels() {
try {
const res: any = await modelApi.listEnabled()
availableModels.value = (res.data || []).map((m: any) => ({
id: m.id,
name: m.name,
provider: m.provider,
modelName: m.modelName,
}))
} catch {
// Silent the picker still works (empty list = only "default" option).
}
}
function parseTags(tags: string): string[] {
return tags.split(',').map(s => s.trim()).filter(Boolean)
}
@ -515,6 +548,7 @@ async function openEditModal(agent: Agent) {
description: agent.description || '',
agentType: agent.agentType,
systemPrompt: agent.systemPrompt || '',
modelName: agent.modelName || '',
maxIterations: agent.maxIterations,
icon: agent.icon || '',
tags: agent.tags || '',