Skip to content

流式中断/0 Token/签名失效:自愈机制与排查路径

在 Antigravity Tools 里调用 /v1/messages(Anthropic 兼容)或 Gemini 原生流式接口时,如果你遇到“流式输出中断”“200 OK 但 0 Token”“Invalid signature”这类问题,本课给你一条从 UI 到日志的排查路径。

学完你能做什么

  • 知道 0 Token/中断问题在代理里通常会先被“peek 预读”拦下来
  • 能从 Proxy Monitor 里确认本次请求的账号与映射模型(X-Account-Email / X-Mapped-Model
  • 能通过日志判断是“上游流早夭”“退避重试”“轮换账号”还是“签名修复重试”
  • 知道哪些情况该等代理自愈,哪些情况要手动介入

你现在的困境

你可能看到这些“现象”,但不知道要从哪里下手:

  • 流式输出到一半断掉,客户端像“卡死”一样不再继续
  • 200 OK,但 usage.output_tokens=0 或内容为空
  • 400 错误里出现 Invalid \signature`Corrupted thought signaturemust be `thinking`` 等

这类问题大多不是“你请求写错了”,而是流式传输、上游限流/波动、或历史消息里携带的签名块触发了上游校验。Antigravity Tools 在代理层做了多道防线,你只需要按固定路径验证它到底卡在哪一步。

什么是 0 Token?

0 Token通常指一次请求最终返回的 output_tokens=0,并且看起来“没生成内容”。在 Antigravity Tools 里,它更常见的成因是“流式响应在真正输出前就结束/报错”,而不是模型真的生成了 0 个 token。代理会尝试用 peek 预读把这类空响应拦下来并触发重试。

代理在背后做的三件事(先有心智模型)

1) 非流式请求可能被自动转换为流式

/v1/messages 路径里,代理会在内部把“客户端非流式请求”转换为流式请求来请求上游,并在收到 SSE 后再收集成 JSON 返回(这样做的原因在日志里写明是“better quota”)。

源码证据:src-tauri/src/proxy/handlers/claude.rs#L665-L913

2) Peek 预读:先等到“第一块有效数据”再把流交给客户端

/v1/messages 的 SSE 输出,代理会先 timeout + next() 预读,跳过心跳/注释行(以 : 开头),直到拿到第一块“不是空、不是心跳”的数据再开始正式转发。如果 peek 阶段就报错/超时/流结束,会直接进入下一轮尝试(下一轮通常会触发账号轮换)。

源码证据:src-tauri/src/proxy/handlers/claude.rs#L812-L926;Gemini 原生流式也有类似 peek:src-tauri/src/proxy/handlers/gemini.rs#L117-L149

3) 统一退避重试 + 按状态码决定“要不要轮换账号”

代理对常见状态码做了明确的退避策略,并定义了哪些状态码会触发轮换账号。

源码证据:src-tauri/src/proxy/handlers/claude.rs#L117-L236

🎒 开始前的准备

跟我做

第 1 步:确认你调用的是哪条接口路径

为什么/v1/messages(claude handler)和 Gemini 原生(gemini handler)的自愈细节不同,先确认路径能避免你在错的日志关键字上浪费时间。

打开 Proxy Monitor,找到那条失败的请求,先记下 Path:

  • /v1/messages:看 src-tauri/src/proxy/handlers/claude.rs 的逻辑
  • /v1beta/models/...:streamGenerateContent:看 src-tauri/src/proxy/handlers/gemini.rs 的逻辑

你应该看到:请求记录里能看到 URL/方法/状态码(以及请求耗时)。

第 2 步:从响应 Header 里抓住“账号 + 映射模型”

为什么 同一个请求失败/成功,很多时候取决于“这次选到哪个账号”“被路由到哪个上游模型”。代理会把这两个信息写到响应头,先记下来,后面看日志能对上号。

在失败的那条请求里,找这些响应头:

  • X-Account-Email
  • X-Mapped-Model

