mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
Merge pull request #1117 from alfadb/fix/empty-text-block-retry
fix: 修复空 text block 导致上游 400 错误未被重试捕获的问题
This commit is contained in:
@@ -28,6 +28,12 @@ var (
|
|||||||
patternEmptyContentSpaced = []byte(`"content": []`)
|
patternEmptyContentSpaced = []byte(`"content": []`)
|
||||||
patternEmptyContentSp1 = []byte(`"content" : []`)
|
patternEmptyContentSp1 = []byte(`"content" : []`)
|
||||||
patternEmptyContentSp2 = []byte(`"content" :[]`)
|
patternEmptyContentSp2 = []byte(`"content" :[]`)
|
||||||
|
|
||||||
|
// Fast-path patterns for empty text blocks: {"type":"text","text":""}
|
||||||
|
patternEmptyText = []byte(`"text":""`)
|
||||||
|
patternEmptyTextSpaced = []byte(`"text": ""`)
|
||||||
|
patternEmptyTextSp1 = []byte(`"text" : ""`)
|
||||||
|
patternEmptyTextSp2 = []byte(`"text" :""`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// SessionContext 粘性会话上下文,用于区分不同来源的请求。
|
// SessionContext 粘性会话上下文,用于区分不同来源的请求。
|
||||||
@@ -233,15 +239,22 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
|
|||||||
bytes.Contains(body, patternThinkingField) ||
|
bytes.Contains(body, patternThinkingField) ||
|
||||||
bytes.Contains(body, patternThinkingFieldSpaced)
|
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.
|
// Note: This is a heuristic check; the actual empty content handling is done below.
|
||||||
hasEmptyContent := bytes.Contains(body, patternEmptyContent) ||
|
hasEmptyContent := bytes.Contains(body, patternEmptyContent) ||
|
||||||
bytes.Contains(body, patternEmptyContentSpaced) ||
|
bytes.Contains(body, patternEmptyContentSpaced) ||
|
||||||
bytes.Contains(body, patternEmptyContentSp1) ||
|
bytes.Contains(body, patternEmptyContentSp1) ||
|
||||||
bytes.Contains(body, patternEmptyContentSp2)
|
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) ||
|
||||||
|
bytes.Contains(body, patternEmptyTextSp1) ||
|
||||||
|
bytes.Contains(body, patternEmptyTextSp2)
|
||||||
|
|
||||||
// Fast path: nothing to process
|
// Fast path: nothing to process
|
||||||
if !hasThinkingContent && !hasEmptyContent {
|
if !hasThinkingContent && !hasEmptyContent && !hasEmptyTextBlock {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +273,7 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
|
|||||||
bytes.Contains(body, patternTypeRedactedThinking) ||
|
bytes.Contains(body, patternTypeRedactedThinking) ||
|
||||||
bytes.Contains(body, patternTypeRedactedSpaced) ||
|
bytes.Contains(body, patternTypeRedactedSpaced) ||
|
||||||
bytes.Contains(body, patternThinkingFieldSpaced)
|
bytes.Contains(body, patternThinkingFieldSpaced)
|
||||||
if !hasEmptyContent && !containsThinkingBlocks {
|
if !hasEmptyContent && !hasEmptyTextBlock && !containsThinkingBlocks {
|
||||||
if topThinking := gjson.Get(jsonStr, "thinking"); topThinking.Exists() {
|
if topThinking := gjson.Get(jsonStr, "thinking"); topThinking.Exists() {
|
||||||
if out, err := sjson.DeleteBytes(body, "thinking"); err == nil {
|
if out, err := sjson.DeleteBytes(body, "thinking"); err == nil {
|
||||||
out = removeThinkingDependentContextStrategies(out)
|
out = removeThinkingDependentContextStrategies(out)
|
||||||
@@ -320,6 +333,16 @@ func FilterThinkingBlocksForRetry(body []byte) []byte {
|
|||||||
|
|
||||||
blockType, _ := blockMap["type"].(string)
|
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.
|
// Convert thinking blocks to text (preserve content) and drop redacted_thinking.
|
||||||
switch blockType {
|
switch blockType {
|
||||||
case "thinking":
|
case "thinking":
|
||||||
|
|||||||
@@ -404,6 +404,51 @@ func TestFilterThinkingBlocksForRetry_EmptyContentGetsPlaceholder(t *testing.T)
|
|||||||
require.NotEmpty(t, content0["text"])
|
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) {
|
func TestFilterSignatureSensitiveBlocksForRetry_DowngradesTools(t *testing.T) {
|
||||||
input := []byte(`{
|
input := []byte(`{
|
||||||
"thinking":{"type":"enabled","budget_tokens":1024},
|
"thinking":{"type":"enabled","budget_tokens":1024},
|
||||||
|
|||||||
@@ -6067,9 +6067,11 @@ func (s *GatewayService) isThinkingBlockSignatureError(respBody []byte) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测空消息内容错误(可能是过滤 thinking blocks 后导致的)
|
// 检测空消息内容错误(可能是过滤 thinking blocks 后导致的,或客户端发送了空 text block)
|
||||||
// 例如: "all messages must have non-empty content"
|
// 例如: "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, "content blocks must be non-empty") {
|
||||||
logger.LegacyPrintf("service.gateway", "[SignatureCheck] Detected empty content error")
|
logger.LegacyPrintf("service.gateway", "[SignatureCheck] Detected empty content error")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user