Skip to content

Commit e7780cd

Browse files
authored
Merge pull request Wei-Shaw#1117 from alfadb/fix/empty-text-block-retry
fix: 修复空 text block 导致上游 400 错误未被重试捕获的问题
2 parents 62a566e + 7d26b81 commit e7780cd

3 files changed

Lines changed: 75 additions & 5 deletions

File tree

backend/internal/service/gateway_request.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ 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": ""`)
35+
patternEmptyTextSp1 = []byte(`"text" : ""`)
36+
patternEmptyTextSp2 = []byte(`"text" :""`)
3137
)
3238

3339
// SessionContext 粘性会话上下文,用于区分不同来源的请求。
@@ -233,15 +239,22 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
233239
bytes.Contains(body, patternThinkingField) ||
234240
bytes.Contains(body, patternThinkingFieldSpaced)
235241

236-
// Also check for empty content arrays that need fixing.
242+
// Also check for empty content arrays and empty text blocks that need fixing.
237243
// Note: This is a heuristic check; the actual empty content handling is done below.
238244
hasEmptyContent := bytes.Contains(body, patternEmptyContent) ||
239245
bytes.Contains(body, patternEmptyContentSpaced) ||
240246
bytes.Contains(body, patternEmptyContentSp1) ||
241247
bytes.Contains(body, patternEmptyContentSp2)
242248

249+
// Check for empty text blocks: {"type":"text","text":""}
250+
// These cause upstream 400: "text content blocks must be non-empty"
251+
hasEmptyTextBlock := bytes.Contains(body, patternEmptyText) ||
252+
bytes.Contains(body, patternEmptyTextSpaced) ||
253+
bytes.Contains(body, patternEmptyTextSp1) ||
254+
bytes.Contains(body, patternEmptyTextSp2)
255+
243256
// Fast path: nothing to process
244-
if !hasThinkingContent && !hasEmptyContent {
257+
if !hasThinkingContent && !hasEmptyContent && !hasEmptyTextBlock {
245258
return body
246259
}
247260

@@ -260,7 +273,7 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
260273
bytes.Contains(body, patternTypeRedactedThinking) ||
261274
bytes.Contains(body, patternTypeRedactedSpaced) ||
262275
bytes.Contains(body, patternThinkingFieldSpaced)
263-
if !hasEmptyContent && !containsThinkingBlocks {
276+
if !hasEmptyContent && !hasEmptyTextBlock && !containsThinkingBlocks {
264277
if topThinking := gjson.Get(jsonStr, "thinking"); topThinking.Exists() {
265278
if out, err := sjson.DeleteBytes(body, "thinking"); err == nil {
266279
out = removeThinkingDependentContextStrategies(out)
@@ -320,6 +333,16 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
320333

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

336+
// Strip empty text blocks: {"type":"text","text":""}
337+
// Upstream rejects these with 400: "text content blocks must be non-empty"
338+
if blockType == "text" {
339+
if txt, _ := blockMap["text"].(string); txt == "" {
340+
modifiedThisMsg = true
341+
ensureNewContent(bi)
342+
continue
343+
}
344+
}
345+
323346
// Convert thinking blocks to text (preserve content) and drop redacted_thinking.
324347
switch blockType {
325348
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, "content blocks must be non-empty") {
60736075
logger.LegacyPrintf("service.gateway", "[SignatureCheck] Detected empty content error")
60746076
return true
60756077
}

0 commit comments

Comments
 (0)