Skip to content

[PR] fix(google): promote stripped proxy call thoughtSignature onto surviving sibling functionCall (#6079, #5252) - #6103

Open
atrokhym wants to merge 1 commit into
ranxianglei:masterfrom
atrokhym:2026-11-09_fix-google-parallel-call-thought-signature
Open

atrokhym wants to merge 1 commit into
ranxianglei:masterfrom
atrokhym:2026-11-09_fix-google-parallel-call-thought-signature

Conversation

@atrokhym

Copy link
Copy Markdown

Fixes #6079
Fixes #5252

Root Cause

In Gemini's function-calling protocol (Gemini 2.5 / 3 / 4), when a model turn emits multiple parallel functionCall parts, only the first functionCall part in the turn carries thoughtSignature; subsequent sibling functionCall parts in the same turn omit thoughtSignature. On the next turn, Gemini validates that the leading functionCall part of the replayed model turn carries thoughtSignature.

When a model turn emits a bili proxy tool call (e.g. compress at call index 0, carrying thoughtSignature) in parallel with a real client tool call (e.g. write_file / run_command / herdr_pane_read at call index 1, without thoughtSignature):

  1. bili executes compress server-side and strips its functionCall part from the response stream forwarded to the client.
  2. The surviving real functionCall part becomes the leading functionCall part in the client's recorded model turn, but bili discarded the thoughtSignature that lived on the stripped compress part instead of promoting it onto the first surviving real functionCall part.
  3. On the next turn (and every subsequent turn), the client echoes back that unsigned functionCall part (which is why #5277's inbound/outbound googleToCore / coreToGoogle audit saw the part arrive unsigned from the client), and Gemini rejects the request with:
    400 INVALID_ARGUMENT: Function call is missing a thought_signature in functionCall parts.

What Changed

  1. src/loop/adapter-google.ts (settleToolCalls + filterRealCallChunk):
    • When a streaming turn contains both stripped proxy call(s) and surviving real call(s), and the first surviving real functionCall part lacks a thoughtSignature while a stripped proxy functionCall part carried one, promotes that thoughtSignature onto the first surviving real functionCall part — both when calls arrive in the same SSE chunk and when they arrive across separate SSE chunks in the same turn, as well as on the structured tool_call event.
    • Adds "acp_cache", "acp_retrieve", "acp_rule", and "acp_read_original_image" to PROXY_TOOL_SET so all server-side proxy tools match isProxyToolFor during Google stream settle.
  2. src/stream-google.ts (rewriteGoogleJsonSteps):
    • Promotes a stripped proxy functionCall part's thoughtSignature onto the first surviving real functionCall part in non-streaming JSON responses (or onto the synthesized marker part on a proxy-only turn when no other part carries a thoughtSignature).
  3. src/loop/core.ts (runCompressLoop):
    • When a Google round contains multiple parallel proxy-only tool calls (which serialize as separate role: "model" turns on the re-request wire), propagates roundProxySignature to sibling proxy tool-call records that lacked their own signature.
  4. tests/adapter-google.test.ts:

Verification

  • Rebased onto current origin/master (v0.1.213)
  • npm run typecheck: clean
  • npm run build: clean (dist-import-annotations: OK, dist-external: OK)
  • node --import tsx --test tests/adapter-google.test.ts: 18/18 passing

@ranxianglei ranxianglei changed the title fix(google): promote stripped proxy call thoughtSignature onto surviving sibling functionCall (#6079, #5252) [PR] fix(google): promote stripped proxy call thoughtSignature onto surviving sibling functionCall (#6079, #5252) Nov 10, 2026
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

(ai: qwen/qwen3.8-27b)

Reviewed PR #6103 against current master — checked out the head, ran typecheck + build + targeted suites locally, and read all three source changes end-to-end while tracing the proxy-vs-real classification path.

Root cause — confirmed correct. Gemini attaches thoughtSignature only to the FIRST functionCall part of a parallel-call turn. When that leading part is a bili server tool (compress, etc.) and gets stripped from the client stream, the first surviving real functionCall becomes the turn's leading part with no signature, so Gemini rejects every subsequent turn with 400 INVALID_ARGUMENT … missing a thought_signature. This matches #6079 / #5252 exactly (both report the same Google-lane post-fold signature loss → sticky 400).

Coverage is comprehensive, not a band-aid. All three surfaces where the unsigned part can reappear are handled: streaming SSE settle (adapter-google.ts settleToolCalls + filterRealCallChunk, incl. calls split across separate SSE chunks); non-streaming JSON (stream-google.ts rewriteGoogleJsonSteps, incl. the proxy-only synthesized note part); and same-loop re-request rebuild (core.ts runCompressLoop) for parallel proxy-only turns. Promotion only fires when the target lacks its own signature — a real call's own thoughtSignature is never overwritten (negative-control test present). The verbatim "keep" path is preserved when nothing changes, so head bytes stay cache-stable.

Local verification: npm run build clean · npm run typecheck clean · tests/adapter-google.test.ts 18/18 · adjacent loop/core/google-signature/openai batch 58/58. CI gate lanes green (kernel-guard / version-guard / config-surface-guard / lane-guard / lock-sync / plan); only the full test shards still running, 0 failures so far.

Two minor observations (non-blocking, follow-up):

  1. The added names make the Google PROXY_TOOL_SET treat acp_retrieve / acp_rule unconditionally as proxy, whereas isProxyToolFor gates them behind ccrEnabled / effectiveRulesEnabled. Divergence only surfaces in the "feature disabled + model hallucinates that tool" corner; the cleaner long-term shape is deriving the adapter set from isProxyToolFor.
  2. The mirror-image PROXY_TOOL_SET in src/loop/adapter-openai.ts was not updated, so the OpenAI wire still classifies acp_cache / acp_retrieve / acp_rule / acp_read_original_image as real (replayed to the client) while core executes them as proxy. Pre-existing and out of scope for this Google-specific fix — worth tracking separately.

Verdict: quality meets the bar — correct root cause, complete surface coverage, tests with teeth (exact-value assertions, both directions), clean focused diff, no dead code, comments explain why. Recommend merge once the remaining test shards go green; the head is ~10 commits behind master with no conflicts, so a rebase is worth doing first.


八问(结构化审核结论)

  1. 是什么问题:Google 线上,模型一轮里并行发出多个 functionCall,且带头的是 bili 服务端工具(如 compress)并携带本轮 thoughtSignature;bili 把该代理调用从客户端流剥掉时把签名一起丢了,存活的真实 functionCall 变成该轮"带头"却无签名,Gemini 之后每轮都回 400。
  2. 什么原因导致:Gemini 只给并行调用轮的第一个 functionCall 部分盖 thoughtSignature。bili 的剥离逻辑删掉了带签名的领头代理部分,却没把签名迁移到第一个存活的真实兄弟部分;流式 SSE 结算、非流式 JSON、同轮重发重建三个面都会出现这个无签名带头部分。
  3. 影响范围与严重程度:Google 线(Gemini 3/4)会话,触发条件=一轮内代理工具与真实工具并行调用(较常见:边压缩边做文件操作)。严重度 p1 级可用性——从下一轮起整条会话每个请求都被 400 拒掉,等于不可用。
  4. 怎么修复的:把被剥代理调用的 thoughtSignature 提升到第一个存活真实 functionCall 部分(同轮纯代理则提升到兄弟重建轮 / 合成 note 部分),三个面统一处理;仅当目标自身无签名才提升,绝不覆盖真实调用自己的签名;无变化时保持逐字原字节(缓存友好)。
  5. 是否有回归问题:filterRealCallChunk 从 keep[]+filter 重构为 outParts[],keep/null/rewrite 三态语义与输出顺序逐项核对等价;缓存友好的"逐字 keep"路径保留;负向对照(真实调用自带签名不被覆盖)有测试钉住;新增的四个都是 bili 自有服务端工具、始终服务端执行,PR 只是把既有剥离逻辑一致地扩到它们身上,未引入新的模式交互;未见意外语义漂移。
  6. 是否破坏缓存:否——无代理/无提升时仍返回原始字节 verbatim;提升只落在本就要重写(含代理)或本就要重建的部分上;头部 system 消息字节未动,前缀缓存不受影响。
  7. 是否改了设计方向:否——完全落在既有 google 线剥离/提升机制内,无架构/边界/数据流转向变化。
  8. 是否引入新配置:否——无新 env/config/默认值。

判定:可合(等剩余 test shard 转绿后由你合并;建议先 rebase,head 落后 master 约 10 提交但无冲突)。

@ranxianglei
ranxianglei force-pushed the 2026-11-09_fix-google-parallel-call-thought-signature branch from 4550055 to 39145bd Compare November 12, 2026 07:27
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

Merge conflict resolved — the PR is now mergeable.

What I did this run:

  • Fetched the PR head (2026-11-09_fix-google-parallel-call-thought-signature, was c76985d3) and the current base. master had advanced from the PR's original base (541cf260) to 9674cfaf, and two of the PR's four touched files were also changed on master (src/loop/adapter-google.ts, src/loop/core.ts) — so the rebase produced one real conflict.
  • Rebased the PR head onto current master. The single conflict was in PROXY_TOOL_SET (src/loop/adapter-google.ts:17): master's Request two capabilities: compressed block "retirement" and summary "external rewrite". | 请求两个能力:压缩块「退役」与摘要「外部重写」 #5765 block-ops added acp_retire/acp_restore/acp_rewrite/acp_cache (with an explanatory comment) to that set, while this PR added acp_cache/acp_retrieve/acp_rule/acp_read_original_image to the same set. Both sides are purely additive lists of legitimate proxy tool names, so I resolved it as the union of both (deduping acp_cache), keeping master's comment. The other three files (core.ts, stream-google.ts, tests/adapter-google.test.ts) auto-merged cleanly.
  • Verified the merged result locally: npm run typecheck clean, and the targeted tests/adapter-google.test.ts suite passes 18/18 (including the signature-promotion cases).
  • Force-pushed the rebased head 39145bd9b5 (on current master 9674cfaf) to the PR branch.

Live state this run: mergeable: true, mergeable_state: blocked (was dirty) — the conflict is gone; "blocked" = pending required CI checks / approvals, which is normal. CI is running on the new head: 5 success, 4 skipped, 19 queued, 0 failures so far.

八问

  1. 是什么问题:PR [PR] fix(google): promote stripped proxy call thoughtSignature onto surviving sibling functionCall (#6079, #5252) #6103(镜像 [Bug][dsh] "Compress NOW" directive span starts one message early, so following it verbatim is always REFUSED by compress.requireRecommendation #6105)与其 base 分支 master 存在 merge 冲突,merge 状态为 dirty,无法合并。
  2. 什么原因导致的:master 已从 PR 的原始 base(541cf260)前进到 9674cfaf,其中两个 PR 触碰的文件(src/loop/adapter-google.ts、src/loop/core.ts)在 master 侧也被改过——Request two capabilities: compressed block "retirement" and summary "external rewrite". | 请求两个能力:压缩块「退役」与摘要「外部重写」 #5765 block-ops 往 PROXY_TOOL_SET 追加了 acp_retire/acp_restore/acp_rewrite/acp_cache,本 PR 往同一个 Set 追加了 acp_cache/acp_retrieve/acp_rule/acp_read_original_image;对同一行的重叠编辑产生了文本冲突。
  3. 影响范围与严重程度:只影响 merge 状态(冲突导致 PR 无法合并),冲突本身不改变任何运行时行为。严重度:阻塞级(不解决就无法合并)。
  4. 怎么修复的:把 PR head rebase 到当前 master(9674cfaf)。唯一冲突在 PROXY_TOOL_SET——两侧都是往同一个 Set 追加合法 proxy 工具名,故取并集(acp_cache 去重)、保留 master 的 Request two capabilities: compressed block "retirement" and summary "external rewrite". | 请求两个能力:压缩块「退役」与摘要「外部重写」 #5765 注释;另三个文件自动合并干净。验证:npm run typecheck 通过 + 定向 tests/adapter-google.test.ts 18/18 通过。force-push 了 rebase 后的 head。
  5. 是否有回归问题:冲突解决是纯字符串字面量的并集,未改任何逻辑、未动既有行为;自动合并保留了 master 周边上下文(已逐段核对 diff)。typecheck + 定向测试全绿,rebase 本身无回归。(PR 自身的行为变更属 PR 作者范畴,另行 review。)
  6. 是否破坏缓存:不涉及——本次是 merge/rebase,未触碰任何缓存/重试/增量游标/去重逻辑。
  7. 是否改了设计方向:不涉及——无架构/边界/数据流转向,纯冲突解决。
  8. 是否引入了新的配置:不涉及——无新增 env/config/默认值。

判定:冲突已解决,PR 现为 mergeable(blocked = 等 CI/审批,属正常),CI 在新 head 上运行中、暂无失败。收工。

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

Re-trigger handled (auto-fix lane, attempt 1/3). Per status-freshness I re-verified the live state this run instead of trusting the prior run's result.

Conflict — still resolved, nothing to rebase. PR head 39145bd9b5, base 9674cfaf which is the true master tip (confirmed via git ls-remote; master has not advanced since run 1). mergeable: true, mergeable_state: blocked (not dirty) — the merge conflict run 1 resolved is still resolved. No new conflict to fix.

CI — in progress, no failures. Last mirror snapshot: 4 success, 13 skipped, 2 in_progress, 11 queued, 0 failures. The test shards (windows/ubuntu × node 22/24) and static-gates are running.

Honest caveat (status-freshness): the mirror check cache has not refreshed since 07:52Z across this run's polls, so I could not confirm the final CI verdict live — I'm reporting the last known state, not asserting green. There is no gh CLI in this environment and policy forbids PAT-polling check states, so the mirror cache is the only source; if it stays stale that's a poller issue to flag to ops, not a PR problem.

Action this run: none required. My primary task (conflict resolution) was completed in the prior run and remains resolved; CI is not red, so there is no red-CI fix to make. The PR is now waiting on CI completion + human approval (merge is human-only). On the next trigger, if CI has come back red I'll take the red-CI fix.

八问:

  1. 是什么问题: PR [PR] fix(google): promote stripped proxy call thoughtSignature onto surviving sibling functionCall (#6079, #5252) #6103 与 master 的合并冲突(上一轮已解决),本轮为复检。
  2. 什么原因: master 的 Request two capabilities: compressed block "retirement" and summary "external rewrite". | 请求两个能力:压缩块「退役」与摘要「外部重写」 #5765 block-ops 与 PR 同时给 PROXY_TOOL_SET(src/loop/adapter-google.ts:17)加代理工具名,两边纯新增导致文本冲突。
  3. 影响范围与严重程度: 仅合并冲突,无行为变更;冲突已解决并保持解决,无用户可见影响。
  4. 怎么修复: 取并集(去重 acp_cache,保留 master 注释),rebase 到真 master 9674cfaf,force-push 39145bd9b5。
  5. 是否有回归: 无;typecheck 干净 + tests/adapter-google.test.ts 18/18 通过(上一轮验证)。
  6. 是否破坏缓存: 不涉及。
  7. 是否改了设计方向: 不涉及。
  8. 是否引入新配置: 不涉及。

判定: 冲突已解决且本轮复检确认保持解决,master 未前进,CI 无失败(进行中;最终结论因镜像缓存未刷新暂无法 live 确认,如实上报而非断言绿)。本轮无需修复,等待 CI 完成 + 人工批准。

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

Re-verified live this run (status-freshness). The state has moved forward since the last check:

CI is now GREEN (all completed). Mirror cache (fresh, 08:38Z): 16 success, 14 skipped, 0 failures, 0 in_progress, 0 queued. The test shards (windows/ubuntu × node 22/24) and static-gates all passed on head 39145bd9b5.

Conflict: still resolved. PR head 39145bd9b5, mergeable_state: blocked (not dirty) — the merge conflict run 1 resolved remains resolved. No new conflict to fix.

Action this run: none required. Both of my responsibilities are clear:

  • Merge-conflict resolution — done in run 1, still resolved.
  • Red-CI fixing — not needed; CI came back green, no failures to fix.

The PR is now ready for human approval (merge is human-only). Nothing left for the CI lane to do.

八问:

  1. 是什么问题: PR [PR] fix(google): promote stripped proxy call thoughtSignature onto surviving sibling functionCall (#6079, #5252) #6103 与 master 的合并冲突(上一轮已解决);本轮复检并确认 CI 结果。
  2. 什么原因: master Request two capabilities: compressed block "retirement" and summary "external rewrite". | 请求两个能力:压缩块「退役」与摘要「外部重写」 #5765 block-ops 与 PR 同时给 PROXY_TOOL_SET(src/loop/adapter-google.ts:17)加代理工具名,两边纯新增导致文本冲突。
  3. 影响范围与严重程度: 仅合并冲突,无行为变更;已解决,无用户可见影响。
  4. 怎么修复: 取并集(去重 acp_cache,保留 master 注释),rebase 到真 master 9674cfaf,force-push 39145bd9b5。
  5. 是否有回归: 无;typecheck 干净 + tests/adapter-google.test.ts 18/18,CI 全套 16 success / 0 failures。
  6. 是否破坏缓存: 不涉及。
  7. 是否改了设计方向: 不涉及。
  8. 是否引入新配置: 不涉及。

判定: 冲突已解决且保持解决,CI 全绿(16 success / 14 skipped / 0 failures),本轮无需修复;PR 已具备合并条件,等待人工批准(合并权在 owner)。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants