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}