Skip to content

bug(antigravity): Pi 0.86+ 下 provider stream 收到 TranscriptContext,每次请求都以 400 失败 #619

Description

@JS-banana

问题

Pi 0.86.0 起,自定义 provider 的 streamSimple 输入由 Context 改为规范化后的 TranscriptContext:系统提示词与工具声明被折进 context.messages 的首条 role: "system" 消息,context.systemPrompt 与 context.tools 不再存在。Pi 0.86.0 CHANGELOG 的 Breaking Changes 已明确要求:

Changed inherited pi-ai provider stream inputs from Context to normalized TranscriptContext values. Custom providers must read system prompts and tool declarations from context.messages with getCurrentSystemPrompt() and getCurrentTools().

extensions/ai-providers/antigravity/ 的适配层仍按旧 Context 读取:

  • provider.ts:normalizeSystemPrompts(context.systemPrompt)、buildTools(context.tools);
  • google-conversion.ts:transformMessages() 把非 user / 非 toolResult 的消息一律当作 assistant 处理,convertMessages() 的兜底分支把其余角色当作 toolResult,两处都没有 role === "system" 分支。

于是首条 system 消息(content: ""、只有 sections 与 toolsAdded)先被迭代成空内容,再落进 toolResult 分支,被转换成一个没有 name 的 functionResponse。contents[0] 恰好就是它,因此每一个请求(含新会话首轮)都会被 Cloud Code Assist 以 400 拒绝。

症状

Error: Cloud Code Assist API error (400): {
  "error": {
    "code": 400,
    "message": "* GenerateContentRequest.contents[0].parts[0].function_response.name: Name cannot be empty.\n",
    "status": "INVALID_ARGUMENT"
  }
}

换提示词、换 antigravity 模型、新建 session 都一样,因为失败点在 contents[0],与用户输入无关。

版本边界(已验证)

pi-ai compat.stream() 对自定义 provider 的输入 适配层是否成立
≤ 0.85.1 原样透传 Context(systemPrompt / tools / messages) ✅
≥ 0.86.0 normalizeContext() 折成 { messages } ❌

对比 dist/compat.js 与 dist/api/google-shared.js:

0.85.1  compat.js normalizeContext: 0 命中   google-shared 系统消息处理: 0 命中
0.86.0  compat.js normalizeContext: 3 命中   google-shared 系统消息处理: 2 命中
0.87.0  compat.js normalizeContext: 3 命中   google-shared 系统消息处理: 2 命中

@earendil-works/pi-ai@0.85.1 的根入口也不导出 getCurrentSystemPrompt / getCurrentTools / collapseSystemMessages(0.86+ 才导出),因此修复不能直接假设这些 helper 可用。

最小复现(已验证)

Pi 0.86+ 实际传给 provider 的 context 形状(新会话首轮即可):

const context = {
  messages: [
    {
      role: "system",
      content: "",
      sections: { preamble: "...", tools: "...", rules: "...", /* ... */ },
      toolsAdded: [/* 工具声明 */],
      timestamp: 0,
    },
    { role: "user", content: [{ type: "text", text: "hi" }], timestamp: 1 },
  ],
};
// 注意:没有 context.systemPrompt,也没有 context.tools

convertMessages(model, context)[0];
// => { role: "user", parts: [{ functionResponse: { response: { output: "" } } }] }

对照:把同一条 transcript 去掉 system 消息、并把系统文本与工具声明放回 context.systemPrompt / context.tools(即 0.85.1 的形状),同一函数输出正常。

按 Pi 的真实链路(convertToLlm → normalizeContext → buildRequestBody)在一份真实 session 上重放,得到的请求体是:

request.systemInstruction: undefined
request.tools:             undefined
request.contents[0]:       {"role":"user","parts":[{"functionResponse":{"response":{"output":""}}}]}

而修复后的同一份 transcript(system 消息被正确 replay):

