Skip to content

Commit 80e80c5

Browse files
author
echoVic
committed
feat(model): 支持单次对话指定模型功能
添加模型切换功能,允许在单次对话中临时使用指定模型。主要修改包括: - 在多个类型定义中添加 modelId 字段 - 新增 getModelById 工具函数 - 实现模型切换逻辑 - 添加 /model once 命令支持 - 在 Agent 类中实现模型切换能力
1 parent a9a885e commit 80e80c5

11 files changed

Lines changed: 160 additions & 45 deletions

File tree

packages/cli/src/agent/Agent.ts

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
type PermissionConfig,
2020
PermissionMode,
2121
} from '../config/index.js';
22+
import type { ModelConfig } from '../config/types.js';
2223
import { CompactionService } from '../context/CompactionService.js';
2324
import { ContextManager } from '../context/ContextManager.js';
2425
import { HookManager } from '../hooks/HookManager.js';
@@ -49,6 +50,7 @@ function toJsonValue(value: string | object): JsonValue {
4950
}
5051
}
5152

53+
5254
import { discoverSkills, injectSkillsMetadata } from '../skills/index.js';
5355
import { SpecManager } from '../spec/SpecManager.js';
5456
import {
@@ -59,6 +61,7 @@ import {
5961
getConfig,
6062
getCurrentModel,
6163
getMcpServers,
64+
getModelById,
6265
getThinkingModeEnabled,
6366
} from '../store/vanilla.js';
6467
import { getBuiltinTools } from '../tools/builtin/index.js';
@@ -111,6 +114,7 @@ export class Agent {
111114

112115
// 当前模型的上下文窗口大小(用于 tokenUsage 上报)
113116
private currentModelMaxContextTokens!: number;
117+
private currentModelId?: string;
114118

115119
constructor(
116120
config: BladeConfig,
@@ -144,6 +148,57 @@ export class Agent {
144148
});
145149
}
146150

151+
private resolveModelConfig(requestedModelId?: string): ModelConfig {
152+
const modelId = requestedModelId && requestedModelId !== 'inherit' ? requestedModelId : undefined;
153+
const modelConfig = modelId ? getModelById(modelId) : getCurrentModel();
154+
if (!modelConfig) {
155+
throw new Error(`❌ 模型配置未找到: ${modelId ?? 'current'}`);
156+
}
157+
return modelConfig;
158+
}
159+
160+
private async applyModelConfig(modelConfig: ModelConfig, label: string): Promise<void> {
161+
this.log(`${label} ${modelConfig.name} (${modelConfig.model})`);
162+
163+
const modelSupportsThinking = isThinkingModel(modelConfig);
164+
const thinkingModeEnabled = getThinkingModeEnabled();
165+
const supportsThinking = modelSupportsThinking && thinkingModeEnabled;
166+
if (modelSupportsThinking && !thinkingModeEnabled) {
167+
this.log(`🧠 模型支持 Thinking,但用户未开启(按 Tab 开启)`);
168+
} else if (supportsThinking) {
169+
this.log(`🧠 Thinking 模式已启用,启用 reasoning_content 支持`);
170+
}
171+
172+
this.currentModelMaxContextTokens =
173+
modelConfig.maxContextTokens ?? this.config.maxContextTokens;
174+
175+
this.chatService = await createChatServiceAsync({
176+
provider: modelConfig.provider,
177+
apiKey: modelConfig.apiKey,
178+
model: modelConfig.model,
179+
baseUrl: modelConfig.baseUrl,
180+
temperature: modelConfig.temperature ?? this.config.temperature,
181+
maxContextTokens: this.currentModelMaxContextTokens,
182+
maxOutputTokens: modelConfig.maxOutputTokens ?? this.config.maxOutputTokens,
183+
timeout: this.config.timeout,
184+
supportsThinking,
185+
});
186+
187+
const contextManager = this.executionEngine?.getContextManager();
188+
this.executionEngine = new ExecutionEngine(this.chatService, contextManager);
189+
this.currentModelId = modelConfig.id;
190+
}
191+
192+
private async switchModelIfNeeded(modelId: string): Promise<void> {
193+
if (!modelId || modelId === this.currentModelId) return;
194+
const modelConfig = getModelById(modelId);
195+
if (!modelConfig) {
196+
this.log(`⚠️ 模型配置未找到: ${modelId}`);
197+
return;
198+
}
199+
await this.applyModelConfig(modelConfig, '🔁 切换模型');
200+
}
201+
147202
/**
148203
* 快速创建并初始化 Agent 实例(静态工厂方法)
149204
* 使用 Store 获取配置
@@ -211,43 +266,8 @@ export class Agent {
211266
await this.discoverSkills();
212267

213268
// 5. 初始化核心组件
214-
// 获取当前模型配置(从 Store)
215-
const modelConfig = getCurrentModel();
216-
if (!modelConfig) {
217-
throw new Error('❌ 当前模型配置未找到');
218-
}
219-
220-
this.log(`🚀 使用模型: ${modelConfig.name} (${modelConfig.model})`);
221-
222-
// 检测模型是否支持 thinking 模式,且用户已开启 thinking 模式
223-
const modelSupportsThinking = isThinkingModel(modelConfig);
224-
const thinkingModeEnabled = getThinkingModeEnabled();
225-
const supportsThinking = modelSupportsThinking && thinkingModeEnabled;
226-
if (modelSupportsThinking && !thinkingModeEnabled) {
227-
this.log(`🧠 模型支持 Thinking,但用户未开启(按 Tab 开启)`);
228-
} else if (supportsThinking) {
229-
this.log(`🧠 Thinking 模式已启用,启用 reasoning_content 支持`);
230-
}
231-
232-
// 保存当前模型的上下文窗口大小(用于 tokenUsage 上报)
233-
this.currentModelMaxContextTokens =
234-
modelConfig.maxContextTokens ?? this.config.maxContextTokens;
235-
236-
// 使用工厂函数创建 ChatService(根据 provider 选择实现)
237-
this.chatService = await createChatServiceAsync({
238-
provider: modelConfig.provider,
239-
apiKey: modelConfig.apiKey,
240-
model: modelConfig.model,
241-
baseUrl: modelConfig.baseUrl,
242-
temperature: modelConfig.temperature ?? this.config.temperature,
243-
maxContextTokens: this.currentModelMaxContextTokens, // 上下文窗口(压缩判断)
244-
maxOutputTokens: modelConfig.maxOutputTokens ?? this.config.maxOutputTokens, // 输出限制(API max_tokens)
245-
timeout: this.config.timeout,
246-
supportsThinking, // 传递 thinking 模式支持标志
247-
});
248-
249-
// 4. 初始化执行引擎
250-
this.executionEngine = new ExecutionEngine(this.chatService);
269+
const modelConfig = this.resolveModelConfig(this.runtimeOptions.modelId);
270+
await this.applyModelConfig(modelConfig, '🚀 使用模型:');
251271

252272
// 5. 初始化附件收集器(@ 文件提及)
253273
this.attachmentCollector = new AttachmentCollector({
@@ -814,7 +834,6 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
814834
// 3. 调用 ChatService(流式或非流式)
815835
// 默认启用流式,除非显式设置 stream: false
816836
const isStreamEnabled = options?.stream !== false;
817-
818837
const turnResult = isStreamEnabled
819838
? await this.processStreamResponse(messages, tools, options)
820839
: await this.chatService.chat(messages, tools, options?.signal);
@@ -1335,6 +1354,14 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
13351354
}
13361355
}
13371356

1357+
const modelId =
1358+
result.metadata?.modelId?.trim() ||
1359+
result.metadata?.model?.trim() ||
1360+
undefined;
1361+
if (modelId) {
1362+
await this.switchModelIfNeeded(modelId);
1363+
}
1364+
13381365
// 添加工具执行结果到消息历史
13391366
let toolResultContent = result.success
13401367
? result.llmContent || result.displayContent || ''

packages/cli/src/agent/subagents/BackgroundAgentManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,12 @@ export class BackgroundAgentManager {
199199
}
200200

201201
const systemPrompt = config.systemPrompt || '';
202+
const modelId =
203+
config.model && config.model !== 'inherit' ? config.model : undefined;
202204
const agent = await Agent.create({
203205
systemPrompt,
204206
toolWhitelist: config.tools,
207+
modelId,
205208
});
206209

207210
const context = {

packages/cli/src/agent/subagents/SubagentExecutor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@ export class SubagentExecutor {
2626
try {
2727
const systemPrompt = this.buildSystemPrompt(context);
2828

29+
const modelId =
30+
this.config.model && this.config.model !== 'inherit'
31+
? this.config.model
32+
: undefined;
2933
const agent = await Agent.create({
3034
toolWhitelist: this.config.tools,
35+
modelId,
3136
});
3237

3338
let finalMessage = '';

packages/cli/src/agent/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface AgentOptions {
5858
permissionMode?: PermissionMode;
5959
maxTurns?: number; // 最大对话轮次 (-1=无限制, 0=禁用对话, N>0=限制轮次)
6060
toolWhitelist?: string[]; // 工具白名单(仅允许指定工具)
61+
modelId?: string;
6162

6263
// MCP 配置
6364
mcpConfig?: string[]; // CLI 参数:MCP 配置文件路径或 JSON 字符串数组

packages/cli/src/slash-commands/model.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ const modelCommand: SlashCommand = {
1616
(无参数) 显示模型选择器(交互式切换)
1717
add 添加新模型配置(交互式向导)
1818
remove <名称> 删除指定模型配置(按名称匹配)
19+
once <模型> <内容> 仅当前轮对话使用指定模型
1920
2021
示例:
2122
/model # 显示模型选择器
2223
/model add # 添加新模型
2324
/model remove 千问 # 删除名称包含"千问"的模型
25+
/model once claude-sonnet 帮我总结这段代码
2426
`,
2527

2628
async handler(
@@ -86,6 +88,34 @@ const modelCommand: SlashCommand = {
8688
return { success: false, message: `❌ ${(error as Error).message}` };
8789
}
8890
}
91+
case 'once': {
92+
const modelQuery = args[1];
93+
const prompt = args.slice(2).join(' ').trim();
94+
if (!modelQuery || !prompt) {
95+
return {
96+
success: false,
97+
message: '❌ 用法: /model once <模型> <内容>',
98+
};
99+
}
100+
const models = getAllModels();
101+
const matched =
102+
models.find((m) => m.id === modelQuery) ||
103+
models.find((m) => m.name.toLowerCase().includes(modelQuery.toLowerCase()));
104+
if (!matched) {
105+
return {
106+
success: false,
107+
message: `❌ 未找到匹配的模型配置: ${modelQuery}`,
108+
};
109+
}
110+
return {
111+
success: true,
112+
data: {
113+
action: 'invoke_once_model',
114+
modelId: matched.id,
115+
prompt,
116+
},
117+
};
118+
}
89119

90120
default:
91121
return {

packages/cli/src/slash-commands/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ type SlashCommandAction =
1616
| 'show_plugins_manager'
1717
| 'invoke_skill'
1818
| 'invoke_custom_command'
19-
| 'invoke_plugin_command';
19+
| 'invoke_plugin_command'
20+
| 'invoke_once_model';
2021

2122
/**
2223
* Slash command 返回的结构化数据

packages/cli/src/store/vanilla.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ export const getCurrentModel = () => {
172172
return model ?? config.models[0];
173173
};
174174

175+
export const getModelById = (modelId: string) => {
176+
const config = getConfig();
177+
if (!config) return undefined;
178+
return config.models.find((m) => m.id === modelId);
179+
};
180+
175181
/**
176182
* 获取所有 MCP 服务器配置
177183
*/

packages/cli/src/tools/builtin/system/skill.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ Important:
9393
content.instructions,
9494
content.metadata.basePath
9595
);
96+
const requestedModelId =
97+
typeof content.metadata.model === 'string' &&
98+
content.metadata.model !== 'inherit' &&
99+
content.metadata.model.trim() !== ''
100+
? content.metadata.model
101+
: undefined;
96102

