From c97bea32af50b8cda74658a90eeb71316c94f1a3 Mon Sep 17 00:00:00 2001 From: nghodki Date: Fri, 19 Jun 2026 19:44:51 -0700 Subject: [PATCH 1/3] fix(schemas): add ExtraContent field to ChatStreamResponseChoiceDelta Preserve provider-specific metadata on streaming delta chunks. Some OpenAI-compatible providers (e.g. Google's Gemini via OpenAI endpoint) return extra_content on delta objects to convey thinking markers (extra_content.google.thought: true) and encrypted thought signatures (extra_content.google.thought_signature) needed for multi-turn continuation with extended thinking enabled. Without this field, any proxy/gateway layer that deserializes streaming chunks through Bifrost's typed structs silently drops extra_content, breaking downstream consumers that rely on thought_signature for multi-turn Gemini conversations. Co-Authored-By: Claude Opus 4.6 --- core/schemas/chatcompletions.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/schemas/chatcompletions.go b/core/schemas/chatcompletions.go index 0296dd04ce..f35b516469 100644 --- a/core/schemas/chatcompletions.go +++ b/core/schemas/chatcompletions.go @@ -1547,13 +1547,14 @@ type ChatStreamResponseChoice struct { // ChatStreamResponseChoiceDelta represents a delta in the stream response type ChatStreamResponseChoiceDelta struct { - Role *string `json:"role,omitempty"` // Only in the first chunk - Content *string `json:"content,omitempty"` // May be empty string or null - Refusal *string `json:"refusal,omitempty"` // Refusal content if any - Audio *ChatAudioMessageAudio `json:"audio,omitempty"` // Audio data if any - Reasoning *string `json:"reasoning,omitempty"` // May be empty string or null + Role *string `json:"role,omitempty"` // Only in the first chunk + Content *string `json:"content,omitempty"` // May be empty string or null + Refusal *string `json:"refusal,omitempty"` // Refusal content if any + Audio *ChatAudioMessageAudio `json:"audio,omitempty"` // Audio data if any + Reasoning *string `json:"reasoning,omitempty"` // May be empty string or null ReasoningDetails []ChatReasoningDetails `json:"reasoning_details,omitempty"` - ToolCalls []ChatAssistantMessageToolCall `json:"tool_calls,omitempty"` // If tool calls used (supports incremental updates) + ToolCalls []ChatAssistantMessageToolCall `json:"tool_calls,omitempty"` // If tool calls used (supports incremental updates) + ExtraContent json.RawMessage `json:"extra_content,omitempty"` // Provider-specific metadata (e.g. Gemini thought markers, thought_signature) } // UnmarshalJSON implements custom unmarshalling for ChatStreamResponseChoiceDelta. From eb23a866bff52327ac2ef50434061248bb0c8d7b Mon Sep 17 00:00:00 2001 From: nghodki Date: Sat, 20 Jun 2026 09:19:12 -0700 Subject: [PATCH 2/3] fix(schemas): deep copy ExtraContent and ReasoningDetails in DeepCopyChatMessage DeepCopyChatMessage was not copying: - ChatAssistantMessageToolCall.ExtraContent (thought_signature) - ChatAssistantMessage.ReasoningDetails (thinking content) This could cause shared mutation between plugin accumulators when processing Gemini responses with extended thinking enabled. Co-Authored-By: Claude Opus 4.6 --- core/schemas/utils.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/core/schemas/utils.go b/core/schemas/utils.go index 89452e90bc..79e4e963be 100644 --- a/core/schemas/utils.go +++ b/core/schemas/utils.go @@ -760,9 +760,44 @@ func DeepCopyChatMessage(original ChatMessage) ChatMessage { copyName := *toolCall.Function.Name copyToolCall.Function.Name = ©Name } + if len(toolCall.ExtraContent) > 0 { + copyToolCall.ExtraContent = append(json.RawMessage(nil), toolCall.ExtraContent...) + } copy.ChatAssistantMessage.ToolCalls[i] = copyToolCall } } + + // Deep copy ReasoningDetails + if original.ChatAssistantMessage.ReasoningDetails != nil { + copy.ChatAssistantMessage.ReasoningDetails = make([]ChatReasoningDetails, len(original.ChatAssistantMessage.ReasoningDetails)) + for i, rd := range original.ChatAssistantMessage.ReasoningDetails { + copyRD := ChatReasoningDetails{ + Index: rd.Index, + Type: rd.Type, + } + if rd.ID != nil { + copyID := *rd.ID + copyRD.ID = ©ID + } + if rd.Summary != nil { + copySummary := *rd.Summary + copyRD.Summary = ©Summary + } + if rd.Text != nil { + copyText := *rd.Text + copyRD.Text = ©Text + } + if rd.Signature != nil { + copySig := *rd.Signature + copyRD.Signature = ©Sig + } + if rd.Data != nil { + copyData := *rd.Data + copyRD.Data = ©Data + } + copy.ChatAssistantMessage.ReasoningDetails[i] = copyRD + } + } } return copy From 89d577e215f04c6b742c534a07fcbdc9898369a2 Mon Sep 17 00:00:00 2001 From: nghodki Date: Sat, 20 Jun 2026 09:32:03 -0700 Subject: [PATCH 3/3] fix(jsonparser): deep copy ExtraContent and ReasoningDetails in delta copy The jsonparser plugin's deepCopyChatStreamResponseChoiceDelta was missing ExtraContent (provider metadata like google.thought_signature) and ReasoningDetails/Audio fields. Add them to prevent shared mutation between plugin accumulators when processing Gemini thinking responses. Co-Authored-By: Claude Opus 4.6 --- plugins/jsonparser/utils.go | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/plugins/jsonparser/utils.go b/plugins/jsonparser/utils.go index 158eedd016..0dbc9bf39e 100644 --- a/plugins/jsonparser/utils.go +++ b/plugins/jsonparser/utils.go @@ -335,6 +335,39 @@ func (p *JsonParserPlugin) deepCopyChatStreamResponseChoiceDelta(original *schem Reasoning: original.Reasoning, // Shallow copy Refusal: original.Refusal, // Shallow copy ToolCalls: original.ToolCalls, // Shallow copy - we don't modify tool calls + Audio: original.Audio, // Shallow copy + } + + // Deep copy ReasoningDetails — elements contain pointer fields + if len(original.ReasoningDetails) > 0 { + result.ReasoningDetails = make([]schemas.ChatReasoningDetails, len(original.ReasoningDetails)) + for i, rd := range original.ReasoningDetails { + copyRD := schemas.ChatReasoningDetails{ + Index: rd.Index, + Type: rd.Type, + } + if rd.ID != nil { + v := *rd.ID + copyRD.ID = &v + } + if rd.Summary != nil { + v := *rd.Summary + copyRD.Summary = &v + } + if rd.Text != nil { + v := *rd.Text + copyRD.Text = &v + } + if rd.Signature != nil { + v := *rd.Signature + copyRD.Signature = &v + } + if rd.Data != nil { + v := *rd.Data + copyRD.Data = &v + } + result.ReasoningDetails[i] = copyRD + } } // Deep copy Content pointer if it exists (this is what we modify) @@ -343,5 +376,10 @@ func (p *JsonParserPlugin) deepCopyChatStreamResponseChoiceDelta(original *schem result.Content = &contentCopy } + // Copy ExtraContent (provider metadata like google.thought / thought_signature) + if len(original.ExtraContent) > 0 { + result.ExtraContent = append(json.RawMessage(nil), original.ExtraContent...) + } + return result }