You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug] GeminiProvider 生成空 text part,导致 Gemini 原生端点 400(required oneof field 'data' must have one initialized field) #1413
API请求失败:400,
{"error":{"message":"* ***.contents[1].parts[0].data: required oneof field 'data' must have one initialized field
* ***.contents[111].parts[0].data: required oneof field 'data' must have one initialized field
* ***.contents[139].parts[0].data: required oneof field 'data' must have one initialized field
* ***.contents[167].parts[0].data: required oneof field 'data' must have one initialized field","type":"upstream_error","param":""}}
[108352] {
[108353] "role": "model",
[108354] "parts": [
[108355] {
[108356] "text": ""
[108357] }
[108358] ]
……
[109380] 2026-10-23 23:04:28.932 D/GeminiProvider: 请求URL: https://api.<redacted>.com/v1beta/models/gemini-4-argon:streamGenerateContent
[109397] 2026-10-23 23:04:34.422 D/GeminiProvider: 收到初始响应, 耗时: 5509ms, 状态码: 400
[109403] 2026-10-23 23:04:34.423 E/GeminiProvider: API请求失败: 400, {"error":{"message":"* ***.contents[1].parts[0].data: required oneof field 'data' must have one initialized field\n* ***.contents[111].parts[0].data: required oneof field 'data' must have one initialized f
[109405] 2026-10-23 23:04:34.425 W/GeminiProvider: API请求失败: 400, {"error":{"message":"* ***.contents[1].parts[0].data: required oneof field 'data' must have one initialized field\n* ***.contents[111].parts[0].data: required oneof field 'data' must have one initialized f
[109406] com.ai.assistance.operit.api.chat.llmprovider.GeminiProvider$HttpStatusException: API请求失败: 400, {"error":{"message":"* ***.contents[1].parts[0].data: required oneof field 'data' must have one initialized field\n* ***.contents[111].parts[0].data: required oneof
补充一点超出原始推测的发现:你给的 repro 是 {"text": ""},代码里还存在另一种同族非法形态 —— 消息只有图片链接、但图片已不在图片池(LRU 淘汰或重启后清缓存)时,媒体分支得到的是 parts: [],服务端报 contents.parts must not be empty。这一条也一并处理并加了回归用例。
验证:新增 GeminiContentsSerializationTest 13 项,用不变量遍历每个 Part 与 Content;本地全量 :app:testDebugUnitTest 1356 项通过、0 失败。为确认测试真能抓住这个缺陷,只回退两处守卫重跑,13 项里 6 项失败,报错正是 contents[1].parts[0] has empty text、空 parts 计数 expected:<0> but was:<1> 和「没有可发送内容」漏检,与本 issue 报告形态一致;恢复守卫后全部通过。
Activity
xingxinag commentedon Oct 23, 2026
补充:相关的另一个 bug(空 thinkingOptionId 抛异常)
在排查上面这个空 part 问题时,同一个
GEMINI_GENERIC配置还先碰到了另一个报错(请求压根没发出去):注意冒号后为空(optionId 为空)。
根因
ThinkingConfigurationApplier.apply(ThinkingQualityMapping.kt):当模型命中一条
required: true的思考规则(例如内置gemini-thinking-level匹配gemini-(3~9...),gemini-4-argon命中)时,reasoningRequired恒为真,即使关掉思考总开关也会进入该分支。若此时模型配置的thinkingOptionId为空,optionFor("")返回 null → 直接抛异常,请求在发出前失败。复现条件
required规则的模型(如gemini-4-argon);thinkingOptionId为空(从未在 UI 上选过档位);... option is not supported:。原因分析
thinkingOptionId只由 UI 交互(ApiConfigDelegate.updateThinkingOptionId)写入,客户端并不会在新建/切换模型时自动补默认档位。而聊天路径又硬性要求必须有一个有效档位,于是形成“不选就报错”的死结。查的建议修复(看看就行)
optionId无效时,自动回退到mapping.options.first(),而不是抛异常(保留调试日志);thinkingOptionId不在规则 options 中,自动写入默认档位(类似现有的migratePreferencesFromVersion*逻辑:if (mapping.optionFor(id)==null) mapping.options.firstOrNull()?.id);optionId.isBlank(),给出可理解的提示而非空参数。xingxinag commentedon Oct 23, 2026
设备日志证据
来自
files/logs/operit.log,本次 400 失败请求的关键几行:可见:
contents里出现了{"role":"model","parts":[{"text":""}]}空 part;v1beta/models/<model>:streamGenerateContent;contents[1].parts[0].data(即空 part)。yoruuuchan commentedon Oct 26, 2026
已认领这个 issue,正在基于 upstream/dev 修复。
说明:尝试用
gh issue edit --add-assignee yoruuuchan认领失败('yoruuuchan' not found),当前账号对上游仓库没有 triage 权限,无法把自己设为 Assignee,因此以评论方式认领。请维护者在方便时帮忙加上 Assignee。计划范围:
GeminiProvider.buildPartsArray()纯文本分支不再无条件写入text关联 PR #1391 覆盖的是
thinkingOptionId档位对齐(ModelConfigManager / 内置思考规则),与本 issue 的请求体序列化是两个独立问题,不重叠,因此单独开发。yoruuuchan commentedon Oct 27, 2026
修复已提交:https://github.com/AAswordman/Operit/pull/1428(目标分支
dev,Fixes #1413)按原计划范围完成:
buildPartsArray()纯文本分支不再无条件写入text,正文为空白时不产生 PartputContent(role, parts),parts 为空时整条 Content 不进入请求;ASSISTANT / TOOL_CALL / USER / SUMMARY / TOOL_RESULT、工具调用队列、未匹配工具结果 flush 以及关闭工具调用的通用分支全部统一走这个入口parts,以及系统消息全为空白时的systemInstruction"contents": []补充一点超出原始推测的发现:你给的 repro 是
{"text": ""},代码里还存在另一种同族非法形态 —— 消息只有图片链接、但图片已不在图片池(LRU 淘汰或重启后清缓存)时,媒体分支得到的是parts: [],服务端报contents.parts must not be empty。这一条也一并处理并加了回归用例。验证:新增
GeminiContentsSerializationTest13 项,用不变量遍历每个 Part 与 Content;本地全量:app:testDebugUnitTest1356 项通过、0 失败。为确认测试真能抓住这个缺陷,只回退两处守卫重跑,13 项里 6 项失败,报错正是contents[1].parts[0] has empty text、空 parts 计数expected:<0> but was:<1>和「没有可发送内容」漏检,与本 issue 报告形态一致;恢复守卫后全部通过。你备注里提到的
ThinkingConfigurationApplier.apply空optionId抛错:已在 PR 里说明它属于配置保存时的档位对齐,由 #1391 覆盖,与本 PR 的请求体序列化不重叠,所以这里没有重复改动。提醒:
pr-check.yml的触发分支仍写着已改名的development,指向dev的 PR 不会跑任何 CI 检查,本条 PR 的验证是本地结果。细节见 PR 评论。