diff --git a/core/http/endpoints/openresponses/responses.go b/core/http/endpoints/openresponses/responses.go index 09f3b82e8..28eb6433a 100644 --- a/core/http/endpoints/openresponses/responses.go +++ b/core/http/endpoints/openresponses/responses.go @@ -1332,6 +1332,7 @@ func handleBackgroundStream(ctx context.Context, store *ResponseStore, responseI // bufferEvent stores an SSE event in the response store for streaming resume func bufferEvent(store *ResponseStore, responseID string, event *schema.ORStreamEvent) { + normalizeORStreamEvent(event) if err := store.AppendEvent(responseID, event); err != nil { xlog.Error("Failed to buffer event", "response_id", responseID, "error", err) } @@ -2605,6 +2606,7 @@ func handleOpenResponsesStream(c echo.Context, responseID string, createdAt int6 // sendSSEEvent sends a Server-Sent Event func sendSSEEvent(c echo.Context, event *schema.ORStreamEvent) { + normalizeORStreamEvent(event) data, err := json.Marshal(event) if err != nil { xlog.Error("Failed to marshal SSE event", "error", err) @@ -2613,6 +2615,13 @@ func sendSSEEvent(c echo.Context, event *schema.ORStreamEvent) { fmt.Fprintf(c.Response().Writer, "event: %s\ndata: %s\n\n", event.Type, string(data)) } +// normalizeORStreamEvent ensures required fields like Summary are never null. +func normalizeORStreamEvent(event *schema.ORStreamEvent) { + if event.Item != nil && event.Item.Summary == nil { + event.Item.Summary = []schema.ORContentPart{} + } +} + // getTopLogprobs returns the top_logprobs value, defaulting to 0 if nil func getTopLogprobs(topLogprobs *int) int { if topLogprobs != nil { @@ -2693,6 +2702,13 @@ func buildORResponse(responseID string, createdAt int64, completedAt *int64, sta outputItems = []schema.ORItemField{} } + // Ensure Summary is never null on any output item + for i := range outputItems { + if outputItems[i].Summary == nil { + outputItems[i].Summary = []schema.ORContentPart{} + } + } + // Ensure tools is never null - always an array tools := input.Tools if tools == nil { diff --git a/core/schema/openresponses.go b/core/schema/openresponses.go index be010f030..09f8360bd 100644 --- a/core/schema/openresponses.go +++ b/core/schema/openresponses.go @@ -86,7 +86,7 @@ type ORReasoningParam struct { // ORItemParam represents an input/output item (discriminated union by type) type ORItemParam struct { Type string `json:"type"` // message|function_call|function_call_output|reasoning|item_reference - ID string `json:"id,omitempty"` // Present for all output items + ID string `json:"id"` // Present for all output items Status string `json:"status,omitempty"` // in_progress|completed|incomplete // Message fields @@ -102,8 +102,8 @@ type ORItemParam struct { Output interface{} `json:"output,omitempty"` // string or []ORContentPart // Reasoning fields (for type == "reasoning") - Summary []ORContentPart `json:"summary,omitempty"` // Array of summary parts - EncryptedContent *string `json:"encrypted_content,omitempty"` // Provider-specific encrypted content + Summary []ORContentPart `json:"summary"` // Array of summary parts + EncryptedContent *string `json:"encrypted_content,omitempty"` // Provider-specific encrypted content // Note: For item_reference type, use the ID field above to reference the item // Note: For reasoning type, Content field (from message fields) contains the raw reasoning content