- DeepSeek/MiMo/Agnes 移除 supportsThinking 元信息硬门控:思考参数完全遵循用户配置 (事故复盘中 vision-exp 元信息标注不支持思考、实际产生了 8189 token 推理内容, 元信息不可靠;预算耗尽由引擎降级重试兜底,元信息不符仅告警不拦截) - Ollama 保留 /api/show 能力探测门控(服务端硬协议约束:向不支持思考的模型发 think 每次请求 400,属协议正确性而非意图覆盖),探测失败 fail-open - LLM 设置提示文案修订:元信息不符仍按用户配置发送,降级重试自动兜底 - 测试契约反向钉住:vision-exp + 用户开启→照发 enabled+reasoning_effort; 关闭/未配置→显式 disabled;Ollama 探测 false→不发 think / null→fail-open - 补录 docs/v0.8.0-迭代实施清单.md(含逐项验证记录与本次修订记录; 首次提交时该文件因故未入库,本次补齐) - 验证:typecheck 0 错误 / lint 0 问题 / 系统 Node 2146 通过 / thinking 矩阵 101 用例全绿
190 lines
7.1 KiB
TypeScript
190 lines
7.1 KiB
TypeScript
/**
|
||
* v0.8.0 P0-3(修订版): 思考参数**用户意图优先**契约。
|
||
*
|
||
* 修订原因:初版按 MODEL_INFO.supportsThinking 元信息硬门控,但事故复盘证明
|
||
* 元信息不可靠 —— deepseek-v4-flash-vision-exp 标注"不支持思考"、实际却产生了
|
||
* 8189 token 推理内容。既然引擎已有完整兜底链(最大输出上限配置 → 空响应守卫
|
||
* → 降级重试 → OUTPUT_LENGTH_EXCEEDED 明确报错),是否开思考应由**用户决定**,
|
||
* 适配器层只负责:①如实透传用户配置;②元信息不符时告警不拦截;③预算过小告警。
|
||
*
|
||
* 保留的唯一门控是 Ollama 的 /api/show capabilities 探测 —— 那是服务端实时
|
||
* 真值且为硬协议约束(向无思考能力的模型发 think 每次请求 400),属协议
|
||
* 正确性而非用户意图覆盖。
|
||
*/
|
||
|
||
import { describe, it, expect } from 'vitest';
|
||
import { DeepSeekAdapter } from '../deepseek.adapter';
|
||
import { AgnesAdapter } from '../agnes-ai.adapter';
|
||
import { MimoAdapter } from '../mimo.adapter';
|
||
import { OllamaAdapter } from '../ollama.adapter';
|
||
import type { MetonaRequest } from '../../types';
|
||
|
||
const SYSTEM_PROMPT = {
|
||
roleDefinition: 'test',
|
||
outputConstraints: '',
|
||
safetyGuidelines: '',
|
||
};
|
||
|
||
function makeRequest(overrides?: Partial<MetonaRequest['params']>): MetonaRequest {
|
||
return {
|
||
meta: {
|
||
sessionId: 's',
|
||
iteration: 1,
|
||
requestId: 'r',
|
||
timestamp: Date.now(),
|
||
agentVersion: '1.0.0',
|
||
},
|
||
systemPrompt: SYSTEM_PROMPT,
|
||
messages: [{ role: 'user', content: 'hi', timestamp: Date.now() }],
|
||
params: {
|
||
maxTokens: 63488,
|
||
temperature: 0,
|
||
stream: true,
|
||
thinkingEnabled: true,
|
||
thinkingEffort: 'max',
|
||
...overrides,
|
||
},
|
||
};
|
||
}
|
||
|
||
function asNative(
|
||
adapter: unknown,
|
||
): (req: MetonaRequest, stream: boolean) => Record<string, unknown> {
|
||
return (
|
||
adapter as { toNativeRequest: (r: MetonaRequest, s: boolean) => Record<string, unknown> }
|
||
).toNativeRequest.bind(adapter);
|
||
}
|
||
|
||
describe('P0-3 修订: 用户思考意图优先于模型元信息', () => {
|
||
it('DeepSeek: vision-exp(元信息 false)+ 用户开启思考 → 照发 enabled + reasoning_effort,max_tokens 仍按模型钳制 8192', async () => {
|
||
const adapter = new DeepSeekAdapter({
|
||
provider: 'deepseek',
|
||
baseURL: 'https://api.deepseek.com',
|
||
apiKey: 'k',
|
||
defaultModel: 'deepseek-v4-flash-vision-exp',
|
||
});
|
||
const body = asNative(adapter)(makeRequest(), false);
|
||
expect(body.thinking).toEqual({ type: 'enabled' });
|
||
expect(body.reasoning_effort).toBe('max');
|
||
expect(body.max_tokens).toBe(8192);
|
||
});
|
||
|
||
it('DeepSeek: 用户关闭思考 → 显式 disabled', async () => {
|
||
const adapter = new DeepSeekAdapter({
|
||
provider: 'deepseek',
|
||
baseURL: 'https://api.deepseek.com',
|
||
apiKey: 'k',
|
||
defaultModel: 'deepseek-v4-flash-vision-exp',
|
||
});
|
||
const body = asNative(adapter)(
|
||
makeRequest({ thinkingEnabled: false, thinkingEffort: undefined }),
|
||
false,
|
||
);
|
||
expect(body.thinking).toEqual({ type: 'disabled' });
|
||
expect(body.reasoning_effort).toBeUndefined();
|
||
});
|
||
|
||
it('DeepSeek: 未配置 → 显式 disabled(确定性契约,不依赖服务端隐式默认)', async () => {
|
||
const adapter = new DeepSeekAdapter({
|
||
provider: 'deepseek',
|
||
baseURL: 'https://api.deepseek.com',
|
||
apiKey: 'k',
|
||
defaultModel: 'deepseek-v4-pro',
|
||
});
|
||
const body = asNative(adapter)(makeRequest({ thinkingEnabled: undefined }), false);
|
||
expect(body.thinking).toEqual({ type: 'disabled' });
|
||
});
|
||
|
||
it('Agnes: 元信息 false + 用户开启思考 → enable_thinking:true(元信息不拦截)', async () => {
|
||
const adapter = new AgnesAdapter({
|
||
provider: 'agnes',
|
||
baseURL: 'https://apihub.agnes-ai.com/v1',
|
||
apiKey: 'k',
|
||
defaultModel: 'agnes-2.0-flash',
|
||
});
|
||
const table = AgnesAdapter['MODEL_INFO'] as Record<string, { supportsThinking: boolean }>;
|
||
const original = table['agnes-2.0-flash'].supportsThinking;
|
||
table['agnes-2.0-flash'].supportsThinking = false;
|
||
try {
|
||
const body = asNative(adapter)(makeRequest(), false);
|
||
expect(body.chat_template_kwargs).toEqual({ enable_thinking: true });
|
||
} finally {
|
||
table['agnes-2.0-flash'].supportsThinking = original;
|
||
}
|
||
});
|
||
|
||
it('Agnes: 用户关闭思考 → enable_thinking:false(对称契约保持)', async () => {
|
||
const adapter = new AgnesAdapter({
|
||
provider: 'agnes',
|
||
baseURL: 'https://apihub.agnes-ai.com/v1',
|
||
apiKey: 'k',
|
||
defaultModel: 'agnes-2.0-flash',
|
||
});
|
||
const body = asNative(adapter)(makeRequest({ thinkingEnabled: false }), false);
|
||
expect(body.chat_template_kwargs).toEqual({ enable_thinking: false });
|
||
});
|
||
|
||
it('MiMo: 元信息 false + 用户开启思考 → thinking enabled(temperature 不传)', async () => {
|
||
const adapter = new MimoAdapter({
|
||
provider: 'mimo',
|
||
baseURL: 'https://api.xiaomimimo.com/v1',
|
||
apiKey: 'k',
|
||
defaultModel: 'mimo-v2.5-pro',
|
||
});
|
||
const table = MimoAdapter['MODEL_INFO'] as Record<string, { supportsThinking: boolean }>;
|
||
const original = table['mimo-v2.5-pro'].supportsThinking;
|
||
table['mimo-v2.5-pro'].supportsThinking = false;
|
||
try {
|
||
const body = asNative(adapter)(makeRequest(), false);
|
||
expect(body.thinking).toEqual({ type: 'enabled' });
|
||
expect(body.temperature).toBeUndefined();
|
||
} finally {
|
||
table['mimo-v2.5-pro'].supportsThinking = original;
|
||
}
|
||
});
|
||
|
||
it('MiMo: 用户关闭思考 → disabled + temperature/top_p 透传', async () => {
|
||
const adapter = new MimoAdapter({
|
||
provider: 'mimo',
|
||
baseURL: 'https://api.xiaomimimo.com/v1',
|
||
apiKey: 'k',
|
||
defaultModel: 'mimo-v2.5-pro',
|
||
});
|
||
const body = asNative(adapter)(makeRequest({ thinkingEnabled: false }), false);
|
||
expect(body.thinking).toEqual({ type: 'disabled' });
|
||
expect(body.temperature).toBe(0);
|
||
});
|
||
|
||
it('Ollama: 探测不支持思考(服务端硬约束)→ 不发 think 参数(唯一保留的门控)', async () => {
|
||
const adapter = new OllamaAdapter({
|
||
provider: 'ollama',
|
||
baseURL: 'http://localhost:11434',
|
||
apiKey: '',
|
||
defaultModel: 'qwen3:latest',
|
||
});
|
||
(adapter as unknown as { cachedThinkingSupport: boolean | null }).cachedThinkingSupport = false;
|
||
const body = await (
|
||
adapter as unknown as {
|
||
toNativeRequest: (r: MetonaRequest) => Promise<Record<string, unknown>>;
|
||
}
|
||
).toNativeRequest(makeRequest());
|
||
expect(body.think).toBeUndefined();
|
||
});
|
||
|
||
it('Ollama: 探测未知(null)fail-open → think 参数照发', async () => {
|
||
const adapter = new OllamaAdapter({
|
||
provider: 'ollama',
|
||
baseURL: 'http://localhost:11434',
|
||
apiKey: '',
|
||
defaultModel: 'qwen3:latest',
|
||
});
|
||
(adapter as unknown as { cachedThinkingSupport: boolean | null }).cachedThinkingSupport = null;
|
||
const body = await (
|
||
adapter as unknown as {
|
||
toNativeRequest: (r: MetonaRequest) => Promise<Record<string, unknown>>;
|
||
}
|
||
).toNativeRequest(makeRequest());
|
||
expect(body.think).toBe(true);
|
||
});
|
||
});
|