Merge pull request #7869 from 3219378872/fix/thinking-signature-key

fix(apicompat): always include `signature` on Anthropic thinking blocks
This commit is contained in:
Wesley Liddick
2026-10-08 11:27:14 +08:00
committed by GitHub
6 changed files with 75 additions and 8 deletions
@@ -122,6 +122,20 @@ type ClaudeContentItem struct {
Input any `json:"input,omitempty"`
}
// MarshalJSON 保证 thinking 块始终带 thinking/signature 字段(即使为空),
// 与 Anthropic 官方一致;Grok Build 等严格客户端缺少 signature 会解析失败。
func (c ClaudeContentItem) MarshalJSON() ([]byte, error) {
type claudeContentItem ClaudeContentItem
if c.Type != "thinking" {
return json.Marshal(claudeContentItem(c))
}
return json.Marshal(struct {
Thinking string `json:"thinking"`
Signature string `json:"signature"`
claudeContentItem
}{Thinking: c.Thinking, Signature: c.Signature, claudeContentItem: claudeContentItem(c)})
}
// ClaudeUsage Claude 用量统计
type ClaudeUsage struct {
InputTokens int `json:"input_tokens"`
@@ -1,6 +1,9 @@
package antigravity
import "testing"
import (
"encoding/json"
"testing"
)
func TestDefaultModels_ContainsNewAndLegacyImageModels(t *testing.T) {
t.Parallel()
@@ -44,3 +47,23 @@ func TestDefaultModels_ContainsNewAndLegacyImageModels(t *testing.T) {
}
}
}
func TestClaudeContentItem_ThinkingAlwaysHasSignature(t *testing.T) {
t.Parallel()
data, err := json.Marshal(ClaudeContentItem{Type: "thinking", Thinking: "plan"})
if err != nil {
t.Fatal(err)
}
if got, want := string(data), `{"thinking":"plan","signature":"","type":"thinking"}`; got != want {
t.Fatalf("thinking block = %s, want %s", got, want)
}
data, err = json.Marshal(ClaudeContentItem{Type: "text", Text: "hi"})
if err != nil {
t.Fatal(err)
}
if got, want := string(data), `{"type":"text","text":"hi"}`; got != want {
t.Fatalf("text block = %s, want %s", got, want)
}
}
@@ -296,8 +296,9 @@ func (p *StreamingProcessor) processThinking(text, signature string) []byte {
// 开始或继续 thinking 块
if p.blockType != BlockTypeThinking {
_, _ = result.Write(p.startBlock(BlockTypeThinking, map[string]any{
"type": "thinking",
"thinking": "",
"type": "thinking",
"thinking": "",
"signature": "",
}))
}
@@ -475,8 +476,9 @@ func (p *StreamingProcessor) emitEmptyThinkingWithSignature(signature string) []
var result bytes.Buffer
_, _ = result.Write(p.startBlock(BlockTypeThinking, map[string]any{
"type": "thinking",
"thinking": "",
"type": "thinking",
"thinking": "",
"signature": "",
}))
_, _ = result.Write(p.emitDelta("thinking_delta", map[string]any{
"thinking": "",
@@ -844,7 +844,7 @@ func TestStreamingReasoning(t *testing.T) {
sse, err := ResponsesAnthropicEventToSSE(events[0])
require.NoError(t, err)
assert.Contains(t, sse, `"content_block":{"thinking":"","type":"thinking"}`)
assert.Contains(t, sse, `"content_block":{"thinking":"","signature":"","type":"thinking"}`)
// reasoning text delta
events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
@@ -0,0 +1,24 @@
package apicompat
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
// Thinking blocks must always carry the `signature` key (Anthropic wire shape);
// strict clients such as Grok Build fail with "missing field `signature`".
func TestAnthropicContentBlock_ThinkingAlwaysHasSignature(t *testing.T) {
data, err := json.Marshal(AnthropicContentBlock{Type: "thinking", Thinking: "plan"})
require.NoError(t, err)
require.JSONEq(t, `{"type":"thinking","thinking":"plan","signature":""}`, string(data))
data, err = json.Marshal(AnthropicContentBlock{Type: "thinking", Signature: "sig"})
require.NoError(t, err)
require.JSONEq(t, `{"type":"thinking","thinking":"","signature":"sig"}`, string(data))
data, err = json.Marshal(AnthropicContentBlock{Type: "text", Text: "hi"})
require.NoError(t, err)
require.JSONEq(t, `{"type":"text","text":"hi"}`, string(data))
}
+6 -2
View File
@@ -95,10 +95,14 @@ func (b AnthropicContentBlock) MarshalJSON() ([]byte, error) {
anthropicContentBlock
}{Text: b.Text, anthropicContentBlock: anthropicContentBlock(b)})
case "thinking":
// Anthropic always sends `signature` on thinking blocks (empty on
// content_block_start); strict clients such as Grok Build reject the
// block with "missing field `signature`" when the key is absent.
return json.Marshal(struct {
Thinking string `json:"thinking"`
Thinking string `json:"thinking"`
Signature string `json:"signature"`
anthropicContentBlock
}{Thinking: b.Thinking, anthropicContentBlock: anthropicContentBlock(b)})
}{Thinking: b.Thinking, Signature: b.Signature, anthropicContentBlock: anthropicContentBlock(b)})
default:
return json.Marshal(base)
}