mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
Merge pull request #7869 from 3219378872/fix/thinking-signature-key
fix(apicompat): always include `signature` on Anthropic thinking blocks
This commit is contained in:
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user