Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions backend/internal/service/gateway_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ var (
patternEmptyContentSpaced = []byte(`"content": []`)
patternEmptyContentSp1 = []byte(`"content" : []`)
patternEmptyContentSp2 = []byte(`"content" :[]`)

// Fast-path patterns for empty text blocks: {"type":"text","text":""}
patternEmptyText = []byte(`"text":""`)
patternEmptyTextSpaced = []byte(`"text": ""`)
)

// SessionContext 粘性会话上下文,用于区分不同来源的请求。
Expand Down Expand Up @@ -233,15 +237,20 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
bytes.Contains(body, patternThinkingField) ||
bytes.Contains(body, patternThinkingFieldSpaced)

// Also check for empty content arrays that need fixing.
// Also check for empty content arrays and empty text blocks that need fixing.
// Note: This is a heuristic check; the actual empty content handling is done below.
hasEmptyContent := bytes.Contains(body, patternEmptyContent) ||
bytes.Contains(body, patternEmptyContentSpaced) ||
bytes.Contains(body, patternEmptyContentSp1) ||
bytes.Contains(body, patternEmptyContentSp2)

// Check for empty text blocks: {"type":"text","text":""}
// These cause upstream 400: "text content blocks must be non-empty"
hasEmptyTextBlock := bytes.Contains(body, patternEmptyText) ||
bytes.Contains(body, patternEmptyTextSpaced)
Comment on lines +247 to +250
Comment on lines +249 to +250

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle empty text blocks with all valid JSON spacing

The new empty-text fast path only checks "text":"" and "text": "", so payloads that are still valid JSON like "text" : "" are missed. In requests without thinking blocks or content: [], hasEmptyTextBlock stays false and the function returns early, which means the retry path resends the same invalid empty text block and still gets a 400. This makes the fix ineffective for pretty-printed clients that include spaces around the colon.

Useful? React with 👍 / 👎.


// Fast path: nothing to process
if !hasThinkingContent && !hasEmptyContent {
if !hasThinkingContent && !hasEmptyContent && !hasEmptyTextBlock {
return body
}

Expand All @@ -260,7 +269,7 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
bytes.Contains(body, patternTypeRedactedThinking) ||
bytes.Contains(body, patternTypeRedactedSpaced) ||
bytes.Contains(body, patternThinkingFieldSpaced)
if !hasEmptyContent && !containsThinkingBlocks {
if !hasEmptyContent && !hasEmptyTextBlock && !containsThinkingBlocks {
if topThinking := gjson.Get(jsonStr, "thinking"); topThinking.Exists() {
if out, err := sjson.DeleteBytes(body, "thinking"); err == nil {
out = removeThinkingDependentContextStrategies(out)
Expand Down Expand Up @@ -320,6 +329,16 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {

blockType, _ := blockMap["type"].(string)

// Strip empty text blocks: {"type":"text","text":""}
// Upstream rejects these with 400: "text content blocks must be non-empty"
if blockType == "text" {
if txt, _ := blockMap["text"].(string); txt == "" {
modifiedThisMsg = true
ensureNewContent(bi)
continue
}
}

// Convert thinking blocks to text (preserve content) and drop redacted_thinking.
switch blockType {
case "thinking":
Expand Down
45 changes: 45 additions & 0 deletions backend/internal/service/gateway_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,51 @@ func TestFilterThinkingBlocksForRetry_EmptyContentGetsPlaceholder(t *testing.T)
require.NotEmpty(t, content0["text"])
}

func TestFilterThinkingBlocksForRetry_StripsEmptyTextBlocks(t *testing.T) {
// Empty text blocks cause upstream 400: "text content blocks must be non-empty"
input := []byte(`{
"messages":[
{"role":"user","content":[{"type":"text","text":"hello"},{"type":"text","text":""}]},
{"role":"assistant","content":[{"type":"text","text":""}]}
]
}`)

out := FilterThinkingBlocksForRetry(input)

var req map[string]any
require.NoError(t, json.Unmarshal(out, &req))
msgs, ok := req["messages"].([]any)
require.True(t, ok)

// First message: empty text block stripped, "hello" preserved
msg0 := msgs[0].(map[string]any)
content0 := msg0["content"].([]any)
require.Len(t, content0, 1)
require.Equal(t, "hello", content0[0].(map[string]any)["text"])

// Second message: only had empty text block → gets placeholder
msg1 := msgs[1].(map[string]any)
content1 := msg1["content"].([]any)
require.Len(t, content1, 1)
block1 := content1[0].(map[string]any)
require.Equal(t, "text", block1["type"])
require.NotEmpty(t, block1["text"])
}

func TestFilterThinkingBlocksForRetry_PreservesNonEmptyTextBlocks(t *testing.T) {
// Non-empty text blocks should pass through unchanged
input := []byte(`{
"messages":[
{"role":"user","content":[{"type":"text","text":"hello"},{"type":"text","text":"world"}]}
]
}`)

out := FilterThinkingBlocksForRetry(input)

// Fast path: no thinking content, no empty content, no empty text blocks → unchanged
require.Equal(t, input, out)
}

func TestFilterSignatureSensitiveBlocksForRetry_DowngradesTools(t *testing.T) {
input := []byte(`{
"thinking":{"type":"enabled","budget_tokens":1024},
Expand Down
6 changes: 4 additions & 2 deletions backend/internal/service/gateway_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6067,9 +6067,11 @@ func (s *GatewayService) isThinkingBlockSignatureError(respBody []byte) bool {
return true
}

// 检测空消息内容错误(可能是过滤 thinking blocks 后导致的)
// 检测空消息内容错误(可能是过滤 thinking blocks 后导致的,或客户端发送了空 text block
// 例如: "all messages must have non-empty content"
if strings.Contains(msg, "non-empty content") || strings.Contains(msg, "empty content") {
// "messages: text content blocks must be non-empty"
if strings.Contains(msg, "non-empty content") || strings.Contains(msg, "empty content") ||
strings.Contains(msg, "must be non-empty") {
logger.LegacyPrintf("service.gateway", "[SignatureCheck] Detected empty content error")
return true
}
Expand Down
Loading