diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index 1a5caa57c6..c8ba3788da 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -90,8 +90,8 @@ en: b6bd570c0159ba08 zh: 7d2d91eea6a8d830 /deepseek-ai-dsh-llm-pi-ai/known-limitations-and-deferred-work: - en: ccaea557a3a3ffaf - zh: 5f70d824d39fb457 + en: a8b6c192acf56f83 + zh: bb6b3ce40425342f /deepseek-ai-dsh-llm-pi-ai/known-limitations-and-deferred-work/dev-note: en: 3678dd0f6642c2a5 zh: 75cbdd064cc64282 diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index c30a414465..0297de30ac 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -258,6 +258,7 @@ These limits define where the adapter stops and future work begins. They are cur - **Provider HTTP status is unavailable** — pi-ai error events do not expose a stable HTTP status across providers. - **Retry policy is provider-owned, not an SDK retry** — pi-ai SDK retries stay disabled so durable agent steps and `llm/retry` events own every visible attempt, and direct `ctx.llm.stream()` calls remain single-attempt. - **Streamed JSON tool-call arguments avoid per-delta reparsing** — the installed pi-ai carries [`patches/@earendil-works__pi-ai@1.3.0.patch`](../../../patches/@earendil-works__pi-ai@1.3.0.patch), which removes the per-delta re-parse of the whole accumulated argument JSON in every stream adapter (upstream [earendil-works/pi#9265](https://github.com/earendil-works/pi/issues/9265)); unpatched, a multi-megabyte argument stream costs O(n²) CPU on the event loop and stalls every session in the process. Completion events supply parsed arguments; Responses may parse at both argument completion and output-item completion. This adapter reads only delta strings and finalized arguments. Re-apply or retire the patch on every pi-ai upgrade. +- **Gemini tool-call thought signatures are patched into Chat Completions** — the same patch reads `tool_calls[].extra_content.google.thought_signature` from a streamed OpenAI-compatible response into the tool call's replay signature and sends it back as `extra_content` when the same model resumes (upstream [earendil-works/pi#10157](https://github.com/earendil-works/pi/issues/10157)); unpatched, Gemini rejects the next step of a tool-calling turn with HTTP 400. Encrypted OpenRouter reasoning details keep their `reasoning_details` replay. ### Dev Note diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index 0d7d5c22fe..f6bf299397 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -258,6 +258,7 @@ pi-ai 事件变成 harness 的推理、文本、工具调用、用量与 finish - **提供方 HTTP 状态不可用**——pi-ai 错误事件不跨提供方暴露稳定 HTTP 状态。 - **重试策略由提供方自有,而非 SDK 重试**——pi-ai SDK 重试保持禁用,因此持久 agent(智能体)步骤与 `llm/retry` 事件拥有每个可见尝试,直接 `ctx.llm.stream()` 调用仍是单次尝试。 - **流式 JSON 工具调用参数避免逐 delta 重复解析**——安装的 pi-ai 带有 [`patches/@earendil-works__pi-ai@1.3.0.patch`](../../../patches/@earendil-works__pi-ai@1.3.0.patch),它移除了每个流适配器中对整段累计参数 JSON 的逐 delta 重新解析(上游 [earendil-works/pi#9265](https://github.com/earendil-works/pi/issues/9265));未打补丁时,数 MB 的参数流会在事件循环上消耗 O(n²) CPU,并使进程内所有会话停滞。完成事件提供解析后的参数;Responses 可能在参数完成和输出项完成时分别解析。本适配器只读取 delta 字符串与最终参数。每次升级 pi-ai 时都要重新应用或撤销该补丁。 +- **Chat Completions 中的 Gemini 工具调用思考签名由补丁保留**——同一补丁把 OpenAI 兼容流式响应里的 `tool_calls[].extra_content.google.thought_signature` 读入工具调用的重放签名,并在同一模型继续时以 `extra_content` 发回(上游 [earendil-works/pi#10157](https://github.com/earendil-works/pi/issues/10157));未打补丁时,Gemini 会以 HTTP 400 拒绝工具调用轮次的下一步。OpenRouter 的加密推理详情仍通过 `reasoning_details` 重放。 ### 开发备注 diff --git a/packages/llm/llm-pi-ai/tests/protocol-roundtrip.spec.ts b/packages/llm/llm-pi-ai/tests/protocol-roundtrip.spec.ts index c8e4ec4ba9..f3daac2842 100644 --- a/packages/llm/llm-pi-ai/tests/protocol-roundtrip.spec.ts +++ b/packages/llm/llm-pi-ai/tests/protocol-roundtrip.spec.ts @@ -66,7 +66,11 @@ function frames(api: Api, tools = false): (string | ReturnType ({ index, id: `call_${index}`, type: 'function', function: { name: 'lookup', arguments: JSON.stringify({ key: index }) } })), + tool_calls: [0, 1].map(index => ({ + index, id: `call_${index}`, type: 'function', function: { name: 'lookup', arguments: JSON.stringify({ key: index }) }, + // Gemini's OpenAI-compatible endpoints sign only the first call of each step. + ...index === 0 ? { extra_content: { google: { thought_signature: 'gemini-signature' } } } : {}, + })), } : {}, }, finish_reason: null }] }), JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: tools ? 'tool_calls' : 'stop' }], usage: { prompt_tokens: 3, completion_tokens: 1 } }), @@ -167,6 +171,10 @@ describe('protocol history round trips', () => { } else if (a === 'openai-responses') { expect(JSON.stringify(baseline)).toContain('encrypted-reasoning') expect(JSON.stringify(baseline)).toContain('fc_0') + } else { + const calls = baseline.flatMap(message => (message as { tool_calls?: unknown[] }).tool_calls ?? []) + expect(calls).toMatchObject([{ extra_content: { google: { thought_signature: 'gemini-signature' } } }, {}]) + expect(calls[1]).not.toHaveProperty('extra_content') } const foreign = await assemble(ctx, { provider: targetProvider, model: 'model', messages: history, tools: [tool] }) expect(foreign.finish).toEqual({ kind: 'stop' }) @@ -175,6 +183,7 @@ describe('protocol history round trips', () => { expect(JSON.stringify(target.requests[0])).not.toContain('encrypted-reasoning') expect(JSON.stringify(target.requests[0])).not.toContain('encrypted-redacted-reasoning') expect(JSON.stringify(target.requests[0])).not.toContain('signed-reasoning') + expect(JSON.stringify(target.requests[0])).not.toContain('gemini-signature') history.push(foreign.message, createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: 'Continue with the original model.' }] })) history = JSON.parse(JSON.stringify(history)) as Message[] expect(history.slice(0, -2)).toEqual(JSON.parse(saved)) diff --git a/patches/@earendil-works__pi-ai@1.3.0.patch b/patches/@earendil-works__pi-ai@1.3.0.patch index fa81bf4837..111e09dcee 100644 --- a/patches/@earendil-works__pi-ai@1.3.0.patch +++ b/patches/@earendil-works__pi-ai@1.3.0.patch @@ -1,8 +1,8 @@ diff --git a/dist/api/anthropic-messages.js b/dist/api/anthropic-messages.js -index befb18b2e7d519b3731f6355921de1cb7892c7ac..7b7da85be215d3f91235ef4c32d4aa0f512ea6a5 100644 +index befb18b..7b7da85 100644 --- a/dist/api/anthropic-messages.js +++ b/dist/api/anthropic-messages.js -@@ -588,7 +588,6 @@ +@@ -588,7 +588,6 @@ export const stream = (model, context, options) => { const block = blocks[index]; if (block && block.type === "toolCall") { block.partialJson += event.delta.partial_json; @@ -11,10 +11,10 @@ index befb18b2e7d519b3731f6355921de1cb7892c7ac..7b7da85be215d3f91235ef4c32d4aa0f type: "toolcall_delta", contentIndex: index, diff --git a/dist/api/bedrock-converse-stream.js b/dist/api/bedrock-converse-stream.js -index 6ec62d045eab17f1298c39962cbada2073cb1bd3..abe5eeee081f608be3a5eb7c866684e807c0521c 100644 +index 6ec62d0..abe5eee 100644 --- a/dist/api/bedrock-converse-stream.js +++ b/dist/api/bedrock-converse-stream.js -@@ -461,7 +461,6 @@ +@@ -461,7 +461,6 @@ function handleContentBlockDelta(event, blocks, output, stream) { } else if (delta?.toolUse && block?.type === "toolCall") { block.partialJson = (block.partialJson || "") + (delta.toolUse.input || ""); @@ -23,10 +23,10 @@ index 6ec62d045eab17f1298c39962cbada2073cb1bd3..abe5eeee081f608be3a5eb7c866684e8 } else if (delta?.reasoningContent) { diff --git a/dist/api/mistral-conversations.js b/dist/api/mistral-conversations.js -index 9038a1c498e0ea0d305fcfdc946455e72714683b..afda5ea6c3bed6abee984d0cb308a769636d01a7 100644 +index 9038a1c..afda5ea 100644 --- a/dist/api/mistral-conversations.js +++ b/dist/api/mistral-conversations.js -@@ -570,7 +570,6 @@ +@@ -570,7 +570,6 @@ async function consumeChatStream(model, output, stream, mistralStream, onProvide ? toolCall.function.arguments : JSON.stringify(toolCall.function.arguments || {}); block.partialArgs = (block.partialArgs || "") + argsDelta; @@ -35,10 +35,19 @@ index 9038a1c498e0ea0d305fcfdc946455e72714683b..afda5ea6c3bed6abee984d0cb308a769 type: "toolcall_delta", contentIndex: toolBlocksByKey.get(key), diff --git a/dist/api/openai-completions.js b/dist/api/openai-completions.js -index 0eeaffa968628593cf6e9f8648650149a97ed55b..fdafb4cfdee8a65bbef86cd1c3a06317c1d87857 100644 +index 0eeaffa..2e13492 100644 --- a/dist/api/openai-completions.js +++ b/dist/api/openai-completions.js -@@ -453,7 +453,6 @@ +@@ -449,11 +449,15 @@ export const stream = (model, context, options) => { + if (!block.name && name) { + block.name = name; + } ++ // Gemini OpenAI-compatible endpoints require this signature on replay. ++ const geminiThoughtSignature = toolCall.extra_content?.google?.thought_signature; ++ if (typeof geminiThoughtSignature === "string" && geminiThoughtSignature.length > 0) { ++ block.thoughtSignature = geminiThoughtSignature; ++ } + let delta = ""; if (toolCall.function?.arguments) { delta = toolCall.function.arguments; block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments; @@ -46,11 +55,29 @@ index 0eeaffa968628593cf6e9f8648650149a97ed55b..fdafb4cfdee8a65bbef86cd1c3a06317 } else if (toolCall.custom?.input) { const nextInput = getCustomToolCallInput(block) + toolCall.custom.input; +@@ -1033,6 +1037,9 @@ export function convertMessages(model, context, compat, options) { + }, + }; + } ++ const geminiThoughtSignature = tc.thoughtSignature && !parseLegacyEncryptedReasoningDetail(tc.thoughtSignature) ++ ? tc.thoughtSignature ++ : undefined; + return { + id: tc.id, + type: "function", +@@ -1040,6 +1047,7 @@ export function convertMessages(model, context, compat, options) { + name: tc.name, + arguments: JSON.stringify(tc.arguments), + }, ++ ...(geminiThoughtSignature && { extra_content: { google: { thought_signature: geminiThoughtSignature } } }), + }; + }); + } diff --git a/dist/api/openai-responses-shared.js b/dist/api/openai-responses-shared.js -index af5f042285334542b9b39239cd9ea540916a1c32..b9f00fbe3a2ba4e7dfabfe4bcca3c54cb2d55f9f 100644 +index af5f042..b9f00fb 100644 --- a/dist/api/openai-responses-shared.js +++ b/dist/api/openai-responses-shared.js -@@ -561,7 +561,6 @@ +@@ -561,7 +561,6 @@ export async function processResponsesStream(openaiStream, output, stream, model if (!slot || slot.block.partialJson === undefined) continue; slot.block.partialJson += event.delta; @@ -59,10 +86,10 @@ index af5f042285334542b9b39239cd9ea540916a1c32..b9f00fbe3a2ba4e7dfabfe4bcca3c54c } else if (event.type === "response.function_call_arguments.done") { diff --git a/dist/api/pi-messages.js b/dist/api/pi-messages.js -index 7da1c45de8a106a68411cf79bec75f553534a1c7..296d131b59b1cf75f2516c39e85bba72972b2c17 100644 +index 7da1c45..296d131 100644 --- a/dist/api/pi-messages.js +++ b/dist/api/pi-messages.js -@@ -155,8 +155,6 @@ +@@ -155,8 +155,6 @@ function createEventConverter(model) { case "toolcall_delta": { const json = `${toolJson.get(event.contentIndex) ?? ""}${event.delta}`; toolJson.set(event.contentIndex, json); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d9a662ef9..a0e9073e23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,7 +11,7 @@ overrides: patchedDependencies: '@botiverse/oar@0.67.0': 65de2ca1966421e30893d8d4fed6a3d88e4247e131e7575fe791246e594538d6 - '@earendil-works/pi-ai@1.3.0': 97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378 + '@earendil-works/pi-ai@1.3.0': 4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765 '@electron/osx-sign@1.3.3': 280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861 '@fortune-sheet/core@1.0.4': 8791a230763dbf853fd65d627c68c31de37209992851377855e16cf08ea70532 '@fortune-sheet/react@1.0.4': 2932e52e79afcd4ced08cfa123b5eded786c85043da32f622f3e80af6fc96c37 @@ -9174,7 +9174,7 @@ importers: version: link:../../../vendor/schemastery '@earendil-works/pi-ai': specifier: ^1.3.0 - version: 1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + version: 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) '@earendil-works/pi-coding-agent': specifier: ^1.3.0 version: 1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.4.3) @@ -11303,7 +11303,7 @@ importers: version: link:../../../vendor/schemastery '@earendil-works/pi-ai': specifier: ^1.3.0 - version: 1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) + version: 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -24447,7 +24447,7 @@ snapshots: '@botiverse/oar@0.67.0(patch_hash=65de2ca1966421e30893d8d4fed6a3d88e4247e131e7575fe791246e594538d6)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.4.3)': dependencies: '@agentclientprotocol/sdk': 1.4.0(zod@4.4.3) - '@earendil-works/pi-ai': 1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-ai': 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) '@earendil-works/pi-coding-agent': 1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.4.3) cross-spawn: 7.0.6 undici: 8.10.2 @@ -24578,7 +24578,7 @@ snapshots: '@earendil-works/pi-agent-core@1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-ai': 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) typebox: 1.3.27 transitivePeerDependencies: - '@aws-sdk/credential-provider-node' @@ -24593,7 +24593,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-ai@1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.129.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1127.0 @@ -24618,7 +24618,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-ai@1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.129.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1127.0 @@ -24650,7 +24650,7 @@ snapshots: '@earendil-works/pi-coding-agent@1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.4.3)': dependencies: '@earendil-works/pi-agent-core': 1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) - '@earendil-works/pi-ai': 1.3.0(patch_hash=97749fcd291996bcd92ab46822f9c410ee5162b1b52a4d87b3f48b1a681a1378)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-ai': 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) '@earendil-works/pi-codemode': 1.3.0 '@earendil-works/pi-mcp': 1.3.0 '@earendil-works/pi-tui': 1.3.0