fix: v0.8.0 修订 — 思考用户意图优先 · 移除元信息硬门控 · 实施清单入库
- 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 用例全绿
This commit is contained in:
@@ -238,10 +238,11 @@ describe('DeepSeek vision 模型多模态请求格式(v0.5.4)', () => {
|
||||
} as MetonaRequest);
|
||||
|
||||
const body = requestBody();
|
||||
// v0.8.0 P0-3 能力门控: vision 模型 supportsThinking=false → 显式 disabled,
|
||||
// 且不发送 reasoning_effort(思考会耗尽该模型 8192 输出预算 —— 生产事故根因)
|
||||
expect(body.thinking).toEqual({ type: 'disabled' });
|
||||
expect(body.reasoning_effort).toBeUndefined();
|
||||
// v0.8.0 修订(用户意图优先): 元信息 supportsThinking=false 不再拦截 ——
|
||||
// 用户开启思考则照发 enabled + reasoning_effort(事故复盘中该模型实际
|
||||
// 产生了推理内容,元信息不可靠;预算耗尽由引擎降级重试兜底)
|
||||
expect(body.thinking).toEqual({ type: 'enabled' });
|
||||
expect(body.reasoning_effort).toBe('high');
|
||||
});
|
||||
|
||||
it('vision 模型 messages 数组首位始终为 system 消息', async () => {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
/**
|
||||
* v0.8.0 P0-3: 思考参数 × 模型能力 门控矩阵。
|
||||
* v0.8.0 P0-3(修订版): 思考参数**用户意图优先**契约。
|
||||
*
|
||||
* 根因回顾:MODEL_INFO 标注 supportsThinking:false 的模型(如
|
||||
* deepseek-v4-flash-vision-exp)此前仍被发送 thinking 参数 —— 思考耗尽输出
|
||||
* 预算(8192 上限)导致 finish_reason=length 空回复、会话静默停止。
|
||||
* 本文件钉住四家 Provider 的能力门控行为与输出预算告警前置条件。
|
||||
* 修订原因:初版按 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';
|
||||
@@ -50,8 +55,8 @@ function asNative(
|
||||
).toNativeRequest.bind(adapter);
|
||||
}
|
||||
|
||||
describe('P0-3 thinking capability gate', () => {
|
||||
it('DeepSeek: vision-exp (supportsThinking:false) → thinking disabled, no reasoning_effort, max_tokens clamped to 8192', async () => {
|
||||
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',
|
||||
@@ -59,54 +64,67 @@ describe('P0-3 thinking capability gate', () => {
|
||||
defaultModel: 'deepseek-v4-flash-vision-exp',
|
||||
});
|
||||
const body = asNative(adapter)(makeRequest(), false);
|
||||
expect(body.thinking).toEqual({ type: 'disabled' });
|
||||
expect(body.reasoning_effort).toBeUndefined();
|
||||
expect(body.thinking).toEqual({ type: 'enabled' });
|
||||
expect(body.reasoning_effort).toBe('max');
|
||||
expect(body.max_tokens).toBe(8192);
|
||||
});
|
||||
|
||||
it('DeepSeek: pro (supportsThinking:true) → thinking enabled + reasoning_effort mapped (max→max)', async () => {
|
||||
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(), false);
|
||||
expect(body.thinking).toEqual({ type: 'enabled' });
|
||||
expect(body.reasoning_effort).toBe('max');
|
||||
const body = asNative(adapter)(makeRequest({ thinkingEnabled: undefined }), false);
|
||||
expect(body.thinking).toEqual({ type: 'disabled' });
|
||||
});
|
||||
|
||||
it('Agnes: supportsThinking:false model → enable_thinking:false', async () => {
|
||||
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: false });
|
||||
expect(body.chat_template_kwargs).toEqual({ enable_thinking: true });
|
||||
} finally {
|
||||
table['agnes-2.0-flash'].supportsThinking = original;
|
||||
}
|
||||
});
|
||||
|
||||
it('Agnes: supportsThinking:true model → enable_thinking:true', async () => {
|
||||
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(), false);
|
||||
expect(body.chat_template_kwargs).toEqual({ enable_thinking: true });
|
||||
const body = asNative(adapter)(makeRequest({ thinkingEnabled: false }), false);
|
||||
expect(body.chat_template_kwargs).toEqual({ enable_thinking: false });
|
||||
});
|
||||
|
||||
it('MiMo: supportsThinking:false model → thinking disabled + temperature passthrough', async () => {
|
||||
it('MiMo: 元信息 false + 用户开启思考 → thinking enabled(temperature 不传)', async () => {
|
||||
const adapter = new MimoAdapter({
|
||||
provider: 'mimo',
|
||||
baseURL: 'https://api.xiaomimimo.com/v1',
|
||||
@@ -118,14 +136,26 @@ describe('P0-3 thinking capability gate', () => {
|
||||
table['mimo-v2.5-pro'].supportsThinking = false;
|
||||
try {
|
||||
const body = asNative(adapter)(makeRequest(), false);
|
||||
expect(body.thinking).toEqual({ type: 'disabled' });
|
||||
expect(body.temperature).toBe(0);
|
||||
expect(body.thinking).toEqual({ type: 'enabled' });
|
||||
expect(body.temperature).toBeUndefined();
|
||||
} finally {
|
||||
table['mimo-v2.5-pro'].supportsThinking = original;
|
||||
}
|
||||
});
|
||||
|
||||
it('Ollama: probed no-thinking (cachedThinkingSupport=false) → no think parameter', async () => {
|
||||
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',
|
||||
@@ -141,7 +171,7 @@ describe('P0-3 thinking capability gate', () => {
|
||||
expect(body.think).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Ollama: probe unknown (null) fails open → think parameter present', async () => {
|
||||
it('Ollama: 探测未知(null)fail-open → think 参数照发', async () => {
|
||||
const adapter = new OllamaAdapter({
|
||||
provider: 'ollama',
|
||||
baseURL: 'http://localhost:11434',
|
||||
@@ -154,6 +184,6 @@ describe('P0-3 thinking capability gate', () => {
|
||||
toNativeRequest: (r: MetonaRequest) => Promise<Record<string, unknown>>;
|
||||
}
|
||||
).toNativeRequest(makeRequest());
|
||||
expect(body.think).toBe(true); // effort=max → true
|
||||
expect(body.think).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,19 +99,22 @@ export class AgnesAdapter extends OpenAICompatibleAdapter {
|
||||
// 显式发送 enable_thinking:false —— 原实现只在 thinkingEnabled===true 时写该字段,
|
||||
// 若服务端默认开启思考,客户端没有任何路径把它关掉(DeepSeek/MiMo 均显式发送
|
||||
// disabled 保持对称,唯独此处漏了)。
|
||||
// v0.8.0 P0-3: 模型能力门控 —— supportsThinking===false 的模型强制关闭思考
|
||||
// v0.8.0 修订(用户意图优先): enable_thinking 完全遵循用户配置,不再按
|
||||
// supportsThinking 元信息硬门控 —— 预算耗尽风险由引擎空响应守卫的降级
|
||||
// 重试链路兜底;元信息与配置不符时仅告警不拦截。
|
||||
{
|
||||
const effort = request.params.thinkingEffort ?? 'high';
|
||||
const modelThinkingSupported =
|
||||
AgnesAdapter.MODEL_INFO[this.config.defaultModel]?.supportsThinking !== false;
|
||||
// 未配置 thinkingEnabled 一律显式关闭 —— 与 DeepSeek/MiMo 的"服务端默认开启,
|
||||
// 必须显式发送 disabled"口径对齐,让行为确定性不依赖服务端隐式默认。
|
||||
const wantThinking =
|
||||
request.params.thinkingEnabled === true && effort !== 'low' && modelThinkingSupported;
|
||||
const wantThinking = request.params.thinkingEnabled === true && effort !== 'low';
|
||||
body.chat_template_kwargs = { enable_thinking: wantThinking };
|
||||
if (request.params.thinkingEnabled === true && !modelThinkingSupported) {
|
||||
if (
|
||||
request.params.thinkingEnabled === true &&
|
||||
wantThinking &&
|
||||
AgnesAdapter.MODEL_INFO[this.config.defaultModel]?.supportsThinking === false
|
||||
) {
|
||||
log.warn(
|
||||
`[Agnes] model "${this.config.defaultModel}" does not support thinking — sending enable_thinking:false (P0-3 capability gate)`,
|
||||
`[Agnes] model "${this.config.defaultModel}" metadata says thinking unsupported — sending enable_thinking per user config (degraded retry handles budget exhaustion)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,13 +203,12 @@ export class DeepSeekAdapter extends OpenAICompatibleAdapter {
|
||||
}
|
||||
|
||||
// Thinking 模式
|
||||
// v0.8.0 P0-3 根治: 模型能力门控 —— MODEL_INFO.supportsThinking === false 的
|
||||
// 模型(如 deepseek-v4-flash-vision-exp)一律不发思考参数并显式 disabled,
|
||||
// 防止思考消耗输出预算(生产事故:vision-exp 8192 输出预算被 max 档思考
|
||||
// 全部烧光 → finish_reason=length 空回复 → 会话静默停止)。
|
||||
const modelThinkingSupported =
|
||||
DeepSeekAdapter.MODEL_INFO[this.config.defaultModel]?.supportsThinking !== false;
|
||||
const wantThinking = request.params.thinkingEnabled === true && modelThinkingSupported;
|
||||
// v0.8.0 修订(用户意图优先): 思考参数完全遵循用户配置,不再按
|
||||
// supportsThinking 元信息硬门控 —— 事故复盘中 vision-exp 元信息标注
|
||||
// "不支持思考"、实际却产生了推理内容,元信息不可靠。预算耗尽风险由
|
||||
// 引擎空响应守卫的降级重试链路兜底(关闭思考重试一次 → 仍失败则
|
||||
// OUTPUT_LENGTH_EXCEEDED 明确报错)。元信息与配置不符时仅告警不拦截。
|
||||
const wantThinking = request.params.thinkingEnabled === true;
|
||||
// API 默认 thinking.type = "enabled",必须显式发送 disabled 才能关闭
|
||||
if (!wantThinking) {
|
||||
body.thinking = { type: 'disabled' };
|
||||
@@ -223,6 +222,12 @@ export class DeepSeekAdapter extends OpenAICompatibleAdapter {
|
||||
};
|
||||
// DeepSeek API 仅支持 high / max 两档,low/medium 映射为 high
|
||||
body.reasoning_effort = effortMap[request.params.thinkingEffort ?? 'high'] ?? 'high';
|
||||
// 元信息标注不支持思考但用户开启 —— 告知降级兜底路径(不拦截)
|
||||
if (DeepSeekAdapter.MODEL_INFO[this.config.defaultModel]?.supportsThinking === false) {
|
||||
log.warn(
|
||||
`[DeepSeek] model "${this.config.defaultModel}" metadata says thinking unsupported — sending thinking params per user config (degraded retry handles budget exhaustion)`,
|
||||
);
|
||||
}
|
||||
// v0.8.0 P0-3: 思考会占用输出预算 —— 钳制后预算过小时显式告警
|
||||
//(思考 token 计入 max_tokens,预算过小会出现"思考耗尽正文为零"截断)
|
||||
if (maxTokens < 8192) {
|
||||
|
||||
@@ -126,11 +126,10 @@ export class MimoAdapter extends OpenAICompatibleAdapter {
|
||||
|
||||
// Thinking 模式(与 DeepSeek 参数结构一致)
|
||||
// MiMo API 默认 thinking.type = "enabled",必须显式发送 disabled 才能关闭
|
||||
// v0.8.0 P0-3: 模型能力门控 —— supportsThinking===false 的模型强制 disabled
|
||||
//(思考 token 计入 max_completion_tokens,能力不符的模型上会耗尽输出预算)
|
||||
const modelThinkingSupported =
|
||||
MimoAdapter.MODEL_INFO[this.config.defaultModel]?.supportsThinking !== false;
|
||||
const wantThinking = request.params.thinkingEnabled !== false && modelThinkingSupported;
|
||||
// v0.8.0 修订(用户意图优先): 思考参数完全遵循用户配置,不再按
|
||||
// supportsThinking 元信息硬门控 —— 预算耗尽风险由引擎空响应守卫的
|
||||
// 降级重试链路兜底;元信息与配置不符时仅告警不拦截。
|
||||
const wantThinking = request.params.thinkingEnabled !== false;
|
||||
if (!wantThinking) {
|
||||
// 显式禁用思考:传 disabled + temperature/top_p(非思考模式下这两个参数有效)
|
||||
body.thinking = { type: 'disabled' };
|
||||
@@ -140,6 +139,12 @@ export class MimoAdapter extends OpenAICompatibleAdapter {
|
||||
// 启用思考(包括 undefined,因为 MiMo 默认 enabled)
|
||||
// 思考模式下 temperature/top_p 被 API 强制覆盖为 1.0/0.95,不传
|
||||
body.thinking = { type: 'enabled' };
|
||||
// 元信息标注不支持思考但用户开启 —— 告知降级兜底路径(不拦截)
|
||||
if (MimoAdapter.MODEL_INFO[this.config.defaultModel]?.supportsThinking === false) {
|
||||
log.warn(
|
||||
`[MiMo] model "${this.config.defaultModel}" metadata says thinking unsupported — sending thinking params per user config (degraded retry handles budget exhaustion)`,
|
||||
);
|
||||
}
|
||||
// v0.8.0 P0-3: 思考占用输出预算 —— 钳制后预算过小时显式告警
|
||||
const effectiveMax = body.max_completion_tokens as number;
|
||||
if (typeof effectiveMax === 'number' && effectiveMax < 8192) {
|
||||
|
||||
@@ -701,10 +701,13 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
}
|
||||
|
||||
// Thinking 模式
|
||||
// v0.8.0 P0-3: 模型能力门控 —— /api/show capabilities 探测为不支持思考
|
||||
//(cachedThinkingSupport === false)时不发 think 参数(Ollama 服务端默认关闭);
|
||||
// 未探测/探测失败(null)fail-open 放行,与 listModels 能力回退策略一致。
|
||||
// 探测在适配器实例创建时 fire-and-forget 发起(refreshContextWindow)。
|
||||
// v0.8.0 P0-3(v0.8.0 修订后保留的唯一门控): /api/show capabilities 探测为
|
||||
// 不支持思考(cachedThinkingSupport === false)时不发 think 参数 —— 与云端
|
||||
// Provider 不同,这是 Ollama 服务端的**硬协议约束**(向无思考能力的模型发
|
||||
// think 会导致每次请求 400 "does not support thinking",而非静默忽略),
|
||||
// 故此处门控属协议正确性而非用户意图覆盖;探测为服务端实时真值(非静态
|
||||
// 元信息)。未探测/探测失败(null)fail-open 放行,与 listModels 能力回退
|
||||
// 策略一致。探测在适配器实例创建时 fire-and-forget 发起(refreshContextWindow)。
|
||||
if (request.params.thinkingEnabled) {
|
||||
const modelThinkingSupported = this.cachedThinkingSupport !== false;
|
||||
if (modelThinkingSupported) {
|
||||
@@ -724,7 +727,7 @@ export class OllamaAdapter extends BaseAdapter {
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
`[Ollama] model "${this.config.defaultModel}" does not support thinking (per /api/show capabilities) — omitting think parameter (P0-3 capability gate)`,
|
||||
`[Ollama] model "${this.config.defaultModel}" does not support thinking (per /api/show capabilities) — omitting think parameter (server would reject with 400 otherwise)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user