responses_params_test.go

  1package openai
  2
  3import (
  4	"encoding/json"
  5	"testing"
  6
  7	"charm.land/fantasy"
  8	"github.com/stretchr/testify/require"
  9)
 10
 11func TestPrepareParams_Store(t *testing.T) {
 12	t.Parallel()
 13
 14	lm := testResponsesLM()
 15	prompt := fantasy.Prompt{testTextMessage(fantasy.MessageRoleUser, "hello")}
 16
 17	tests := []struct {
 18		name      string
 19		opts      *ResponsesProviderOptions
 20		wantStore bool
 21	}{
 22		{
 23			name:      "store true",
 24			opts:      &ResponsesProviderOptions{Store: fantasy.Opt(true)},
 25			wantStore: true,
 26		},
 27		{
 28			name:      "store false",
 29			opts:      &ResponsesProviderOptions{Store: fantasy.Opt(false)},
 30			wantStore: false,
 31		},
 32		{
 33			name:      "store default",
 34			opts:      &ResponsesProviderOptions{},
 35			wantStore: false,
 36		},
 37		{
 38			name:      "no provider options",
 39			opts:      nil,
 40			wantStore: false,
 41		},
 42	}
 43
 44	for _, tt := range tests {
 45		tt := tt
 46		t.Run(tt.name, func(t *testing.T) {
 47			t.Parallel()
 48
 49			params, warnings, err := lm.prepareParams(testCall(prompt, tt.opts))
 50			require.NoError(t, err)
 51			require.Empty(t, warnings)
 52			require.True(t, params.Store.Valid())
 53			require.Equal(t, tt.wantStore, params.Store.Value)
 54		})
 55	}
 56}
 57
 58func TestPrepareParams_PreviousResponseID(t *testing.T) {
 59	t.Parallel()
 60
 61	lm := testResponsesLM()
 62	prompt := fantasy.Prompt{testTextMessage(fantasy.MessageRoleUser, "hello")}
 63
 64	t.Run("forwarded", func(t *testing.T) {
 65		t.Parallel()
 66
 67		params, warnings, err := lm.prepareParams(testCall(prompt, &ResponsesProviderOptions{
 68			PreviousResponseID: fantasy.Opt("resp_abc123"),
 69			Store:              fantasy.Opt(true),
 70		}))
 71		require.NoError(t, err)
 72		require.Empty(t, warnings)
 73		require.True(t, params.PreviousResponseID.Valid())
 74		require.Equal(t, "resp_abc123", params.PreviousResponseID.Value)
 75	})
 76
 77	t.Run("not set", func(t *testing.T) {
 78		t.Parallel()
 79
 80		params, warnings, err := lm.prepareParams(testCall(prompt, &ResponsesProviderOptions{}))
 81		require.NoError(t, err)
 82		require.Empty(t, warnings)
 83		require.False(t, params.PreviousResponseID.Valid())
 84	})
 85
 86	t.Run("empty string ignored", func(t *testing.T) {
 87		t.Parallel()
 88
 89		params, warnings, err := lm.prepareParams(testCall(prompt, &ResponsesProviderOptions{
 90			PreviousResponseID: fantasy.Opt(""),
 91		}))
 92		require.NoError(t, err)
 93		require.Empty(t, warnings)
 94		require.False(t, params.PreviousResponseID.Valid())
 95	})
 96}
 97
 98func TestPrepareParams_PreviousResponseID_Validation(t *testing.T) {
 99	t.Parallel()
100
101	lm := testResponsesLM()
102	opts := &ResponsesProviderOptions{
103		PreviousResponseID: fantasy.Opt("resp_abc123"),
104		Store:              fantasy.Opt(true),
105	}
106
107	t.Run("rejects with assistant messages", func(t *testing.T) {
108		t.Parallel()
109
110		_, _, err := lm.prepareParams(testCall(fantasy.Prompt{
111			testTextMessage(fantasy.MessageRoleUser, "hello"),
112			testTextMessage(fantasy.MessageRoleAssistant, "hi there"),
113		}, opts))
114		require.EqualError(t, err, previousResponseIDHistoryError)
115	})
116
117	t.Run("allows user-only prompt", func(t *testing.T) {
118		t.Parallel()
119
120		_, warnings, err := lm.prepareParams(testCall(fantasy.Prompt{
121			testTextMessage(fantasy.MessageRoleUser, "hello"),
122			testTextMessage(fantasy.MessageRoleUser, "follow up"),
123		}, opts))
124		require.NoError(t, err)
125		require.Empty(t, warnings)
126	})
127
128	t.Run("allows system + user prompt", func(t *testing.T) {
129		t.Parallel()
130
131		_, warnings, err := lm.prepareParams(testCall(fantasy.Prompt{
132			testTextMessage(fantasy.MessageRoleSystem, "be concise"),
133			testTextMessage(fantasy.MessageRoleUser, "hello"),
134		}, opts))
135		require.NoError(t, err)
136		require.Empty(t, warnings)
137	})
138
139	t.Run("rejects tool messages", func(t *testing.T) {
140		t.Parallel()
141
142		_, _, err := lm.prepareParams(testCall(fantasy.Prompt{
143			testToolResultMessage("done"),
144			testTextMessage(fantasy.MessageRoleUser, "hello"),
145		}, opts))
146		require.EqualError(t, err, previousResponseIDHistoryError)
147	})
148
149	t.Run("rejects without store", func(t *testing.T) {
150		t.Parallel()
151
152		_, _, err := lm.prepareParams(testCall(fantasy.Prompt{
153			testTextMessage(fantasy.MessageRoleUser, "hello"),
154		}, &ResponsesProviderOptions{
155			PreviousResponseID: fantasy.Opt("resp_abc123"),
156		}))
157		require.EqualError(t, err, previousResponseIDStoreError)
158	})
159
160	t.Run("rejects with store false", func(t *testing.T) {
161		t.Parallel()
162
163		_, _, err := lm.prepareParams(testCall(fantasy.Prompt{
164			testTextMessage(fantasy.MessageRoleUser, "hello"),
165		}, &ResponsesProviderOptions{
166			PreviousResponseID: fantasy.Opt("resp_abc123"),
167			Store:              fantasy.Opt(false),
168		}))
169		require.EqualError(t, err, previousResponseIDStoreError)
170	})
171}
172
173func TestValidatePreviousResponseIDPrompt(t *testing.T) {
174	t.Parallel()
175
176	tests := []struct {
177		name    string
178		prompt  fantasy.Prompt
179		wantErr bool
180	}{
181		{
182			name:   "empty prompt",
183			prompt: nil,
184		},
185		{
186			name: "user-only messages",
187			prompt: fantasy.Prompt{
188				testTextMessage(fantasy.MessageRoleUser, "hello"),
189				testTextMessage(fantasy.MessageRoleUser, "follow up"),
190			},
191		},
192		{
193			name: "system + user messages",
194			prompt: fantasy.Prompt{
195				testTextMessage(fantasy.MessageRoleSystem, "be concise"),
196				testTextMessage(fantasy.MessageRoleUser, "hello"),
197			},
198		},
199		{
200			name: "contains assistant message",
201			prompt: fantasy.Prompt{
202				testTextMessage(fantasy.MessageRoleAssistant, "hi there"),
203			},
204			wantErr: true,
205		},
206		{
207			name: "assistant in the middle",
208			prompt: fantasy.Prompt{
209				testTextMessage(fantasy.MessageRoleUser, "hello"),
210				testTextMessage(fantasy.MessageRoleAssistant, "hi there"),
211				testTextMessage(fantasy.MessageRoleUser, "follow up"),
212			},
213			wantErr: true,
214		},
215		{
216			name: "contains tool message",
217			prompt: fantasy.Prompt{
218				testToolResultMessage("done"),
219				testTextMessage(fantasy.MessageRoleUser, "follow up"),
220			},
221			wantErr: true,
222		},
223	}
224
225	for _, tt := range tests {
226		tt := tt
227		t.Run(tt.name, func(t *testing.T) {
228			t.Parallel()
229
230			err := validatePreviousResponseIDPrompt(tt.prompt)
231			if tt.wantErr {
232				require.EqualError(t, err, previousResponseIDHistoryError)
233				return
234			}
235
236			require.NoError(t, err)
237		})
238	}
239}
240
241func TestResponsesProviderMetadata_Helper(t *testing.T) {
242	t.Parallel()
243
244	t.Run("non-empty id", func(t *testing.T) {
245		t.Parallel()
246
247		metadata := responsesProviderMetadata("resp_123")
248		require.Len(t, metadata, 1)
249
250		providerMetadata, ok := metadata[Name].(*ResponsesProviderMetadata)
251		require.True(t, ok)
252		require.Equal(t, "resp_123", providerMetadata.ResponseID)
253	})
254
255	t.Run("empty id", func(t *testing.T) {
256		t.Parallel()
257
258		metadata := responsesProviderMetadata("")
259		require.Empty(t, metadata)
260	})
261}
262
263func TestResponsesProviderMetadata_JSON(t *testing.T) {
264	t.Parallel()
265
266	encoded, err := json.Marshal(ResponsesProviderMetadata{ResponseID: "resp_123"})
267	require.NoError(t, err)
268	require.Contains(t, string(encoded), `"response_id":"resp_123"`)
269
270	decoded, err := fantasy.UnmarshalProviderMetadata(map[string]json.RawMessage{
271		Name: encoded,
272	})
273	require.NoError(t, err)
274
275	providerMetadata, ok := decoded[Name].(*ResponsesProviderMetadata)
276	require.True(t, ok)
277	require.Equal(t, "resp_123", providerMetadata.ResponseID)
278}
279
280func testCall(prompt fantasy.Prompt, opts *ResponsesProviderOptions) fantasy.Call {
281	call := fantasy.Call{
282		Prompt: prompt,
283	}
284	if opts != nil {
285		call.ProviderOptions = fantasy.ProviderOptions{
286			Name: opts,
287		}
288	}
289	return call
290}
291
292func testResponsesLM() responsesLanguageModel {
293	return responsesLanguageModel{
294		provider: Name,
295		modelID:  "gpt-4o",
296	}
297}
298
299func testTextMessage(role fantasy.MessageRole, text string) fantasy.Message {
300	return fantasy.Message{
301		Role: role,
302		Content: []fantasy.MessagePart{
303			fantasy.TextPart{Text: text},
304		},
305	}
306}
307
308func testToolResultMessage(text string) fantasy.Message {
309	return fantasy.Message{
310		Role: fantasy.MessageRoleTool,
311		Content: []fantasy.MessagePart{
312			fantasy.ToolResultPart{
313				ToolCallID: "call_123",
314				Output: fantasy.ToolResultOutputContentText{
315					Text: text,
316				},
317			},
318		},
319	}
320}