这两项在 /v1/messages 和 Gemini handler 里都会设置(例如 /v1/messages 的 SSE 响应里:src-tauri/src/proxy/handlers/claude.rs#L887-L896;Gemini SSE:src-tauri/src/proxy/handlers/gemini.rs#L235-L245)。

你应该看到X-Account-Email 是邮箱,X-Mapped-Model 是实际请求的模型名。

第 3 步:在 app.log 里判断是不是“peek 阶段就失败”

为什么 peek 失败通常意味着“上游根本没开始吐有效数据”。这类问题最常见的处理方式是重试/轮换账号,你需要确认代理有没有触发。

先定位日志文件(日志目录来自数据目录的 logs/,并按天滚动写入 app.log*)。

bash
ls -lt "$HOME/.antigravity_tools/logs" | head
powershell
Get-ChildItem -Force (Join-Path $HOME ".antigravity_tools\logs") | Sort-Object LastWriteTime -Descending | Select-Object -First 5

然后在最新的 app.log* 里搜这些关键字:

  • /v1/messages(claude handler):Stream error during peek / Stream ended during peek / Timeout waiting for first datasrc-tauri/src/proxy/handlers/claude.rs#L828-L864
  • Gemini 原生流式:[Gemini] Empty first chunk received, retrying... / Stream error during peek / Stream ended immediatelysrc-tauri/src/proxy/handlers/gemini.rs#L117-L144

你应该看到:如果触发了 peek 重试,日志里会出现类似 “retrying...” 的告警,并且随后会进入下一轮 attempt(通常会带来账号轮换)。

第 4 步:如果是 400/Invalid signature,确认代理是否做了“签名修复重试”

为什么 签名类错误经常来自历史消息里的 Thinking 块/签名块不符合上游要求。Antigravity Tools 会尝试“降级历史 thinking 块 + 注入修复提示词”再重试,你应该先让它自愈跑完。

你可以用 2 个信号判断它是否进入了修复逻辑:

  1. 日志里出现 Unexpected thinking signature error ... Retrying with all thinking blocks removed.src-tauri/src/proxy/handlers/claude.rs#L999-L1025
  2. 随后会把历史 Thinking 块转换为 Text,并在最后一条 user message 追加修复提示词(src-tauri/src/proxy/handlers/claude.rs#L1027-L1102;Gemini handler 也会对 contents[].parts 追加同样的提示词:src-tauri/src/proxy/handlers/gemini.rs#L300-L325

你应该看到:代理会在短延迟后自动重试(FixedDelay),并可能进入下一轮尝试。

检查点 ✅

  • [ ] 你能在 Proxy Monitor 里确认请求路径(/v1/messages 或 Gemini 原生)
  • [ ] 你能拿到本次请求的 X-Account-EmailX-Mapped-Model
  • [ ] 你能在 logs/app.log* 里搜到 peek/重试相关关键字
  • [ ] 遇到 400 签名错误时,你能确认代理是否进入“修复提示词 + 清理 thinking 块”的重试逻辑

踩坑提醒

场景你可能会怎么做(❌)推荐做法(✓)
看到 0 Token 就立刻手动重试很多次一直按客户端重试按钮,完全不看日志先看一次 Proxy Monitor + app.log,确认是否是 peek 阶段早夭(会自动重试/轮换)
遇到 Invalid \signature`` 就直接清空数据目录.antigravity_tools 整个删掉,账号/统计全没了先让代理执行一次“签名修复重试”;只有在日志明确提示不可恢复时,再考虑手动介入
把“服务端波动”当成“账号坏了”400/503/529 一律轮换账号轮换是否有效取决于状态码;代理本身有 should_rotate_account(...) 规则(src-tauri/src/proxy/handlers/claude.rs#L226-L236

本课小结

  • 0 Token/流式中断在代理里通常先经过 peek 预读;peek 阶段失败会触发重试并进入下一轮 attempt
  • /v1/messages 可能会把非流式请求内部转换为流式再收集回 JSON,这会影响你理解“为什么看起来像流式问题”
  • 签名失效类 400 错误,代理会尝试“修复提示词 + 清理 thinking 块”再重试,你优先验证这条自愈路径是否走通

下一课预告

下一课我们学习 端点速查表


附录:源码参考

点击展开查看源码位置

更新时间:2026-01-23

功能文件路径行号
Claude handler:退避重试策略 + 轮换规则src-tauri/src/proxy/handlers/claude.rs117-236
Claude handler:内部把非流式转换为流式(better quota)src-tauri/src/proxy/handlers/claude.rs665-776
Claude handler:peek 预读(跳过心跳/注释,避免空流)src-tauri/src/proxy/handlers/claude.rs812-926
Claude handler:400 签名/块顺序错误的修复重试src-tauri/src/proxy/handlers/claude.rs999-1102
Gemini handler:peek 预读(防止空流 200 OK)src-tauri/src/proxy/handlers/gemini.rs117-149
Gemini handler:400 签名错误的修复提示词注入src-tauri/src/proxy/handlers/gemini.rs300-325
签名缓存(三层:tool/family/session,含 TTL/最小长度)src-tauri/src/proxy/signature_cache.rs5-207
Claude SSE 转换:捕获签名并写入签名缓存src-tauri/src/proxy/mappers/claude/streaming.rs639-787

关键常量

  • MAX_RETRY_ATTEMPTS = 3:最大重试次数(src-tauri/src/proxy/handlers/claude.rs#L27
  • SIGNATURE_TTL = 2 * 60 * 60 秒:签名缓存 TTL(src-tauri/src/proxy/signature_cache.rs#L6
  • MIN_SIGNATURE_LENGTH = 50:签名最小长度(src-tauri/src/proxy/signature_cache.rs#L7

关键函数

  • determine_retry_strategy(...):按状态码选择退避策略(src-tauri/src/proxy/handlers/claude.rs#L117-L167
  • should_rotate_account(...):按状态码决定是否轮换账号(src-tauri/src/proxy/handlers/claude.rs#L226-L236
  • SignatureCache::cache_session_signature(...):缓存会话签名(src-tauri/src/proxy/signature_cache.rs#L149-L188