diff --git a/providers/openai/openai_test.go b/providers/openai/openai_test.go index f517e8e3f7553c589955a09a7f8aec8c4678627a..dde9080ae06890dd46d1026c2e7332fa53b01e66 100644 --- a/providers/openai/openai_test.go +++ b/providers/openai/openai_test.go @@ -3105,7 +3105,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 1, "should only have user message") require.Len(t, warnings, 1) @@ -3131,7 +3131,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 2, "should have both user and assistant messages") require.Empty(t, warnings) @@ -3159,7 +3159,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 2, "should have both user and assistant messages") require.Empty(t, warnings) @@ -3180,7 +3180,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Empty(t, input) require.Len(t, warnings, 2) // One for unsupported type, one for empty message @@ -3202,7 +3202,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 1) require.Empty(t, warnings) @@ -3223,7 +3223,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 1) require.Empty(t, warnings) @@ -3244,7 +3244,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 1) require.Empty(t, warnings) @@ -3874,7 +3874,7 @@ func TestResponsesToPrompt_WebSearchProviderExecutedToolResults(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system instructions") + input, warnings := toResponsesPrompt(prompt, "system instructions", false) require.Empty(t, warnings) @@ -3886,6 +3886,78 @@ func TestResponsesToPrompt_WebSearchProviderExecutedToolResults(t *testing.T) { "expected user + item_reference + assistant text") } +func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) { + t.Parallel() + + encryptedContent := "gAAAAABpvAwtDPh5dSXW86hwbwoTo4DJHANQ" + reasoningItemID := "rs_08d030b87966238b0069bc095b7e5c81" + + reasoningPart := fantasy.ReasoningPart{ + Text: "Let me think about this...", + ProviderOptions: fantasy.ProviderOptions{ + Name: &ResponsesReasoningMetadata{ + ItemID: reasoningItemID, + EncryptedContent: &encryptedContent, + Summary: []string{}, + }, + }, + } + + prompt := fantasy.Prompt{ + { + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{ + fantasy.TextPart{Text: "What is 2+2?"}, + }, + }, + { + Role: fantasy.MessageRoleAssistant, + Content: []fantasy.MessagePart{ + reasoningPart, + fantasy.TextPart{Text: "4"}, + }, + }, + { + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{ + fantasy.TextPart{Text: "And 3+3?"}, + }, + }, + } + + t.Run("store true skips reasoning", func(t *testing.T) { + t.Parallel() + + input, warnings := toResponsesPrompt(prompt, "system", true) + require.Empty(t, warnings) + + // With store=true: user, assistant text (reasoning + // skipped), follow-up user. + require.Len(t, input, 3) + + // Verify no reasoning item leaked through. + for _, item := range input { + require.Nil(t, item.OfReasoning, + "reasoning items must not appear when store=true") + } + }) + + t.Run("store false includes reasoning", func(t *testing.T) { + t.Parallel() + + input, warnings := toResponsesPrompt(prompt, "system", false) + require.Empty(t, warnings) + + // With store=false: user, reasoning, assistant text, + // follow-up user. + require.Len(t, input, 4) + + // Second item should be the reasoning. + require.NotNil(t, input[1].OfReasoning) + require.Equal(t, reasoningItemID, input[1].OfReasoning.ID) + }) +} + func TestResponsesStream_WebSearchResponse(t *testing.T) { t.Parallel() diff --git a/providers/openai/responses_language_model.go b/providers/openai/responses_language_model.go index dc11e5eb6aa6a1a84fc4123709e3307cafb7b07b..f24dd07c1a8ee48db026658da5b4ee2ffaf93a9d 100644 --- a/providers/openai/responses_language_model.go +++ b/providers/openai/responses_language_model.go @@ -176,7 +176,8 @@ func (o responsesLanguageModel) prepareParams(call fantasy.Call) (*responses.Res params.PreviousResponseID = param.NewOpt(*openaiOptions.PreviousResponseID) } - input, inputWarnings := toResponsesPrompt(call.Prompt, modelConfig.systemMessageMode) + storeEnabled := openaiOptions != nil && openaiOptions.Store != nil && *openaiOptions.Store + input, inputWarnings := toResponsesPrompt(call.Prompt, modelConfig.systemMessageMode, storeEnabled) warnings = append(warnings, inputWarnings...) var include []IncludeType @@ -391,7 +392,7 @@ func responsesUsage(resp responses.Response) fantasy.Usage { return usage } -func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string) (responses.ResponseInputParam, []fantasy.CallWarning) { +func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string, store bool) (responses.ResponseInputParam, []fantasy.CallWarning) { var input responses.ResponseInputParam var warnings []fantasy.CallWarning @@ -560,6 +561,16 @@ func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string) (respons // recognised Responses API input type; skip. continue case fantasy.ContentTypeReasoning: + if store { + // When Store is enabled the API already has the + // reasoning persisted server-side. Replaying the + // full OfReasoning item causes a validation error + // ("reasoning was provided without its required + // following item") because the API cannot pair the + // reconstructed reasoning with the output item + // that followed it. + continue + } reasoningMetadata := GetReasoningMetadata(c.Options()) if reasoningMetadata == nil || reasoningMetadata.ItemID == "" { continue