Просмотр исходного кода

fix(proxy): handle extra signature field error in thinking signature rectifier

- Add detection for "signature: Extra inputs are not permitted" error
- Support third-party API channels that don't accept signature field
- Reuse existing rectifier logic for consistency
- Add unit tests for extra signature field detection

Co-Authored-By: Claude Opus 4.5 <[email protected]>
ding113 3 недель назад
Родитель
Сommit
63d8ed57b0

+ 24 - 0
src/app/v1/_lib/proxy/thinking-signature-rectifier.test.ts

@@ -66,6 +66,30 @@ describe("thinking-signature-rectifier", () => {
         "invalid_signature_in_thinking_block"
       );
     });
+
+    test("应命中:signature Extra inputs are not permitted(上游不支持 signature 字段)", () => {
+      // 从 Anthropic 官方渠道切换到第三方渠道时,历史 content block 包含 signature 字段
+      // 第三方渠道不支持该字段,返回 "Extra inputs are not permitted" 错误
+      expect(
+        detectThinkingSignatureRectifierTrigger(
+          "content.1.tool_use.signature: Extra inputs are not permitted"
+        )
+      ).toBe("invalid_signature_in_thinking_block");
+
+      // 完整错误消息格式(含 request id)
+      expect(
+        detectThinkingSignatureRectifierTrigger(
+          '{"error":{"type":"<nil>","message":"***.***content.1.tool_use.signature: Extra inputs are not permitted (request id: 20260122042750493345237oUastQMk)"}}'
+        )
+      ).toBe("invalid_signature_in_thinking_block");
+
+      // 大小写混合
+      expect(
+        detectThinkingSignatureRectifierTrigger(
+          "Signature: EXTRA INPUTS ARE NOT PERMITTED"
+        )
+      ).toBe("invalid_signature_in_thinking_block");
+    });
   });
 
   describe("rectifyAnthropicRequestMessage", () => {

+ 10 - 0
src/app/v1/_lib/proxy/thinking-signature-rectifier.ts

@@ -60,6 +60,16 @@ export function detectThinkingSignatureRectifierTrigger(
     return "invalid_signature_in_thinking_block"; // 复用现有触发类型,整流逻辑相同
   }
 
+  // 检测:signature 字段不被接受(上游 API 返回 "xxx.signature: Extra inputs are not permitted")
+  // 场景:请求体中存在 signature 字段但上游 API 不支持(如非 Anthropic 官方 API)
+  // 常见于从 Anthropic 官方渠道切换到第三方渠道时,历史 content block 包含 signature 字段
+  const looksLikeExtraSignatureField =
+    lower.includes("signature") && lower.includes("extra inputs are not permitted");
+
+  if (looksLikeExtraSignatureField) {
+    return "invalid_signature_in_thinking_block"; // 复用现有触发类型,整流逻辑相同
+  }
+
   // 与默认错误规则保持一致(Issue #432 / Rule 6)
   if (/非法请求|illegal request|invalid request/i.test(errorMessage)) {
     return "invalid_request";

+ 68 - 0
tests/unit/proxy/proxy-forwarder-thinking-signature-rectifier.test.ts

@@ -376,4 +376,72 @@ describe("ProxyForwarder - thinking signature rectifier", () => {
     expect(special).not.toBeNull();
     expect(JSON.stringify(special)).toContain("thinking_signature_rectifier");
   });
+
+  test("命中 signature Extra inputs not permitted 错误时应整流并对同供应商重试一次", async () => {
+    const session = createSession();
+    session.setProvider(createAnthropicProvider());
+
+    // 模拟包含 signature 字段的 tool_use content block
+    const msg = session.request.message as any;
+    msg.messages = [
+      {
+        role: "assistant",
+        content: [
+          { type: "text", text: "hello" },
+          {
+            type: "tool_use",
+            id: "toolu_1",
+            name: "WebSearch",
+            input: { query: "q" },
+            signature: "sig_tool_should_remove",
+          },
+        ],
+      },
+    ];
+
+    const doForward = vi.spyOn(ProxyForwarder as any, "doForward");
+
+    doForward.mockImplementationOnce(async () => {
+      throw new ProxyError(
+        "content.1.tool_use.signature: Extra inputs are not permitted",
+        400,
+        {
+          body: "",
+          providerId: 1,
+          providerName: "anthropic-1",
+        }
+      );
+    });
+
+    doForward.mockImplementationOnce(async (s: ProxySession) => {
+      const bodyMsg = s.request.message as any;
+      const blocks = bodyMsg.messages[0].content as any[];
+
+      // 验证 signature 字段已被移除
+      expect(blocks.some((b: any) => "signature" in b)).toBe(false);
+
+      const body = JSON.stringify({
+        type: "message",
+        content: [{ type: "text", text: "ok" }],
+      });
+
+      return new Response(body, {
+        status: 200,
+        headers: {
+          "content-type": "application/json",
+          "content-length": String(body.length),
+        },
+      });
+    });
+
+    const response = await ProxyForwarder.send(session);
+
+    expect(response.status).toBe(200);
+    expect(doForward).toHaveBeenCalledTimes(2);
+    expect(mocks.updateMessageRequestDetails).toHaveBeenCalledTimes(1);
+
+    const special = session.getSpecialSettings();
+    expect(special).not.toBeNull();
+    expect(JSON.stringify(special)).toContain("thinking_signature_rectifier");
+  });
 });