问题描述
当客户端请求使用带有 适配器/模型 命名空间前缀的模型 ID(如 gemini_text/gemini-3.1-flash)时,服务端会错误地把该模型判定为图像生成模型,从而走 parseImageRequest 逻辑,只取最后一条 user 消息发送给目标网站,导致:
- System Prompt 完全丢失
- 历史对话完全丢失
- (对 SillyTavern 等客户端)角色卡、世界书、作者注等上下文全部不会被发送
复现环境
- 项目版本:3.0.0
- Node: v24.13.1
- OS: Windows 11
- 配置:
backend.pool.instances:
- name: browser_default
workers:
- name: gemini_text_worker
type: gemini_text
复现步骤
- 配置一个
type: gemini_text 的 worker 并正常登录
- 使用 OpenAI 兼容客户端(如 SillyTavern)发起请求,模型名填
gemini_text/gemini-3.1-flash
- 请求包含完整的
system 消息与多轮 user/assistant 历史
- 查询
data/history/history.db 的 requests 表,可以看到该请求的 prompt 字段内容只有最后一条 user 消息,没有三段式结构
实测对比数据(来自 data/history/history.db)
| model_id |
是否带前缀 |
prompt 长度 |
prompt 开头 |
gemini-3-flash(lmarena_text 注册) |
否 |
1189 ~ 3632 |
=== 系统指令 (永远置顶) ===\nWrite ... ✅ |
gemini_text/gemini-3.1-flash |
是 |
2 ~ 46 |
只有 "Hi" / "你在干嘛" 等原始用户消息 ❌ |
同一条酒馆请求,仅改模型名,结果截然不同。
根本原因
位于 src/backend/pool/Worker.js:605-622:
getModelType(modelKey) {
if (this.type === 'merge') {
// merge 分支有前缀处理...
} else {
return registry.getModelType(this.type, modelKey); // ❌ 未剥离 type/ 前缀
}
}
同文件 318-343 行的 supports() 方法对 type/model 前缀做了剥离处理:
if (modelId.includes('/')) {
const [specifiedType, actualModel] = modelId.split('/', 2);
if (specifiedType === this.type) {
return registry.supportsModel(this.type, actualModel); // ✅ 用剥离后的 key
}
return false;
}
return registry.supportsModel(this.type, modelId);
而 getModelType() 的非 merge 分支漏掉了相同的前缀剥离,把 "gemini_text/gemini-3.1-flash" 整个字符串传给了 registry.getModelType。
src/backend/registry.js:304-312 里用的是精确匹配:
const model = adapter.models.find(m => m.id === modelKey);
return model?.type || 'image';
注册表里的 key 是 "gemini-3.1-flash"(见 src/backend/adapter/gemini_text.js:228),带前缀的 key 当然匹配不到,于是默认返回 'image'。
这导致 PoolManager.getModelType(src/backend/pool/PoolManager.js:275-282)虽然通过 supports 找到了对应 worker,但 worker.getModelType 返回错误结果。
下游的 src/server/api/openai/parse.js:98-105 根据此错误类型判断:
const type = getModelType ? getModelType(data.model) : 'image';
isTextMode = type === 'text';
if (isTextMode) {
// parseTextRequest - 构建三段式虚拟上下文 ✅
} else {
// parseImageRequest - 只取最后一条 user 消息 ❌
}
判定为 image,走了 parseImageRequest,system 和历史被丢弃。
影响范围
任何带 适配器/ 前缀的文本模型 ID:
gemini_text/*
chatgpt_text/*
deepseek_text/*
zai_is_text/*
doubao_text/*
- 等等
这些在 WebUI 中出现冲突时会自动带前缀,用户几乎一定会遇到此问题。
建议修复
参照 supports() 的写法,在 Worker.getModelType() 非 merge 分支加入前缀剥离:
getModelType(modelKey) {
if (this.type === 'merge') {
// ... 保持不变
} else {
if (modelKey.includes('/')) {
const [specifiedType, actualModel] = modelKey.split('/', 2);
if (specifiedType === this.type) {
return registry.getModelType(this.type, actualModel);
}
}
return registry.getModelType(this.type, modelKey);
}
}
同样,建议检查 getImagePolicy 等其他会接受 modelKey 的方法是否存在相同问题。
临时绕过方法
客户端模型名不使用前缀,直接填 gemini-3.1-flash(仅当该 key 在各适配器中不冲突时可用)。
问题描述
当客户端请求使用带有
适配器/模型命名空间前缀的模型 ID(如gemini_text/gemini-3.1-flash)时,服务端会错误地把该模型判定为图像生成模型,从而走parseImageRequest逻辑,只取最后一条 user 消息发送给目标网站,导致:复现环境
复现步骤
type: gemini_text的 worker 并正常登录gemini_text/gemini-3.1-flashsystem消息与多轮user/assistant历史data/history/history.db的requests表,可以看到该请求的prompt字段内容只有最后一条 user 消息,没有三段式结构实测对比数据(来自
data/history/history.db)gemini-3-flash(lmarena_text 注册)=== 系统指令 (永远置顶) ===\nWrite ...✅gemini_text/gemini-3.1-flash"Hi"/"你在干嘛"等原始用户消息 ❌同一条酒馆请求,仅改模型名,结果截然不同。
根本原因
位于
src/backend/pool/Worker.js:605-622:同文件 318-343 行的
supports()方法对type/model前缀做了剥离处理:而
getModelType()的非 merge 分支漏掉了相同的前缀剥离,把"gemini_text/gemini-3.1-flash"整个字符串传给了registry.getModelType。src/backend/registry.js:304-312里用的是精确匹配:注册表里的 key 是
"gemini-3.1-flash"(见src/backend/adapter/gemini_text.js:228),带前缀的 key 当然匹配不到,于是默认返回'image'。这导致
PoolManager.getModelType(src/backend/pool/PoolManager.js:275-282)虽然通过supports找到了对应 worker,但worker.getModelType返回错误结果。下游的
src/server/api/openai/parse.js:98-105根据此错误类型判断:判定为
image,走了parseImageRequest,system 和历史被丢弃。影响范围
任何带
适配器/前缀的文本模型 ID:gemini_text/*chatgpt_text/*deepseek_text/*zai_is_text/*doubao_text/*这些在 WebUI 中出现冲突时会自动带前缀,用户几乎一定会遇到此问题。
建议修复
参照
supports()的写法,在Worker.getModelType()非 merge 分支加入前缀剥离:同样,建议检查
getImagePolicy等其他会接受modelKey的方法是否存在相同问题。临时绕过方法
客户端模型名不使用前缀,直接填
gemini-3.1-flash(仅当该 key 在各适配器中不冲突时可用)。