mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-13 06:30:45 +08:00
feat: add reasoning effort commands
This commit is contained in:
parent
36cbbe48e7
commit
9e39333ff6
24 changed files with 1346 additions and 240 deletions
|
|
@ -801,6 +801,12 @@ test('DingTalk lists models and presets without prompting and advertises fast co
|
|||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
||||
await bridge.accept(message('reasoning-dingtalk', '/reasoninglist'));
|
||||
assert.match(sent.at(-1).text, /还没有会话/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
||||
const presetReplyStart = sent.length;
|
||||
await bridge.accept(message('presets-dingtalk', '/presetlist'));
|
||||
const presetReplies = sent.slice(presetReplyStart).map((entry) => entry.text);
|
||||
|
|
@ -832,10 +838,15 @@ test('DingTalk lists models and presets without prompting and advertises fast co
|
|||
|
||||
await bridge.accept(message('help-models-dingtalk', '/help'));
|
||||
const help = sent.at(-1).text;
|
||||
for (const command of ['/models', '/model', '/presetlist', '/preset', '/preset --default', '/stop', '/steer']) {
|
||||
for (const command of [
|
||||
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
|
||||
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
|
||||
]) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
assert.match(help, /\/model .*\[推理等级ID\]/);
|
||||
assert.match(help, /示例:先发 \/models,再发 \/model 2 \[推理等级ID\]/);
|
||||
assert.doesNotMatch(help, /\/model 2 high\b/);
|
||||
assert.match(help, /\/preset id:<ID>/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -344,6 +344,13 @@ test('Feishu lists models and presets without prompting and advertises fast comm
|
|||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
||||
await bridge.accept(event('reasoning-feishu', '/reasoninglist'));
|
||||
await bridge.waitForIdle();
|
||||
assert.match(sent.at(-1), /还没有会话/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
||||
const presetReplyStart = sent.length;
|
||||
await bridge.accept(event('presets-feishu', '/presetlist'));
|
||||
await bridge.waitForIdle();
|
||||
|
|
@ -380,10 +387,13 @@ test('Feishu lists models and presets without prompting and advertises fast comm
|
|||
await bridge.accept(event('help-feishu', '/help'));
|
||||
await bridge.waitForIdle();
|
||||
const help = sent.at(-1);
|
||||
for (const command of ['/models', '/model', '/presetlist', '/preset', '/preset --default', '/stop', '/steer']) {
|
||||
for (const command of [
|
||||
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
|
||||
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
|
||||
]) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
assert.match(help, /\/model .*\[推理等级ID\]/);
|
||||
assert.match(help, /\/preset id:<ID>/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -64,12 +64,21 @@ test('menu exposes the increased command set and keeps repair number-only', () =
|
|||
assert.equal(actions.includes('repair'), false);
|
||||
});
|
||||
|
||||
test('menu help advertises Agent Preset commands', () => {
|
||||
test('menu and card help advertise Agent Preset and reasoning commands', () => {
|
||||
const help = menuHelpText();
|
||||
assert.match(help, /\/presetlist/);
|
||||
assert.match(help, /\/preset \[序号或完整ID\]/);
|
||||
assert.match(help, /\/preset id:<ID>/);
|
||||
assert.match(help, /\/preset --default/);
|
||||
assert.match(help, /\/reasoninglist 或 \/reasonings/);
|
||||
assert.match(help, /\/reasoning \[序号、等级ID或 --default\]/);
|
||||
assert.match(help, /\/model \[序号或完整模型ID\] \[推理等级ID\]/);
|
||||
|
||||
const card = helpCard();
|
||||
assert.match(card, /\/reasoninglist/);
|
||||
assert.match(card, /\/reasonings/);
|
||||
assert.match(card, /\/reasoning \[序号、等级ID或 --default\]/);
|
||||
assert.match(card, /\/model \[序号或完整模型ID\] \[推理等级ID\]/);
|
||||
});
|
||||
|
||||
test('card-action probe carries only its action and opaque nonce', () => {
|
||||
|
|
|
|||
|
|
@ -633,6 +633,12 @@ test('QQ lists models and presets without prompting and advertises fast commands
|
|||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
||||
await bridge.accept(message({ messageId: 'reasoning-qq', content: '/reasoninglist' }));
|
||||
assert.match(sent.at(-1), /还没有会话/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
||||
const presetReplyStart = sent.length;
|
||||
await bridge.accept(message({ messageId: 'presets-qq', content: '/presetlist' }));
|
||||
const presetReplies = sent.slice(presetReplyStart);
|
||||
|
|
@ -664,10 +670,15 @@ test('QQ lists models and presets without prompting and advertises fast commands
|
|||
|
||||
await bridge.accept(message({ messageId: 'help-models-qq', content: '/help' }));
|
||||
const help = sent.at(-1);
|
||||
for (const command of ['/models', '/model', '/presetlist', '/preset', '/preset --default', '/stop', '/steer']) {
|
||||
for (const command of [
|
||||
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
|
||||
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
|
||||
]) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
assert.match(help, /\/model .*\[推理等级ID\]/);
|
||||
assert.match(help, /示例:先发 \/models,再发 \/model 2 \[推理等级ID\]/);
|
||||
assert.doesNotMatch(help, /\/model 2 high\b/);
|
||||
assert.match(help, /\/preset id:<ID>/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -93,11 +93,81 @@ test('HarnessClient exposes and validates model and run-state RPCs', async () =>
|
|||
assert.ok(calls.filter(([type]) => type === 'ensureRunning').every(([, value]) => value === options));
|
||||
});
|
||||
|
||||
test('HarnessClient preserves reasoning metadata and forwards an optional reasoning effort', async () => {
|
||||
const { calls, client, responses } = modelClient();
|
||||
const reasoningCatalog = {
|
||||
groups: [{
|
||||
id: 'deepseek-official',
|
||||
name: 'DeepSeek',
|
||||
models: [{
|
||||
id: 'deepseek-v4',
|
||||
name: 'DeepSeek V4',
|
||||
description: 'Reasoning model',
|
||||
reasoning: {
|
||||
efforts: [
|
||||
{ id: 'off', name: 'Off' },
|
||||
{ id: 'high', name: 'High', description: 'More thinking' },
|
||||
],
|
||||
defaultEffort: 'high',
|
||||
},
|
||||
}],
|
||||
}],
|
||||
failures: [],
|
||||
};
|
||||
responses.set('llm.models', reasoningCatalog);
|
||||
responses.set('session.models', {
|
||||
...reasoningCatalog,
|
||||
current: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
routable: true,
|
||||
});
|
||||
responses.set('session.selectModel', {
|
||||
selected: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4',
|
||||
reasoningEffort: 'off',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal((await client.listModels()).groups[0].models[0].reasoning.defaultEffort, 'high');
|
||||
assert.equal((await client.getSessionModels('session-one')).current.reasoningEffort, 'high');
|
||||
await client.selectSessionModel('session-one', {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4',
|
||||
reasoningEffort: 'off',
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
calls.find((entry) => entry[0] === 'rpc' && entry[1] === 'session.selectModel')?.[2],
|
||||
{
|
||||
sessionId: 'session-one',
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4',
|
||||
reasoningEffort: 'off',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('HarnessClient rejects malformed model and run-state responses', async () => {
|
||||
const { client, responses } = modelClient();
|
||||
responses.set('llm.models', { groups: null, failures: [] });
|
||||
await assert.rejects(client.listModels(), /invalid response for llm\.models/);
|
||||
|
||||
responses.set('llm.models', {
|
||||
...CATALOG,
|
||||
groups: [{
|
||||
...CATALOG.groups[0],
|
||||
models: [{
|
||||
...CATALOG.groups[0].models[0],
|
||||
reasoning: { efforts: [] },
|
||||
}],
|
||||
}],
|
||||
});
|
||||
await assert.rejects(client.listModels(), /invalid response for llm\.models/);
|
||||
|
||||
responses.set('session.models', {
|
||||
...CATALOG,
|
||||
current: { provider: 'deepseek-official' },
|
||||
|
|
@ -108,12 +178,35 @@ test('HarnessClient rejects malformed model and run-state responses', async () =
|
|||
/invalid response for session\.models/,
|
||||
);
|
||||
|
||||
responses.set('session.models', {
|
||||
...CATALOG,
|
||||
current: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4',
|
||||
reasoningEffort: '',
|
||||
},
|
||||
routable: true,
|
||||
});
|
||||
await assert.rejects(
|
||||
client.getSessionModels('session-one'),
|
||||
/invalid response for session\.models/,
|
||||
);
|
||||
|
||||
responses.set('session.selectModel', { selected: { provider: '', model: 'bad' } });
|
||||
await assert.rejects(
|
||||
client.selectSessionModel('session-one', { provider: 'p', model: 'm' }),
|
||||
/invalid response for session\.selectModel/,
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
client.selectSessionModel('session-one', {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4',
|
||||
reasoningEffort: '',
|
||||
}),
|
||||
/provider and model are required/,
|
||||
);
|
||||
|
||||
responses.set('session.list', { items: [{ sessionId: 'session-one', running: 'yes' }] });
|
||||
await assert.rejects(client.isSessionRunning('session-one'), /invalid response for session\.list/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ test('t() translates known keys and fills placeholders in English mode', () => {
|
|||
}
|
||||
assert.equal(t('未收录的中文'), '未收录的中文');
|
||||
assert.equal(t('{value} 测试', { value: 'x' }), EN['{value} 测试'] ?? 'x 测试');
|
||||
assert.equal(
|
||||
t('示例:先发 /models,再发 /model 2 [推理等级ID]'),
|
||||
'Example: send /models first, then /model 2 [reasoning effort ID]',
|
||||
);
|
||||
assert.equal(t(42), 42);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -420,6 +420,12 @@ test('all four shared text channels list models and presets locally and advertis
|
|||
assert.equal(creates, 0, `${name} create`);
|
||||
assert.equal(fixture.sessions.size, 0, `${name} session binding`);
|
||||
|
||||
await bridge.accept(message(`reasoning-${name}`, '/reasoninglist'));
|
||||
assert.match(sent.at(-1), /还没有会话/, `${name} reasoning command`);
|
||||
assert.equal(asks, 0, `${name} reasoning ask`);
|
||||
assert.equal(creates, 0, `${name} reasoning create`);
|
||||
assert.equal(fixture.sessions.size, 0, `${name} reasoning session binding`);
|
||||
|
||||
const presetReplyStart = sent.length;
|
||||
await bridge.accept(message(`presets-${name}`, '/presetlist'));
|
||||
const presetReplies = sent.slice(presetReplyStart);
|
||||
|
|
@ -451,10 +457,15 @@ test('all four shared text channels list models and presets locally and advertis
|
|||
|
||||
await bridge.accept(message(`help-${name}`, '/help'));
|
||||
const help = sent.at(-1);
|
||||
for (const command of ['/models', '/model', '/presetlist', '/preset', '/preset --default', '/stop', '/steer']) {
|
||||
for (const command of [
|
||||
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
|
||||
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
|
||||
]) {
|
||||
assert.match(help, new RegExp(`\\${command}`), `${name} ${command}`);
|
||||
}
|
||||
assert.match(help, /\/model 2/, `${name} numbered model selection`);
|
||||
assert.match(help, /\/model .*\[推理等级ID\]/, `${name} optional model reasoning effort`);
|
||||
assert.match(help, /示例:先发 \/models,再发 \/model 2 \[推理等级ID\]/, `${name} model effort placeholder`);
|
||||
assert.doesNotMatch(help, /\/model 2 high\b/, `${name} does not assume a reasoning effort ID`);
|
||||
assert.match(help, /\/preset id:<ID>/, `${name} numeric preset ID selection`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -421,6 +421,11 @@ test('Enterprise WeChat lists models and presets without prompting and advertise
|
|||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
|
||||
await bridge.accept(frame({ msgid: 'reasoning-wecom', text: { content: '/reasoninglist' } }));
|
||||
assert.match(transport.streamed.at(-1).content, /还没有会话/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
|
||||
const presetReplyStart = transport.streamed.length;
|
||||
await bridge.accept(frame({ msgid: 'presets-wecom', text: { content: '/presetlist' } }));
|
||||
const presetReplies = transport.streamed
|
||||
|
|
@ -454,10 +459,15 @@ test('Enterprise WeChat lists models and presets without prompting and advertise
|
|||
|
||||
await bridge.accept(frame({ msgid: 'help-models-wecom', text: { content: '/help' } }));
|
||||
const help = transport.streamed.at(-1).content;
|
||||
for (const command of ['/models', '/model', '/presetlist', '/preset', '/preset --default', '/stop', '/steer']) {
|
||||
for (const command of [
|
||||
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
|
||||
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
|
||||
]) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
assert.match(help, /\/model .*\[推理等级ID\]/);
|
||||
assert.match(help, /示例:先发 \/models,再发 \/model 2 \[推理等级ID\]/);
|
||||
assert.doesNotMatch(help, /\/model 2 high\b/);
|
||||
assert.match(help, /\/preset id:<ID>/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -738,6 +738,12 @@ test('Weixin lists models and presets without prompting and advertises fast comm
|
|||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
||||
await bridge.accept(message('reasoning-weixin', '/reasoninglist'));
|
||||
assert.match(sent.at(-1).text, /还没有会话/);
|
||||
assert.equal(asks, 0);
|
||||
assert.equal(creates, 0);
|
||||
assert.equal(fixture.sessions.size, 0);
|
||||
|
||||
const presetReplyStart = sent.length;
|
||||
await bridge.accept(message('presets-weixin', '/presetlist'));
|
||||
const presetReplies = sent.slice(presetReplyStart).map((entry) => entry.text);
|
||||
|
|
@ -769,10 +775,15 @@ test('Weixin lists models and presets without prompting and advertises fast comm
|
|||
|
||||
await bridge.accept(message('help-models-weixin', '/help'));
|
||||
const help = sent.at(-1).text;
|
||||
for (const command of ['/models', '/model', '/presetlist', '/preset', '/preset --default', '/stop', '/steer']) {
|
||||
for (const command of [
|
||||
'/models', '/model', '/reasoninglist', '/reasonings', '/reasoning',
|
||||
'/presetlist', '/preset', '/preset --default', '/stop', '/steer',
|
||||
]) {
|
||||
assert.equal(help.includes(command), true, command);
|
||||
}
|
||||
assert.match(help, /\/model 2/);
|
||||
assert.match(help, /\/model .*\[推理等级ID\]/);
|
||||
assert.match(help, /示例:先发 \/models,再发 \/model 2 \[推理等级ID\]/);
|
||||
assert.doesNotMatch(help, /\/model 2 high\b/);
|
||||
assert.match(help, /\/preset id:<ID>/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,54 @@ const CATALOG = Object.freeze({
|
|||
failures: [],
|
||||
});
|
||||
|
||||
const REASONING_CATALOG = Object.freeze({
|
||||
groups: [
|
||||
{
|
||||
id: 'deepseek-official',
|
||||
name: 'DeepSeek',
|
||||
models: [
|
||||
{
|
||||
id: 'deepseek-v4-flash',
|
||||
name: 'DeepSeek V4 Flash',
|
||||
reasoning: {
|
||||
efforts: [
|
||||
{ id: 'off', name: 'Off' },
|
||||
{ id: 'high', name: 'High', description: '复杂任务' },
|
||||
{ id: 'max', name: 'Max' },
|
||||
],
|
||||
defaultEffort: 'high',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'deepseek-v4-pro',
|
||||
name: 'DeepSeek V4 Pro',
|
||||
reasoning: {
|
||||
efforts: [
|
||||
{ id: 'medium', name: 'Medium' },
|
||||
{ id: 'xhigh', name: 'XHigh' },
|
||||
],
|
||||
defaultEffort: 'medium',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
CATALOG.groups[1],
|
||||
],
|
||||
failures: [],
|
||||
});
|
||||
|
||||
function reasoningSessionCatalog(current = {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
reasoningEffort: 'high',
|
||||
}) {
|
||||
return {
|
||||
...REASONING_CATALOG,
|
||||
current,
|
||||
routable: true,
|
||||
};
|
||||
}
|
||||
|
||||
function deferred() {
|
||||
let resolve;
|
||||
const promise = new Promise((settle) => { resolve = settle; });
|
||||
|
|
@ -113,13 +161,17 @@ function fixture({
|
|||
return { calls, harness, state, boundId: () => boundId };
|
||||
}
|
||||
|
||||
test('isModelCommand recognizes only /models and /model command prefixes', () => {
|
||||
test('isModelCommand recognizes model and reasoning command prefixes', () => {
|
||||
for (const command of [
|
||||
'/models', ' /MODELS ', '/models ignored', '/model', '/MoDeL openai/gpt-5',
|
||||
'/reasoning', '/REASONING high', '/reasoninglist', '/ReAsOnInGs',
|
||||
]) {
|
||||
assert.equal(isModelCommand(command), true, command);
|
||||
}
|
||||
for (const value of [null, '', 'model', '/modelx', '/modelsx', 'hello /models']) {
|
||||
for (const value of [
|
||||
null, '', 'model', '/modelx', '/modelsx', 'hello /models',
|
||||
'/reasoningx', '/reasoninglists', 'hello /reasoning',
|
||||
]) {
|
||||
assert.equal(isModelCommand(value), false, String(value));
|
||||
}
|
||||
});
|
||||
|
|
@ -209,6 +261,310 @@ test('/model reports current state without creating or selecting', async () => {
|
|||
assert.equal(existingFixture.calls.some(([name]) => name === 'selectModel'), false);
|
||||
});
|
||||
|
||||
test('/reasoninglist and /reasonings are identical read-only aliases', async () => {
|
||||
const first = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
});
|
||||
const second = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
});
|
||||
const listed = await runModelCommand(
|
||||
'/reasoninglist', first.harness, first.state, 'direct:one',
|
||||
);
|
||||
const aliased = await runModelCommand(
|
||||
'/REASONINGS', second.harness, second.state, 'direct:one',
|
||||
);
|
||||
|
||||
assert.equal(aliased.message, listed.message);
|
||||
assert.match(listed.message, /deepseek-official\/deepseek-v4-flash/);
|
||||
assert.match(listed.message, /2\. High \(high\)(当前、默认)/);
|
||||
assert.match(listed.message, /复杂任务/);
|
||||
assert.match(listed.message, /\/reasoning --default/);
|
||||
assert.equal(first.calls.some(([name]) => name === 'selectModel'), false);
|
||||
assert.equal(second.calls.some(([name]) => name === 'selectModel'), false);
|
||||
});
|
||||
|
||||
test('/reasoninglist keeps default reset available without current-model metadata', async () => {
|
||||
for (const sessionCatalog of [
|
||||
{
|
||||
...CATALOG,
|
||||
current: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
routable: true,
|
||||
},
|
||||
{
|
||||
...CATALOG,
|
||||
current: {
|
||||
provider: 'private-provider',
|
||||
model: 'private-model',
|
||||
reasoningEffort: 'custom-high',
|
||||
},
|
||||
routable: true,
|
||||
},
|
||||
]) {
|
||||
const current = fixture({ initialSessionId: 'session-one', sessionCatalog });
|
||||
const listed = await runModelCommand(
|
||||
'/reasoninglist', current.harness, current.state, 'direct:one',
|
||||
);
|
||||
|
||||
assert.match(listed.message, /不提供可切换的推理等级/);
|
||||
assert.match(listed.message, /恢复默认等级:\/reasoning --default/);
|
||||
assert.equal(current.calls.some(([name]) => name === 'selectModel'), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('reasoning read commands require an existing Session and validate syntax', async () => {
|
||||
const missing = fixture();
|
||||
for (const command of ['/reasoning', '/reasoninglist', '/reasonings']) {
|
||||
const result = await runModelCommand(command, missing.harness, missing.state, 'direct:one');
|
||||
assert.match(result.message, /还没有会话/, command);
|
||||
}
|
||||
assert.equal(missing.calls.some(([name]) => name === 'createSession'), false);
|
||||
|
||||
const existing = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
});
|
||||
assert.match(
|
||||
(await runModelCommand(
|
||||
'/reasoninglist extra', existing.harness, existing.state, 'direct:one',
|
||||
)).message,
|
||||
/不带参数/,
|
||||
);
|
||||
assert.match(
|
||||
(await runModelCommand(
|
||||
'/reasoning high extra', existing.harness, existing.state, 'direct:one',
|
||||
)).message,
|
||||
/用法/,
|
||||
);
|
||||
});
|
||||
|
||||
test('/reasoning reports the explicit or model-default effort without selecting', async () => {
|
||||
const explicit = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
});
|
||||
const explicitResult = await runModelCommand(
|
||||
'/reasoning', explicit.harness, explicit.state, 'direct:one',
|
||||
);
|
||||
assert.match(explicitResult.message, /当前推理等级:High \(high\)/);
|
||||
|
||||
const inherited = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog({
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
}),
|
||||
});
|
||||
const inheritedResult = await runModelCommand(
|
||||
'/reasoning', inherited.harness, inherited.state, 'direct:one',
|
||||
);
|
||||
assert.match(inheritedResult.message, /当前推理等级:High \(high\)/);
|
||||
assert.equal(explicit.calls.some(([name]) => name === 'selectModel'), false);
|
||||
assert.equal(inherited.calls.some(([name]) => name === 'selectModel'), false);
|
||||
});
|
||||
|
||||
test('/reasoning preserves an unknown current effort and represents Provider Default', async () => {
|
||||
const groups = structuredClone(REASONING_CATALOG.groups);
|
||||
delete groups[0].models[0].reasoning.defaultEffort;
|
||||
const raw = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: {
|
||||
groups,
|
||||
failures: [],
|
||||
current: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
reasoningEffort: 'gateway-ultra',
|
||||
},
|
||||
routable: true,
|
||||
},
|
||||
});
|
||||
const listed = await runModelCommand(
|
||||
'/reasoninglist', raw.harness, raw.state, 'direct:one',
|
||||
);
|
||||
assert.match(listed.message, /当前推理等级:gateway-ultra/);
|
||||
assert.doesNotMatch(listed.message, /(当前/);
|
||||
|
||||
const providerDefault = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: {
|
||||
groups,
|
||||
failures: [],
|
||||
current: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
},
|
||||
routable: true,
|
||||
},
|
||||
});
|
||||
const current = await runModelCommand(
|
||||
'/reasoning', providerDefault.harness, providerDefault.state, 'direct:one',
|
||||
);
|
||||
assert.match(current.message, /Default(由模型或 Provider 决定)/);
|
||||
});
|
||||
|
||||
test('/reasoning switches the current model effort by index or exact ID', async () => {
|
||||
for (const [requested, expected] of [['3', 'max'], ['off', 'off']]) {
|
||||
const { calls, harness, state } = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
});
|
||||
const result = await runModelCommand(
|
||||
`/reasoning ${requested}`, harness, state, 'direct:one',
|
||||
);
|
||||
|
||||
assert.match(result.message, new RegExp(`推理等级已切换为:[\\s\\S]*${expected}`));
|
||||
assert.deepEqual(calls.find(([name]) => name === 'selectModel'), [
|
||||
'selectModel',
|
||||
'session-one',
|
||||
{
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
reasoningEffort: expected,
|
||||
},
|
||||
{},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('/reasoning prefers an exact opaque numeric effort ID before an index', async () => {
|
||||
const groups = structuredClone(REASONING_CATALOG.groups);
|
||||
groups[0].models[0].reasoning = {
|
||||
efforts: [
|
||||
{ id: '3', name: 'Literal Three' },
|
||||
{ id: '0', name: 'Literal Zero' },
|
||||
{ id: '9007199254740992', name: 'Huge Numeric' },
|
||||
],
|
||||
defaultEffort: '3',
|
||||
};
|
||||
|
||||
for (const requested of ['3', '0', '9007199254740992']) {
|
||||
const { calls, harness, state } = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: {
|
||||
groups,
|
||||
failures: [],
|
||||
current: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
reasoningEffort: '3',
|
||||
},
|
||||
routable: true,
|
||||
},
|
||||
});
|
||||
const result = await runModelCommand(
|
||||
`/reasoning ${requested}`, harness, state, 'direct:one',
|
||||
);
|
||||
|
||||
assert.match(result.message, /推理等级已切换为/);
|
||||
assert.equal(
|
||||
calls.find(([name]) => name === 'selectModel')?.[2]?.reasoningEffort,
|
||||
requested,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('/reasoning --default restores the current model default effort', async () => {
|
||||
const { calls, harness, state } = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog({
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
reasoningEffort: 'max',
|
||||
}),
|
||||
selectionResult: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
selectionReadback: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
});
|
||||
const result = await runModelCommand(
|
||||
'/reasoning --default', harness, state, 'direct:one',
|
||||
);
|
||||
|
||||
assert.match(result.message, /推理等级已切换为:[\s\S]*High \(high\)/);
|
||||
assert.deepEqual(calls.find(([name]) => name === 'selectModel')?.[2], {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
});
|
||||
});
|
||||
|
||||
test('/reasoning --default can reset an advisory-unlisted current model', async () => {
|
||||
const sessionCatalog = {
|
||||
...CATALOG,
|
||||
current: {
|
||||
provider: 'private-provider',
|
||||
model: 'private-model',
|
||||
reasoningEffort: 'custom-high',
|
||||
},
|
||||
routable: true,
|
||||
};
|
||||
const { calls, harness, state } = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog,
|
||||
});
|
||||
const result = await runModelCommand(
|
||||
'/reasoning --default', harness, state, 'direct:one',
|
||||
);
|
||||
|
||||
assert.match(result.message, /推理等级已切换为/);
|
||||
assert.match(result.message, /Default/);
|
||||
assert.deepEqual(calls.find(([name]) => name === 'selectModel')?.[2], {
|
||||
provider: 'private-provider',
|
||||
model: 'private-model',
|
||||
});
|
||||
});
|
||||
|
||||
test('/reasoning rejects unsupported levels without changing the selection', async () => {
|
||||
for (const requested of ['0', '4', '9007199254740992', 'medium']) {
|
||||
const { calls, harness, state } = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
});
|
||||
const result = await runModelCommand(
|
||||
`/reasoning ${requested}`, harness, state, 'direct:one',
|
||||
);
|
||||
assert.match(result.message, /序号无效|不支持推理等级/, requested);
|
||||
assert.equal(calls.some(([name]) => name === 'selectModel'), false, requested);
|
||||
}
|
||||
|
||||
const unsupported = fixture({ initialSessionId: 'session-one' });
|
||||
const result = await runModelCommand(
|
||||
'/reasoning high', unsupported.harness, unsupported.state, 'direct:one',
|
||||
);
|
||||
assert.match(result.message, /不提供可切换的推理等级/);
|
||||
assert.equal(unsupported.calls.some(([name]) => name === 'selectModel'), false);
|
||||
|
||||
const noSession = fixture({ globalCatalog: REASONING_CATALOG });
|
||||
const noSessionResult = await runModelCommand(
|
||||
'/reasoning high', noSession.harness, noSession.state, 'direct:one',
|
||||
);
|
||||
assert.match(noSessionResult.message, /还没有会话/);
|
||||
assert.equal(noSession.calls.some(([name]) => name === 'createSession'), false);
|
||||
});
|
||||
|
||||
test('reasoning commands reject image-bearing control messages', async () => {
|
||||
const current = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
});
|
||||
for (const command of ['/reasoning', '/reasoninglist', '/reasonings', '/reasoning high']) {
|
||||
const result = await runModelCommand(
|
||||
command, current.harness, current.state, 'direct:one', { hasImages: true },
|
||||
);
|
||||
assert.match(result.message, /仅支持纯文字/, command);
|
||||
}
|
||||
assert.equal(current.calls.some(([name]) => name === 'models'), false);
|
||||
assert.equal(current.calls.some(([name]) => name === 'selectModel'), false);
|
||||
});
|
||||
|
||||
test('/model uses an exact catalog ID and preserves slashes inside the model ID', async () => {
|
||||
const { calls, harness, state } = fixture({ initialSessionId: 'session-one' });
|
||||
const control = Object.freeze({ route: 'direct:one' });
|
||||
|
|
@ -242,6 +598,71 @@ test('/model accepts the current catalog\'s global 1-based model number', async
|
|||
]);
|
||||
});
|
||||
|
||||
test('/model accepts an optional effort and otherwise applies the target model default', async () => {
|
||||
for (const [command, expectedEffort] of [
|
||||
['/model 2 xhigh', 'xhigh'],
|
||||
['/model deepseek-official/deepseek-v4-pro', 'medium'],
|
||||
]) {
|
||||
const resolvedDefault = command.includes(' xhigh') ? undefined : {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-pro',
|
||||
reasoningEffort: 'medium',
|
||||
};
|
||||
const { calls, harness, state } = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
globalCatalog: REASONING_CATALOG,
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
selectionResult: resolvedDefault,
|
||||
selectionReadback: resolvedDefault,
|
||||
});
|
||||
const result = await runModelCommand(command, harness, state, 'direct:one');
|
||||
|
||||
assert.match(result.message, /deepseek-official\/deepseek-v4-pro/);
|
||||
assert.match(result.message, new RegExp(`推理等级:.*${expectedEffort}`));
|
||||
assert.deepEqual(calls.find(([name]) => name === 'selectModel')?.[2], {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-pro',
|
||||
...(command.includes(' xhigh') ? { reasoningEffort: expectedEffort } : {}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('/model validates an optional effort against the target model atomically', async () => {
|
||||
const invalid = fixture({ globalCatalog: REASONING_CATALOG });
|
||||
const failed = await runModelCommand(
|
||||
'/model 2 high', invalid.harness, invalid.state, 'direct:one',
|
||||
);
|
||||
|
||||
assert.match(failed.message, /不支持推理等级:high/);
|
||||
assert.match(failed.message, /medium, xhigh/);
|
||||
assert.equal(invalid.calls.some(([name]) => name === 'createSession'), false);
|
||||
assert.equal(invalid.calls.some(([name]) => name === 'selectModel'), false);
|
||||
|
||||
const valid = fixture({ globalCatalog: REASONING_CATALOG });
|
||||
const switched = await runModelCommand(
|
||||
'/model 2 xhigh', valid.harness, valid.state, 'direct:one',
|
||||
);
|
||||
assert.match(switched.message, /模型已切换为/);
|
||||
assert.equal(valid.boundId(), 'session-created');
|
||||
assert.deepEqual(valid.calls.find(([name]) => name === 'selectModel')?.[2], {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-pro',
|
||||
reasoningEffort: 'xhigh',
|
||||
});
|
||||
});
|
||||
|
||||
test('/model reports the current reasoning effort when the Session exposes it', async () => {
|
||||
const current = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
});
|
||||
const result = await runModelCommand('/model', current.harness, current.state, 'direct:one');
|
||||
|
||||
assert.match(result.message, /当前推理等级:High \(high\)/);
|
||||
assert.match(result.message, /\/reasoninglist/);
|
||||
assert.equal(current.calls.some(([name]) => name === 'selectModel'), false);
|
||||
});
|
||||
|
||||
test('/model rejects invalid model numbers without creating or selecting a Session', async () => {
|
||||
for (const requested of ['0', '4', '9007199254740992']) {
|
||||
const { calls, harness, state } = fixture();
|
||||
|
|
@ -469,6 +890,78 @@ test('/model refuses pending interactions and active or running Sessions', async
|
|||
}
|
||||
});
|
||||
|
||||
test('/reasoning mutations refuse pending or busy Sessions while reads remain available', async () => {
|
||||
const pending = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
});
|
||||
const pendingResult = await runModelCommand(
|
||||
'/reasoning off', pending.harness, pending.state, 'direct:one',
|
||||
{ pendingInteraction: true },
|
||||
);
|
||||
assert.match(pendingResult.message, /等待你的回答或审批/);
|
||||
assert.equal(pending.calls.some(([name]) => name === 'selectModel'), false);
|
||||
|
||||
for (const runState of [{ running: true }, { activeTurn: true }]) {
|
||||
const active = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
...runState,
|
||||
});
|
||||
const failed = await runModelCommand(
|
||||
'/reasoning off', active.harness, active.state, 'direct:one',
|
||||
{ control: 'owner-one' },
|
||||
);
|
||||
assert.match(failed.message, /当前任务正在运行/);
|
||||
assert.equal(active.calls.some(([name]) => name === 'selectModel'), false);
|
||||
|
||||
const listed = await runModelCommand(
|
||||
'/reasoninglist', active.harness, active.state, 'direct:one',
|
||||
);
|
||||
assert.match(listed.message, /可用推理等级/);
|
||||
}
|
||||
});
|
||||
|
||||
test('/reasoning verifies both the selected effort and authoritative readback', async () => {
|
||||
const wrongSelection = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
selectionResult: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
});
|
||||
const selectionFailure = await runModelCommand(
|
||||
'/reasoning max',
|
||||
wrongSelection.harness,
|
||||
wrongSelection.state,
|
||||
'direct:one',
|
||||
);
|
||||
assert.match(selectionFailure.message, /推理等级切换失败/);
|
||||
assert.match(selectionFailure.message, /reasoningEffort=max/);
|
||||
assert.match(selectionFailure.message, /reasoningEffort=high/);
|
||||
|
||||
const wrongReadback = fixture({
|
||||
initialSessionId: 'session-one',
|
||||
sessionCatalog: reasoningSessionCatalog(),
|
||||
selectionReadback: {
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-v4-flash',
|
||||
reasoningEffort: 'off',
|
||||
},
|
||||
});
|
||||
const readbackFailure = await runModelCommand(
|
||||
'/reasoning max',
|
||||
wrongReadback.harness,
|
||||
wrongReadback.state,
|
||||
'direct:one',
|
||||
);
|
||||
assert.match(readbackFailure.message, /推理等级切换失败/);
|
||||
assert.match(readbackFailure.message, /reasoningEffort=max/);
|
||||
assert.match(readbackFailure.message, /reasoningEffort=off/);
|
||||
});
|
||||
|
||||
test('a missing bound Session is cleared and /models falls back to the global catalog', async () => {
|
||||
const { calls, harness, state, boundId } = fixture({
|
||||
initialSessionId: 'session-missing',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue