Merge pull request #375 from CJackHwang/codex/investigate-data-loss-issue-in-pr-369

sse/parser: treat object-shaped `v` as visible content, preserve INCOMPLETE across omitted status; add tests and samples
This commit is contained in:
CJACK.
2026-04-30 02:14:26 +08:00
committed by GitHub
14 changed files with 10000 additions and 3705 deletions

View File

@@ -219,3 +219,33 @@ func (d failingOrCompletionDoer) Do(req *http.Request) (*http.Response, error) {
}
return nil, errors.New("forced stream failure")
}
func TestAutoContinuePreservesIncompleteStateWhenNextChunkOmitsStatus(t *testing.T) {
initialBody := strings.Join([]string{
`data: {"response_message_id":321,"v":{"response":{"message_id":321,"status":"INCOMPLETE"}}}`,
`data: {"p":"response/content","v":{"text":"continued"}}`,
`data: [DONE]`,
}, "\n") + "\n"
var continueCalls atomic.Int32
body := newAutoContinueBody(context.Background(), io.NopCloser(strings.NewReader(initialBody)), "session-123", 8, func(context.Context, string, int) (*http.Response, error) {
continueCalls.Add(1)
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(
`data: {"response_message_id":322,"p":"response/status","v":"FINISHED"}` + "\n" +
`data: [DONE]` + "\n",
)),
}, nil
})
defer func() { _ = body.Close() }()
_, err := io.ReadAll(body)
if err != nil {
t.Fatalf("read body failed: %v", err)
}
if continueCalls.Load() != 1 {
t.Fatalf("expected exactly one continue call, got %d", continueCalls.Load())
}
}

View File

@@ -2,7 +2,7 @@
"client": {
"name": "DeepSeek",
"platform": "android",
"version": "2.0.1",
"version": "2.0.3",
"android_api_level": "35",
"locale": "zh_CN"
},

View File

@@ -244,11 +244,29 @@ func appendChunkValueContent(v any, partType string, newType *string, parts *[]C
}
*parts = append(*parts, pp...)
case map[string]any:
if appendObjectContentByPath(path, val, partType, parts) {
return false
}
appendWrappedFragments(val, partType, newType, parts)
}
return false
}
func appendObjectContentByPath(path string, val map[string]any, partType string, parts *[]ContentPart) bool {
if path != "response/content" && path != "response/thinking_content" && path != "" {
return false
}
text, _ := val["text"].(string)
if text == "" {
text, _ = val["content"].(string)
}
if text == "" {
return false
}
appendContentPart(parts, text, partType)
return true
}
func appendWrappedFragments(val map[string]any, partType string, newType *string, parts *[]ContentPart) {
resp := val
if wrapped, ok := val["response"].(map[string]any); ok {

View File

@@ -163,3 +163,44 @@ func TestParseSSEChunkForContentStripsLeakedThinkTagsFromText(t *testing.T) {
t.Fatalf("expected leaked think tag to be stripped, got %#v", parts[0])
}
}
func TestParseSSEChunkForContentResponseContentObjectShape(t *testing.T) {
chunk := map[string]any{
"p": "response/content",
"v": map[string]any{"text": "对象内容"},
}
parts, finished, _ := ParseSSEChunkForContent(chunk, false, "text")
if finished {
t.Fatal("expected unfinished")
}
if len(parts) != 1 || parts[0].Text != "对象内容" || parts[0].Type != "text" {
t.Fatalf("unexpected parts: %#v", parts)
}
}
func TestParseSSEChunkForThinkingContentObjectShape(t *testing.T) {
chunk := map[string]any{
"p": "response/thinking_content",
"v": map[string]any{"content": "对象思考"},
}
parts, finished, _ := ParseSSEChunkForContent(chunk, true, "thinking")
if finished {
t.Fatal("expected unfinished")
}
if len(parts) != 1 || parts[0].Text != "对象思考" || parts[0].Type != "thinking" {
t.Fatalf("unexpected parts: %#v", parts)
}
}
func TestParseSSEChunkForContentObjectShapeWithoutPath(t *testing.T) {
chunk := map[string]any{
"v": map[string]any{"text": "无路径对象内容"},
}
parts, finished, _ := ParseSSEChunkForContent(chunk, false, "text")
if finished {
t.Fatal("expected unfinished")
}
if len(parts) != 1 || parts[0].Text != "无路径对象内容" || parts[0].Type != "text" {
t.Fatalf("unexpected parts: %#v", parts)
}
}