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
13 changes: 7 additions & 6 deletions providers/openai/openai_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4598,15 +4598,15 @@ func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) {
require.NoError(t, err)
require.Empty(t, warnings)

// With store=true the reasoning item is replayed as an
// item_reference so any following provider-executed item
// pairs correctly. Order: user, item_reference(rs_*),
// assistant text, user.
// Unfinalized metadata (stream placeholders or rows persisted
// before Finalized existed) falls back to an item_reference with
// store=true. Order: user, item_reference(rs_*), assistant text,
// user.
require.Len(t, input, 4)

for _, item := range input {
require.Nil(t, item.OfReasoning,
"reasoning items must not appear inline when store=true")
"unfinalized reasoning must not be replayed inline")
}

require.NotNil(t, input[1].OfItemReference,
Expand Down Expand Up @@ -4666,7 +4666,8 @@ func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) {
require.NoError(t, err)
require.Empty(t, warnings)

// With store=false: user, assistant text, follow-up user.
// Unfinalized metadata is skipped with store=false: user,
// assistant text, follow-up user.
require.Len(t, input, 3)

for _, item := range input {
Expand Down
96 changes: 63 additions & 33 deletions providers/openai/responses_language_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ func toResponsesPromptWithValidation(prompt fantasy.Prompt, systemMessageMode st

case fantasy.MessageRoleAssistant:
startIdx := len(input)
lastEmittedReasoningReference := false
canReferenceWebSearch := false
for _, c := range msg.Content {
switch c.GetType() {
case fantasy.ContentTypeText:
Expand All @@ -588,7 +588,7 @@ func toResponsesPromptWithValidation(prompt fantasy.Prompt, systemMessageMode st
message.OfMessage.Phase = responses.EasyInputMessagePhase(metadata.Phase)
}
input = append(input, message)
lastEmittedReasoningReference = false
canReferenceWebSearch = false

case fantasy.ContentTypeToolCall:
toolCallPart, ok := fantasy.AsContentType[fantasy.ToolCallPart](c)
Expand All @@ -601,12 +601,12 @@ func toResponsesPromptWithValidation(prompt fantasy.Prompt, systemMessageMode st
}

if toolCallPart.ProviderExecuted {
if store && lastEmittedReasoningReference &&
if store && canReferenceWebSearch &&
isResponsesWebSearchToolCall(toolCallPart) &&
toolCallPart.ToolCallID != "" {
input = append(input, responses.ResponseInputItemParamOfItemReference(toolCallPart.ToolCallID))
}
lastEmittedReasoningReference = false
canReferenceWebSearch = false
continue
}

Expand All @@ -621,18 +621,13 @@ func toResponsesPromptWithValidation(prompt fantasy.Prompt, systemMessageMode st
}

input = append(input, responses.ResponseInputItemParamOfFunctionCall(toolCallPart.Input, toolCallPart.ToolCallID, toolCallPart.ToolName))
lastEmittedReasoningReference = false
canReferenceWebSearch = false
case fantasy.ContentTypeSource:
// Source citations from web search are not a
// recognised Responses API input type; skip.
continue
case fantasy.ContentTypeReasoning:
lastEmittedReasoningReference = false
if !store {
// When store is disabled, server-side reasoning
// items are ephemeral and cannot be referenced.
continue
}
canReferenceWebSearch = false
reasoningPart, ok := fantasy.AsContentType[fantasy.ReasoningPart](c)
if !ok {
warnings = append(warnings, fantasy.CallWarning{
Expand All @@ -645,8 +640,17 @@ func toResponsesPromptWithValidation(prompt fantasy.Prompt, systemMessageMode st
if meta == nil || meta.ItemID == "" {
continue
}
input = append(input, responses.ResponseInputItemParamOfItemReference(meta.ItemID))
lastEmittedReasoningReference = true
if item, ok := responsesReasoningInputItem(meta); ok {
input = append(input, item)
} else if store {
input = append(input, responses.ResponseInputItemParamOfItemReference(meta.ItemID))
} else {
// Unstored reasoning without finalized encrypted
// content cannot be resolved by the API.
continue
}
// Item-reference replay predates source storage metadata.
canReferenceWebSearch = !meta.Finalized || meta.SourceStoreEnabled
Comment thread
ibetitsmike marked this conversation as resolved.
continue
}
}
Expand Down Expand Up @@ -812,6 +816,40 @@ func toResponsesPromptWithValidation(prompt fantasy.Prompt, systemMessageMode st
return input, warnings, nil
}

func responsesReasoningInputItem(meta *ResponsesReasoningMetadata) (responses.ResponseInputItemUnionParam, bool) {
if !meta.Finalized || meta.EncryptedContent == nil || *meta.EncryptedContent == "" {
return responses.ResponseInputItemUnionParam{}, false
}
summary := make([]responses.ResponseReasoningItemSummaryParam, 0, len(meta.Summary))
for _, text := range meta.Summary {
summary = append(summary, responses.ResponseReasoningItemSummaryParam{Text: text})
}
item := responses.ResponseInputItemParamOfReasoning(meta.ItemID, summary)
item.OfReasoning.EncryptedContent = param.NewOpt(*meta.EncryptedContent)
return item, true
}

func finalResponsesReasoningMetadata(item responses.ResponseOutputItemUnion, sourceStoreEnabled bool) *ResponsesReasoningMetadata {
metadata := &ResponsesReasoningMetadata{
ItemID: item.ID,
Summary: make([]string, 0, len(item.Summary)),
Finalized: true,
SourceStoreEnabled: sourceStoreEnabled,
}
if item.EncryptedContent != "" {
metadata.EncryptedContent = &item.EncryptedContent
}
for _, s := range item.Summary {
metadata.Summary = append(metadata.Summary, s.Text)
}
return metadata
}

// OpenAI echoes store=false for zero data retention organizations.
func effectiveResponseStore(requested bool, response *responses.Response) bool {
return requested && response.JSON.ExtraFields["store"].Raw() != "false"
}

func isResponsesWebSearchToolCall(toolCallPart fantasy.ToolCallPart) bool {
return toolCallPart.ToolName == "web_search" ||
toolCallPart.ToolName == "web_search_preview"
Expand Down Expand Up @@ -908,6 +946,10 @@ func validateResponsesFunctionCallOutputs(input responses.ResponseInputParam) er
func validateResponsesItemReferences(input responses.ResponseInputParam) error {
previousReferenceID := ""
for _, item := range input {
if item.OfReasoning != nil {
previousReferenceID = item.OfReasoning.ID
continue
}
if item.OfItemReference == nil {
previousReferenceID = ""
continue
Expand All @@ -929,7 +971,7 @@ func hasVisibleResponsesUserContent(content responses.ResponseInputMessageConten
func hasVisibleResponsesAssistantContent(items []responses.ResponseInputItemUnionParam, startIdx int) bool {
// Check if we added any assistant content parts from this message
for i := startIdx; i < len(items); i++ {
if items[i].OfMessage != nil || items[i].OfFunctionCall != nil || items[i].OfItemReference != nil || items[i].OfComputerCall != nil {
if items[i].OfMessage != nil || items[i].OfFunctionCall != nil || items[i].OfItemReference != nil || items[i].OfReasoning != nil || items[i].OfComputerCall != nil {
return true
}
}
Expand Down Expand Up @@ -1148,27 +1190,11 @@ func (o responsesLanguageModel) Generate(ctx context.Context, call fantasy.Call)
},
})
case "reasoning":
metadata := &ResponsesReasoningMetadata{
ItemID: outputItem.ID,
}
if outputItem.EncryptedContent != "" {
metadata.EncryptedContent = &outputItem.EncryptedContent
}

if len(outputItem.Summary) == 0 && metadata.EncryptedContent == nil {
metadata := finalResponsesReasoningMetadata(outputItem, effectiveResponseStore(params.Store.Value, response))
if len(metadata.Summary) == 0 && metadata.EncryptedContent == nil {
continue
}

// When there are no summary parts, add an empty reasoning part
summaries := outputItem.Summary
if len(summaries) == 0 {
summaries = []responses.ResponseReasoningItemSummary{{Type: "summary_text", Text: ""}}
}

for _, s := range summaries {
metadata.Summary = append(metadata.Summary, s.Text)
}

content = append(content, fantasy.ReasoningContent{
Text: strings.Join(metadata.Summary, "\n"),
ProviderMetadata: fantasy.ProviderMetadata{
Expand Down Expand Up @@ -1227,6 +1253,7 @@ func (o responsesLanguageModel) Stream(ctx context.Context, call fantasy.Call) (
ongoingToolCalls := make(map[int64]*ongoingToolCall)
hasFunctionCall := false
activeReasoning := make(map[string]*reasoningState)
sourceStoreEnabled := params.Store.Value

return func(yield func(fantasy.StreamPart) bool) {
if len(warnings) > 0 {
Expand All @@ -1245,6 +1272,7 @@ func (o responsesLanguageModel) Stream(ctx context.Context, call fantasy.Call) (
case "response.created":
created := event.AsResponseCreated()
responseID = created.Response.ID
sourceStoreEnabled = effectiveResponseStore(sourceStoreEnabled, &created.Response)

case "response.output_item.added":
added := event.AsResponseOutputItemAdded()
Expand Down Expand Up @@ -1417,11 +1445,13 @@ func (o responsesLanguageModel) Stream(ctx context.Context, call fantasy.Call) (
case "reasoning":
state := activeReasoning[done.Item.ID]
if state != nil {
// The completed item owns replay data; the added
// item can carry placeholder encrypted content.
if !yield(fantasy.StreamPart{
Type: fantasy.StreamPartTypeReasoningEnd,
ID: done.Item.ID,
ProviderMetadata: fantasy.ProviderMetadata{
Name: state.metadata,
Name: finalResponsesReasoningMetadata(done.Item, sourceStoreEnabled),
},
}) {
return
Expand Down
7 changes: 7 additions & 0 deletions providers/openai/responses_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ type ResponsesReasoningMetadata struct {
ItemID string `json:"item_id"`
EncryptedContent *string `json:"encrypted_content"`
Summary []string `json:"summary"`
// Finalized marks metadata copied from the completed reasoning output
// item. Only finalized encrypted content is replayed inline; streaming
// placeholders and metadata persisted before this field existed are not.
Finalized bool `json:"finalized,omitempty"`
// SourceStoreEnabled records the source request's store setting, so
// unstored search items are not referenced after storage is enabled.
SourceStoreEnabled bool `json:"source_store_enabled,omitempty"`
}

// Options implements the ProviderOptions interface.
Expand Down
Loading
Loading