request.systemInstruction: {"role":"user","parts":[{"text":"You are an expert coding assistant ..."}]}
request.tools[0].functionDeclarations.length: 11
request.contents[0]:       {"role":"user","parts":[{"text":"..."}]}

影响面

  1. 完全不可用:antigravity 下所有模型、所有请求都 400,没有 provider 侧 workaround。
  2. 即使兜掉 400,请求仍是错的:system prompt 不会发出(systemInstruction 缺失),工具声明一个都不会发出(request.tools 缺失)。即模型既不知道自己的系统提示,也没有任何工具。
  3. 打破 feat(ai-providers): add Google Antigravity and Cursor OAuth providers #234 的验收项:/login google-antigravity 后「普通 Pi 工具回合可跑」目前不成立。
  4. Cursor provider 同源假设:extensions/ai-providers/cursor/provider.ts 同样读 context.systemPrompt / context.tools(buildSystemPrompt(context.systemPrompt, store, !!context.tools?.length) 等),需一并核对;本票先按 antigravity 的实测结论立据。

为什么现有测试与 CI 没有拦住

  • 测试仍按旧形状构造 context:tests/extensions/ai-providers/antigravity.test.ts 使用 { systemPrompt: "You are helpful.", messages: [{ role: "user", ... }], tools: [...] },从未覆盖 0.86+ 的首条 system 消息形状;全仓也没有任何一处引用 toolsAdded / getCurrentTools / collapseSystemMessages。
  • 开发依赖与 lock 固定在变更之前的版本:package.json devDependencies ^0.85.1,bun.lock 实际锁定 0.85.1。
  • CI 只验证锁定版本:.github/workflows/ci.yml:89-95 从 node_modules 读取 pi-ai / pi-coding-agent 版本再原样安装,peer 区间不进入任何验证。

也就是说:peerDependencies: ">=0.85.1" 让 Pi 0.86/0.87 成为合法但从未被验证的安装组合,而该区间内存在硬失败。这与 #328 中已记录的现象同类(09-04 评论里 testikun 指出的 fresh-install 漂移当时表现为 web 子进程退出),只是后果更严重。

Workaround

建议

路线 A(与基线解耦,建议先做):做版本无关的双形状适配 —— context.messages 中存在 system 消息时按 transcript replay 取系统文本与工具声明,否则回退 context.systemPrompt / context.tools;同时让 convertMessages 显式跳过非 user / assistant / toolResult 的角色,保证任何残留形状都不会再产出匿名 functionResponse。不引入 0.85.1 不存在的新 API,因此可在当前锁定基线上直接补 0.86+ 形状的回归测试。

路线 B:按 #410 的先例把支持基线抬到 ≥0.86,并改用上游 getCurrentSystemPrompt() / getCurrentTools() / collapseSystemMessages(),成套更新 manifest / lock / README / SETUP / CI / package-contract 测试与决策记录。

两条路线都需要在 PR 中显式说明与 #488 的关系:#488 正在改同一个 extensions/ai-providers/antigravity/google-conversion.ts 与同一个测试文件,需 rebase 或先合。

回归测试至少应覆盖:0.86+ 形状下(首条 system 消息 content: "" + sections + toolsAdded)不产生无 name 的 functionResponse、systemInstruction 非空、tools 声明数与 transcript 一致;以及 0.85.1 旧形状不回归。

与其它票的关系

验收

  • Pi 0.87.1 + antigravity,新会话首轮不再返回 function_response.name 400
  • systemInstruction 非空,且与 transcript 中的系统提示一致
  • request.tools 的声明数与 transcript 工具集合一致
  • 0.86+ transcript 形状进入回归测试;0.85.1 旧形状无回归
  • cursor provider 同源路径给出核对结论(修复或明确不受影响)
  • 与 chore(deps): define and enforce the supported Pi baseline #328 的基线策略保持一致(最低版本与锁定版本都有验证)

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions