Skip to content

Commit b8ada63

Browse files
alfadbclaude
andcommitted
fix: strip empty text blocks in retry filter and fix error pattern matching
Empty text blocks ({"type":"text","text":""}) cause Anthropic upstream to return 400: "text content blocks must be non-empty". This was not caught by the existing error detection pattern in isThinkingBlockSignatureError, nor handled by FilterThinkingBlocksForRetry. - Add empty text block stripping to FilterThinkingBlocksForRetry - Fix isThinkingBlockSignatureError to match new Anthropic error format - Add fast-path byte patterns to avoid unnecessary JSON parsing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 21f349c commit b8ada63

3 files changed

Lines changed: 71 additions & 5 deletions

File tree

backend/internal/service/gateway_request.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ var (
2828
patternEmptyContentSpaced = []byte(`"content": []`)
2929
patternEmptyContentSp1 = []byte(`"content" : []`)
3030
patternEmptyContentSp2 = []byte(`"content" :[]`)
31+
32+
// Fast-path patterns for empty text blocks: {"type":"text","text":""}
33+
patternEmptyText = []byte(`"text":""`)
34+
patternEmptyTextSpaced = []byte(`"text": ""`)
3135
)
3236

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

236-
// Also check for empty content arrays that need fixing.
240+
// Also check for empty content arrays and empty text blocks that need fixing.
237241
// Note: This is a heuristic check; the actual empty content handling is done below.
238242
hasEmptyContent := bytes.Contains(body, patternEmptyContent) ||
239243
bytes.Contains(body, patternEmptyContentSpaced) ||
240244
bytes.Contains(body, patternEmptyContentSp1) ||
241245
bytes.Contains(body, patternEmptyContentSp2)
242246

247+
// Check for empty text blocks: {"type":"text","text":""}
248+
// These cause upstream 400: "text content blocks must be non-empty"
249+
hasEmptyTextBlock := bytes.Contains(body, patternEmptyText) ||
250+
bytes.Contains(body, patternEmptyTextSpaced)
251+
243252
// Fast path: nothing to process
244-
if !hasThinkingContent && !hasEmptyContent {
253+
if !hasThinkingContent && !hasEmptyContent && !hasEmptyTextBlock {
245254
return body
246255
}
247256

@@ -260,7 +269,7 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
260269
bytes.Contains(body, patternTypeRedactedThinking) ||
261270
bytes.Contains(body, patternTypeRedactedSpaced) ||
262271
bytes.Contains(body, patternThinkingFieldSpaced)
263-
if !hasEmptyContent && !containsThinkingBlocks {
272+
if !hasEmptyContent && !hasEmptyTextBlock && !containsThinkingBlocks {
264273
if topThinking := gjson.Get(jsonStr, "thinking"); topThinking.Exists() {
265274
if out, err := sjson.DeleteBytes(body, "thinking"); err == nil {
266275
out = removeThinkingDependentContextStrategies(out)
@@ -320,6 +329,16 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
320329

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

332+
// Strip empty text blocks: {"type":"text","text":""}
333+
// Upstream rejects these with 400: "text content blocks must be non-empty"
334+
if blockType == "text" {
335+
if txt, _ := blockMap["text"].(string); txt == "" {
336+
modifiedThisMsg = true
337+
ensureNewContent(bi)
338+
continue
339+
}
340+
}
341+
323342
// Convert thinking blocks to text (preserve content) and drop redacted_thinking.
324343
switch blockType {
325344
case "thinking":

backend/internal/service/gateway_request_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,51 @@ func TestFilterThinkingBlocksForRetry_EmptyContentGetsPlaceholder(t *testing.T)
404404
require.NotEmpty(t, content0["text"])
405405
}
406406

407+
func TestFilterThinkingBlocksForRetry_StripsEmptyTextBlocks(t *testing.T) {
408+
// Empty text blocks cause upstream 400: "text content blocks must be non-empty"
409+
input := []byte(`{
410+
"messages":[
411+
{"role":"user","content":[{"type":"text","text":"hello"},{"type":"text","text":""}]},
412+
{"role":"assistant","content":[{"type":"text","text":""}]}
413+
]
414+
}`)
415+
416+
out := FilterThinkingBlocksForRetry(input)
417+
418+
var req map[string]any
419+
require.NoError(t, json.Unmarshal(out, &req))
420+
msgs, ok := req["messages"].([]any)
421+
require.True(t, ok)
422+
423+
// First message: empty text block stripped, "hello" preserved
424+
msg0 := msgs[0].(map[string]any)
425+
content0 := msg0["content"].([]any)
426+
require.Len(t, content0, 1)
427+
require.Equal(t, "hello", content0[0].(map[string]any)["text"])
428+
429+
// Second message: only had empty text block → gets placeholder
430+
msg1 := msgs[1].(map[string]any)
431+
content1 := msg1["content"].([]any)
432+
require.Len(t, content1, 1)
433+
block1 := content1[0].(map[string]any)
434+
require.Equal(t, "text", block1["type"])
435+
require.NotEmpty(t, block1["text"])
436+
}
437+
438+
func TestFilterThinkingBlocksForRetry_PreservesNonEmptyTextBlocks(t *testing.T) {
439+
// Non-empty text blocks should pass through unchanged
440+
input := []byte(`{
441+
"messages":[
442+
{"role":"user","content":[{"type":"text","text":"hello"},{"type":"text","text":"world"}]}
443+
]
444+
}`)
445+
446+
out := FilterThinkingBlocksForRetry(input)
447+
448+
// Fast path: no thinking content, no empty content, no empty text blocks → unchanged
449+
require.Equal(t, input, out)
450+
}
451+
407452
func TestFilterSignatureSensitiveBlocksForRetry_DowngradesTools(t *testing.T) {
408453
input := []byte(`{
409454
"thinking":{"type":"enabled","budget_tokens":1024},

backend/internal/service/gateway_service.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6067,9 +6067,11 @@ func (s *GatewayService) isThinkingBlockSignatureError(respBody []byte) bool {
60676067
return true
60686068
}
60696069

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

0 commit comments

Comments
 (0)