有些流式错误看起来像模型断流,实际上是客户端把“没有正文”的片段当成“没有字段”。
NousResearch/hermes-agent issue #24974 讲的就是这个细节。
用户通过 google-gemini-cli provider 使用 Gemini 2.5 Flash。模型先输出 thinking / reasoning block,还没有 visible text。Hermes 的 Gemini Code Assist streaming adapter 把这个 reasoning-only chunk 转成 OpenAI 风格的 stream delta。
但旧代码只在有正文时才写入:
content
于是 downstream consumer 访问:
delta.content
直接炸:
AttributeError: 'types.SimpleNamespace' object has no attribute 'content'
这不是 Gemini 没回复,也不是 API key、网络、配额第一时间出问题。
更准确地说:
流式 chunk 的 schema 没补齐。
现场表现:thinking 已经来了,正文还没来,流先挂了
issue 里的 live log 很典型:
run_agent - INFO - Gemini Cloud Code Assist client created
[thinking] **Acknowledging your input**
I've processed your acknowledgment and am ready for your next directive...
run_agent - INFO - Streaming failed before delivery: 'types.SimpleNamespace' object has no attribute 'content'
run_agent - WARNING - API call failed (attempt 2/3) error_type=AttributeError
关键点在这里:
[thinking] 已经出现了
说明模型并非完全没有响应。
真正失败点是 Hermes 收到 reasoning-only delta 后,后续消费代码假设每个 delta 都有 .content 属性。
但这个属性压根没被创建。
最小复现:不需要真实 API quota
这个 bug 甚至不需要真的调用 Gemini。
issue 给出的最小复现是:
from agent.gemini_cloudcode_adapter import _make_stream_chunk
d = _make_stream_chunk(model="m", reasoning="think").choices[0].delta
assert d.content is None
旧版本会在这里报:
AttributeError
因为 SimpleNamespace 不是 dict。
你没传进去的 key,不会变成 None。
它就是不存在。
根因:SimpleNamespace 不会自动补字段
问题出在:
agent/gemini_cloudcode_adapter.py::_make_stream_chunk()
旧逻辑大概是:
delta_kwargs = {"role": "assistant"}
if content:
delta_kwargs["content"] = content
if reasoning:
delta_kwargs["reasoning"] = reasoning
delta_kwargs["reasoning_content"] = reasoning
delta = SimpleNamespace(**delta_kwargs)
如果当前 chunk 是 reasoning-only:
content = ""
reasoning = "think"
那么 if content: 不成立。
最终生成的是:
SimpleNamespace(
role="assistant",
reasoning="think",
reasoning_content="think"
)
里面没有:
content
于是访问:
delta.content
必然 AttributeError。
为什么这类 bug 在 Gemini 上特别容易出现?
因为 Gemini / reasoning 模型的流式输出不一定先给你可见文本。
它可能先吐:
- thinking block;
- reasoning delta;
- tool call delta;
- metadata;
- 然后才是 visible content。
如果客户端 adapter 模拟 OpenAI ChatCompletionChunk,就必须保证 delta schema 稳定。
即使内容为空,也应该有:
content = None
而不是直接缺字段。
对下游代码来说,这两者差别巨大:
hasattr(delta, "content") == True
和:
AttributeError
不是一个级别的问题。
正确修法:给 delta 补完整 schema
issue 提出的最小修复是一行:
- delta_kwargs = {"role": "assistant"}
- if content:
- delta_kwargs["content"] = content
+ delta_kwargs = {"role": "assistant", "content": content or None}
为什么是 None 而不是空字符串?
因为 OpenAI 风格的 ChatCompletionChunk 里,stream delta 的:
delta.content
通常是:
Optional[str]
也就是可以为 None。
当这个片段只有 reasoning,没有 visible text 时,content=None 比 content="" 更贴近语义。
后续维护者评论也确认类似方向:
#21856: always populate full delta schema in stream chunks
也就是给 stream chunk 的 delta key 预先填成 None。
相关路径:Gemini CLI ACP 也踩过类似坑
评论还提到相关 issue / 修复:
#21609 fixes the content field omission in the Gemini CLI ACP path
这说明问题不是一个孤立 typo,而是 adapter 设计里常见的协议形态陷阱。
当一个系统把 Gemini、OpenAI、ACP、Cloud Code Assist 等不同流式协议统一成一个内部 chunk 对象时,字段缺省策略必须统一。
否则下游处理逻辑会变成:
有些 provider 是 content=None
有些 provider 是没有 content 属性
有些 provider 是 content=""
这种不一致会让 streaming consumer 很脆弱。
为什么错误会被重试,反而更难定位?
issue 里还有一个很实用的细节:
这个 AttributeError 被 Hermes 分类成:
reason=unknown
retryable=True
于是用户看到的是:
API call failed (attempt 2/3)
这会制造误导。
因为看起来像:
- Gemini API 不稳定;
- 网络断了;
- 模型超时;
- 配额不足;
- provider 要 fallback。
如果后续 retry 又撞上 429 quota,用户甚至可能以为根因是 quota。
但第一次真正的根因是本地 adapter schema 缺字段。
排查时怎么一眼识别?
如果你看到这些组合,就要怀疑 reasoning-only delta 问题:
provider: google-gemini-cli
model: gemini-2.5-flash 或其他 Gemini thinking 模型
streaming failed before delivery
SimpleNamespace object has no attribute 'content'
[thinking] 已经打印过
尤其是日志里先出现:
[thinking]
再出现:
AttributeError: ... no attribute 'content'
基本就不是 API 服务端没返回,而是客户端处理 reasoning chunk 出错。
对 DeepAI API 中转站用户有什么启发?
这篇问题本身发生在 Gemini Code Assist / Cloud Code adapter,不是 DeepAI API 中转站的问题。
但它对 OpenAI-compatible 工具链有一个重要提醒:
统一 API 入口只能统一请求入口,不能替每个客户端修正内部 streaming schema。
如果你在 Cherry Studio、Cline、Dify、Open WebUI 或 Hermes 里使用 DeepAI API 中转站,重点要区分两层问题:
- API 层:Base URL、API Key、模型 ID、请求参数、计费、限流;
- 客户端适配层:stream chunk schema、tool call delta、reasoning metadata、history replay。
DeepAI 可以作为稳定的 OpenAI-compatible API 入口,方便统一模型调用和 key 管理。
但如果某个客户端内部把 content=None 写成了“没有 content 字段”,那仍然需要客户端升级或修补。
临时规避方案
如果你还在旧版本 Hermes,可以考虑:
1. 暂时关闭 streaming
如果配置允许,关闭流式输出,让客户端走 non-stream response path。
因为 issue 指向的是:
_make_stream_chunk()
非流式路径未必触发同样问题。
2. 换不先输出 reasoning-only delta 的模型
如果某些 Gemini 模型不会先吐 thinking block,可以暂时避开触发条件。
但这不是根治。
3. 升级到包含相关修复的 Hermes
重点关注:
#21856#21609- commit
26933c2f592bda25df735c555620a2a978cfefb6
4. 本地打补丁
最小补丁思路:
delta_kwargs = {"role": "assistant", "content": content or None}
更完整的方式是把 delta schema 里的常见字段都初始化为 None。
FAQ
看到 .content AttributeError 是 Gemini API 挂了吗?
不一定。这个 issue 中 Gemini 已经返回了 reasoning chunk,失败发生在 Hermes 本地 streaming adapter。
为什么 reasoning-only chunk 会没有 content?
因为旧代码只有 if content: 为真时才把 content 放进 SimpleNamespace。reasoning-only chunk 的 content 为空,所以字段缺失。
content=None 和没有 content 字段有什么区别?
区别很大。content=None 表示这个 delta 没有 visible text;没有字段会让 delta.content 访问直接抛 AttributeError。
关闭 streaming 有用吗?
可能有用。该问题指向 _make_stream_chunk(),关闭 streaming 可能避开这个具体路径,但最好还是升级或修补客户端。
DeepAI API 中转站能修复这个问题吗?
不能。这是 Hermes Gemini adapter 的本地 schema bug。DeepAI 可作为 OpenAI-compatible API 入口,但不能修复客户端内部 chunk 字段缺失。
总结
#24974 的核心不是“Gemini 没有 content”,而是:
reasoning-only delta 也必须有 content 字段,只是值可以是 None。
当 Agent 框架把不同 provider 的流式协议统一成 OpenAI-like chunk 时,schema 稳定性比字段值本身更重要。
少一个 .content,就足以让整条 streaming 链路从“模型正在思考”变成“客户端直接崩溃”。