diff --git a/providers/openai/openai_test.go b/providers/openai/openai_test.go index dde9080ae06890dd46d1026c2e7332fa53b01e66..f89a6b9d43b405c6d20aef485b57aa5629f606ae 100644 --- a/providers/openai/openai_test.go +++ b/providers/openai/openai_test.go @@ -3874,16 +3874,29 @@ func TestResponsesToPrompt_WebSearchProviderExecutedToolResults(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system instructions", false) + t.Run("store false skips item reference", func(t *testing.T) { + t.Parallel() + + input, warnings := toResponsesPrompt(prompt, "system instructions", false) + + require.Empty(t, warnings) + require.Len(t, input, 2, + "expected user + assistant text when store=false") + require.Nil(t, input[0].OfItemReference) + require.Nil(t, input[1].OfItemReference) + }) + + t.Run("store true uses item reference", func(t *testing.T) { + t.Parallel() - require.Empty(t, warnings) + input, warnings := toResponsesPrompt(prompt, "system instructions", true) - // Expected input items: user message, item_reference (for - // provider-executed tool call; the ToolResultPart is skipped), - // and assistant text message. System instructions are passed - // via params.Instructions, not as an input item. - require.Len(t, input, 3, - "expected user + item_reference + assistant text") + require.Empty(t, warnings) + require.Len(t, input, 3, + "expected user + item_reference + assistant text when store=true") + require.NotNil(t, input[1].OfItemReference) + require.Equal(t, "ws_01", input[1].OfItemReference.ID) + }) } func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) { @@ -3942,19 +3955,19 @@ func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) { } }) - t.Run("store false includes reasoning", func(t *testing.T) { + t.Run("store false skips 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) + // With store=false: user, assistant text, follow-up user. + require.Len(t, input, 3) - // Second item should be the reasoning. - require.NotNil(t, input[1].OfReasoning) - require.Equal(t, reasoningItemID, input[1].OfReasoning.ID) + for _, item := range input { + require.Nil(t, item.OfReasoning, + "reasoning items must not appear when store=false") + } }) } diff --git a/providers/openai/responses_language_model.go b/providers/openai/responses_language_model.go index 5f22735f7a6c84fd55ba7d8d979a3ced617d66e0..1fa9af515bf1426599ea5069af741addec26c2c8 100644 --- a/providers/openai/responses_language_model.go +++ b/providers/openai/responses_language_model.go @@ -537,10 +537,16 @@ func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string, store bo } if toolCallPart.ProviderExecuted { - // Round-trip provider-executed tools via - // item_reference, letting the API resolve - // the stored output item by ID. - input = append(input, responses.ResponseInputItemParamOfItemReference(toolCallPart.ToolCallID)) + if store { + // Round-trip provider-executed tools via + // item_reference, letting the API resolve + // the stored output item by ID. + input = append(input, responses.ResponseInputItemParamOfItemReference(toolCallPart.ToolCallID)) + } + // When store is disabled, server-side items are + // ephemeral and cannot be referenced. Skip the + // tool call; results are already omitted for + // provider-executed tools. continue } @@ -559,45 +565,13 @@ func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string, store bo // 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 - } - if len(reasoningMetadata.Summary) == 0 && reasoningMetadata.EncryptedContent == nil { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: "assistant message reasoning part does is empty", - }) - continue - } - // we want to always send an empty array - summary := make([]responses.ResponseReasoningItemSummaryParam, 0, len(reasoningMetadata.Summary)) - for _, s := range reasoningMetadata.Summary { - summary = append(summary, responses.ResponseReasoningItemSummaryParam{ - Type: "summary_text", - Text: s, - }) - } - reasoning := &responses.ResponseReasoningItemParam{ - ID: reasoningMetadata.ItemID, - Summary: summary, - } - if reasoningMetadata.EncryptedContent != nil { - reasoning.EncryptedContent = param.NewOpt(*reasoningMetadata.EncryptedContent) - } - input = append(input, responses.ResponseInputItemUnionParam{ - OfReasoning: reasoning, - }) + // Reasoning items are always skipped during replay. + // When store is enabled, the API already has them + // persisted server-side. When store is disabled, the + // item IDs are ephemeral and referencing them causes + // "Item not found" errors. In both cases, replaying + // reasoning inline is not supported by the API. + continue } }