97103
// 返回双消息
98104
return {
@@ -107,6 +113,7 @@ Important:
107113
version: content.metadata.version,
108114
// allowed-tools: 限制 Skill 执行期间可用的工具
109115
allowedTools: content.metadata.allowedTools,
116+
modelId: requestedModelId,
110117
},
111118
};
112119
},

packages/cli/src/tools/types/ToolTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ interface BaseMetadataFields {
2929
summary?: string;
3030
shouldExitLoop?: boolean;
3131
targetMode?: PermissionMode;
32+
modelId?: string;
33+
model?: string;
3234
}
3335

3436
/**

packages/cli/src/ui/hooks/useAgent.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface AgentOptions {
1111
systemPrompt?: string;
1212
appendSystemPrompt?: string;
1313
maxTurns?: number;
14+
modelId?: string;
1415
}
1516

1617
/**
@@ -29,12 +30,13 @@ export function useAgent(options: AgentOptions) {
2930
/**
3031
* 创建并设置 Agent 实例
3132
*/
32-
const createAgent = useMemoizedFn(async (): Promise<Agent> => {
33+
const createAgent = useMemoizedFn(async (overrides?: Partial<AgentOptions>): Promise<Agent> => {
3334
// 创建新 Agent
3435
const agent = await Agent.create({
35-
systemPrompt: options.systemPrompt,
36-
appendSystemPrompt: options.appendSystemPrompt,
37-
maxTurns: options.maxTurns,
36+
systemPrompt: overrides?.systemPrompt ?? options.systemPrompt,
37+
appendSystemPrompt: overrides?.appendSystemPrompt ?? options.appendSystemPrompt,
38+
maxTurns: overrides?.maxTurns ?? options.maxTurns,
39+
modelId: overrides?.modelId ?? options.modelId,
3840
});
3941
agentRef.current = agent;
4042

0 commit comments

Comments
 (0)