Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions core/schemas/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,7 @@ type ResponsesMessage struct {
ID *string `json:"id,omitempty"` // Common ID field for most item types
Type *ResponsesMessageType `json:"type,omitempty"`
Status *string `json:"status,omitempty"` // "in_progress" | "completed" | "incomplete" | "interpreting" | "failed"
Phase *string `json:"phase,omitempty"` // OpenAI emits this for multi-phase message items, e.g. "final_answer"

Role *ResponsesMessageRoleType `json:"role,omitempty"`
Content *ResponsesMessageContent `json:"content,omitempty"`
Expand Down Expand Up @@ -2475,16 +2476,18 @@ type BifrostResponsesStreamResponse struct {

Response *BifrostResponsesResponse `json:"response,omitempty"`

OutputIndex *int `json:"output_index,omitempty"`
Item *ResponsesMessage `json:"item"`
OutputIndex *int `json:"output_index,omitempty"`
Item *ResponsesMessage `json:"item"`
SummaryIndex *int `json:"summary_index,omitempty"`

ContentIndex *int `json:"content_index,omitempty"`
ItemID *string `json:"item_id,omitempty"`
Part *ResponsesMessageContentBlock `json:"part,omitempty"`

Delta *string `json:"delta,omitempty"`
Signature *string `json:"signature,omitempty"` // Not in OpenAI's spec, but sent by other providers
LogProbs []ResponsesOutputMessageContentTextLogProb `json:"logprobs"`
Delta *string `json:"delta,omitempty"`
Signature *string `json:"signature,omitempty"` // Not in OpenAI's spec, but sent by other providers
Obfuscation *string `json:"obfuscation,omitempty"`
LogProbs []ResponsesOutputMessageContentTextLogProb `json:"logprobs"`

Text *string `json:"text,omitempty"` // Full text of the output item, comes with event "response.output_text.done"

Expand Down Expand Up @@ -2531,11 +2534,13 @@ func (resp *BifrostResponsesStreamResponse) WithDefaults() *BifrostResponsesStre
// Copy all streaming-specific fields
result.OutputIndex = resp.OutputIndex
result.Item = resp.Item
result.SummaryIndex = resp.SummaryIndex
result.ContentIndex = resp.ContentIndex
result.ItemID = resp.ItemID
result.Part = resp.Part
result.Delta = resp.Delta
result.Signature = resp.Signature
result.Obfuscation = resp.Obfuscation
result.Text = resp.Text
result.Refusal = resp.Refusal
result.Arguments = resp.Arguments
Expand Down
73 changes: 73 additions & 0 deletions core/schemas/responses_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package schemas

import (
"strings"
"testing"
)

func TestBifrostResponsesStreamResponsePreservesOpenAIStreamMetadata(t *testing.T) {
raw := []byte(`{"type":"response.reasoning_summary_text.delta","delta":"thinking","item_id":"rs_123","obfuscation":"opaque","output_index":0,"sequence_number":4,"summary_index":0}`)

var resp BifrostResponsesStreamResponse
if err := Unmarshal(raw, &resp); err != nil {
t.Fatalf("unmarshal response stream chunk: %v", err)
}

if resp.SummaryIndex == nil || *resp.SummaryIndex != 0 {
t.Fatalf("expected summary_index to survive unmarshal, got %#v", resp.SummaryIndex)
}
if resp.Obfuscation == nil || *resp.Obfuscation != "opaque" {
t.Fatalf("expected obfuscation to survive unmarshal, got %#v", resp.Obfuscation)
}

defaulted := resp.WithDefaults()
if defaulted.SummaryIndex == nil || *defaulted.SummaryIndex != 0 {
t.Fatalf("expected summary_index to survive WithDefaults, got %#v", defaulted.SummaryIndex)
}
if defaulted.Obfuscation == nil || *defaulted.Obfuscation != "opaque" {
t.Fatalf("expected obfuscation to survive WithDefaults, got %#v", defaulted.Obfuscation)
}

encoded, err := MarshalSorted(defaulted)
if err != nil {
t.Fatalf("marshal defaulted response stream chunk: %v", err)
}
if !strings.Contains(string(encoded), `"summary_index":0`) {
t.Fatalf("expected encoded chunk to contain summary_index, got %s", encoded)
}
if !strings.Contains(string(encoded), `"obfuscation":"opaque"`) {
t.Fatalf("expected encoded chunk to contain obfuscation, got %s", encoded)
}

encodedChunk, err := MarshalSorted(BifrostStreamChunk{BifrostResponsesStreamResponse: defaulted})
if err != nil {
t.Fatalf("marshal response stream chunk wrapper: %v", err)
}
if !strings.Contains(string(encodedChunk), `"summary_index":0`) {
t.Fatalf("expected encoded stream chunk to contain summary_index, got %s", encodedChunk)
}
if !strings.Contains(string(encodedChunk), `"obfuscation":"opaque"`) {
t.Fatalf("expected encoded stream chunk to contain obfuscation, got %s", encodedChunk)
}
}

func TestResponsesMessagePreservesOpenAIPhase(t *testing.T) {
raw := []byte(`{"id":"msg_123","type":"message","status":"in_progress","content":[],"phase":"final_answer","role":"assistant"}`)

var msg ResponsesMessage
if err := Unmarshal(raw, &msg); err != nil {
t.Fatalf("unmarshal responses message: %v", err)
}

if msg.Phase == nil || *msg.Phase != "final_answer" {
t.Fatalf("expected phase to survive unmarshal, got %#v", msg.Phase)
}

encoded, err := MarshalSorted(msg)
if err != nil {
t.Fatalf("marshal responses message: %v", err)
}
if !strings.Contains(string(encoded), `"phase":"final_answer"`) {
t.Fatalf("expected encoded message to contain phase, got %s", encoded)
}
}
25 changes: 25 additions & 0 deletions framework/streaming/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ func deepCopyResponsesStreamResponse(original *schemas.BifrostResponsesStreamRes
copy.OutputIndex = &copyOutputIndex
}

if original.SummaryIndex != nil {
copySummaryIndex := *original.SummaryIndex
copy.SummaryIndex = &copySummaryIndex
}

if original.Item != nil {
copyItem := deepCopyResponsesMessage(*original.Item)
copy.Item = &copyItem
Expand All @@ -76,6 +81,16 @@ func deepCopyResponsesStreamResponse(original *schemas.BifrostResponsesStreamRes
copy.Delta = &copyDelta
}

if original.Signature != nil {
copySignature := *original.Signature
copy.Signature = &copySignature
}

if original.Obfuscation != nil {
copyObfuscation := *original.Obfuscation
copy.Obfuscation = &copyObfuscation
}

// Deep copy LogProbs slice if present
if original.LogProbs != nil {
copy.LogProbs = make([]schemas.ResponsesOutputMessageContentTextLogProb, len(original.LogProbs))
Expand Down Expand Up @@ -173,6 +188,16 @@ func deepCopyResponsesMessage(original schemas.ResponsesMessage) schemas.Respons
copy.Type = &copyType
}

if original.Status != nil {
copyStatus := *original.Status
copy.Status = &copyStatus
}

if original.Phase != nil {
copyPhase := *original.Phase
copy.Phase = &copyPhase
}

if original.Role != nil {
copyRole := *original.Role
copy.Role = &copyRole
Expand Down