fix(backend): UniversalGPT.create_messages emit string content when no images

DeepSeek deepseek-chat 等非多模态模型只接受 ``content`` 为字符串。旧实现在
没有 ``video_img_urls`` 输入时也把 ``content`` 拼成
``[{"type":"text","text":...}]`` 多模态数组,导致 DeepSeek API 返回
``Failed to deserialize the JSON body into the target type: messages[0]:
unknown variant `image_url`, expected `text```,整个笔记生成流程随之崩溃。

修复方式:``create_messages`` 在没有截图时退回 string content;有截图时维持
原多模态数组形态,多模态模型功能不退化。同时把 ``_build_merge_messages`` 也
改为 string content —— 合并阶段从不带图片,旧的数组形态会让长视频 chunk
之后的合并阶段同样命中 DeepSeek 400。

新增 ``backend/tests/test_universal_gpt_content_format.py`` (6 cases):

- 无图片 / 显式空 image 列表都走 string content
- 有图片仍输出多模态数组(含 ``image_url`` + ``detail: auto``)
- 纯文本响应里完全不含 ``image_url`` 字段
- ``_build_merge_messages`` 用 string content + 仍带入 partials 文本

红基线:在不打补丁的 ``universal_gpt.py`` 上跑这 6 个 case,3 个 string-
content 断言会失败(命中 issue #282 的同一根因),打补丁后 6/6 通过。

Closes #282
This commit is contained in:
voidborne-d
2026-05-07 13:50:59 +08:00
parent 3d0838ba72
commit 3ff7086491
2 changed files with 208 additions and 12 deletions

View File

@@ -53,20 +53,26 @@ class UniversalGPT(GPT):
extras=kwargs.get('extras'),
)
# ⛳ 组装 content 数组,支持 text + image_url 混合
content: List[dict] = [{"type": "text", "text": content_text}]
video_img_urls = kwargs.get('video_img_urls', [])
for url in video_img_urls:
content.append({
"type": "image_url",
"image_url": {
"url": url,
"detail": "auto"
}
})
content: list[dict] | str
if video_img_urls:
# 有截图时走 OpenAI 多模态 content 数组text + image_url
content = [{"type": "text", "text": content_text}]
for url in video_img_urls:
content.append({
"type": "image_url",
"image_url": {
"url": url,
"detail": "auto"
}
})
else:
# 纯文本场景退回 string contentDeepSeek deepseek-chat 等非多模态模型
# 不识别 [{"type":"text",...}] 数组形态,会返回 invalid_request_error
# issue #282。OpenAI 规范本身也允许 content 为 string。
content = content_text
# 正确格式:整体包在一个 message 里role + content array
messages = [{
"role": "user",
"content": content
@@ -83,9 +89,10 @@ class UniversalGPT(GPT):
def _build_merge_messages(self, partials: list) -> list:
merge_text = MERGE_PROMPT + "\n\n" + "\n\n---\n\n".join(partials)
# 合并阶段没有图片,直接用 string content 兼容非多模态模型issue #282
return [{
"role": "user",
"content": [{"type": "text", "text": merge_text}]
"content": merge_text
}]
def _checkpoint_path(self, checkpoint_key: str) -> Path: