1package openaicompat
2
3import (
4 "testing"
5
6 "charm.land/fantasy"
7 "github.com/stretchr/testify/require"
8)
9
10func TestToPromptFunc_ReasoningContent(t *testing.T) {
11 t.Parallel()
12
13 t.Run("should add reasoning_content field to assistant messages", func(t *testing.T) {
14 t.Parallel()
15
16 prompt := fantasy.Prompt{
17 {
18 Role: fantasy.MessageRoleUser,
19 Content: []fantasy.MessagePart{
20 fantasy.TextPart{Text: "What is 2+2?"},
21 },
22 },
23 {
24 Role: fantasy.MessageRoleAssistant,
25 Content: []fantasy.MessagePart{
26 fantasy.ReasoningPart{Text: "Let me think... 2+2 equals 4."},
27 fantasy.TextPart{Text: "The answer is 4."},
28 },
29 },
30 {
31 Role: fantasy.MessageRoleUser,
32 Content: []fantasy.MessagePart{
33 fantasy.TextPart{Text: "What about 3+3?"},
34 },
35 },
36 }
37
38 messages, warnings := ToPromptFunc(prompt, "", "")
39
40 require.Empty(t, warnings)
41 require.Len(t, messages, 3)
42
43 // First message (user) - no reasoning
44 msg1 := messages[0].OfUser
45 require.NotNil(t, msg1)
46 require.Equal(t, "What is 2+2?", msg1.Content.OfString.Value)
47
48 // Second message (assistant) - with reasoning
49 msg2 := messages[1].OfAssistant
50 require.NotNil(t, msg2)
51 require.Equal(t, "The answer is 4.", msg2.Content.OfString.Value)
52 // Check reasoning_content in extra fields
53 extraFields := msg2.ExtraFields()
54 reasoningContent, hasReasoning := extraFields["reasoning_content"]
55 require.True(t, hasReasoning)
56 require.Equal(t, "Let me think... 2+2 equals 4.", reasoningContent)
57
58 // Third message (user) - no reasoning
59 msg3 := messages[2].OfUser
60 require.NotNil(t, msg3)
61 require.Equal(t, "What about 3+3?", msg3.Content.OfString.Value)
62 })
63
64 t.Run("should handle assistant messages with only reasoning content", func(t *testing.T) {
65 t.Parallel()
66
67 prompt := fantasy.Prompt{
68 {
69 Role: fantasy.MessageRoleUser,
70 Content: []fantasy.MessagePart{
71 fantasy.TextPart{Text: "Hello"},
72 },
73 },
74 {
75 Role: fantasy.MessageRoleAssistant,
76 Content: []fantasy.MessagePart{
77 fantasy.ReasoningPart{Text: "Internal reasoning only..."},
78 },
79 },
80 }
81
82 messages, warnings := ToPromptFunc(prompt, "", "")
83
84 require.Empty(t, warnings)
85 require.Len(t, messages, 2)
86
87 // Assistant message with only reasoning
88 msg := messages[1].OfAssistant
89 require.NotNil(t, msg)
90 extraFields := msg.ExtraFields()
91 reasoningContent, hasReasoning := extraFields["reasoning_content"]
92 require.True(t, hasReasoning)
93 require.Equal(t, "Internal reasoning only...", reasoningContent)
94 })
95
96 t.Run("should not add reasoning_content to messages without reasoning", func(t *testing.T) {
97 t.Parallel()
98
99 prompt := fantasy.Prompt{
100 {
101 Role: fantasy.MessageRoleUser,
102 Content: []fantasy.MessagePart{
103 fantasy.TextPart{Text: "Hello"},
104 },
105 },
106 {
107 Role: fantasy.MessageRoleAssistant,
108 Content: []fantasy.MessagePart{
109 fantasy.TextPart{Text: "Hi there!"},
110 },
111 },
112 }
113
114 messages, warnings := ToPromptFunc(prompt, "", "")
115
116 require.Empty(t, warnings)
117 require.Len(t, messages, 2)
118
119 // Assistant message without reasoning
120 msg := messages[1].OfAssistant
121 require.NotNil(t, msg)
122 require.Equal(t, "Hi there!", msg.Content.OfString.Value)
123 extraFields := msg.ExtraFields()
124 _, hasReasoning := extraFields["reasoning_content"]
125 require.False(t, hasReasoning)
126 })
127
128 t.Run("should preserve system and user messages unchanged", func(t *testing.T) {
129 t.Parallel()
130
131 prompt := fantasy.Prompt{
132 {
133 Role: fantasy.MessageRoleSystem,
134 Content: []fantasy.MessagePart{
135 fantasy.TextPart{Text: "You are helpful."},
136 },
137 },
138 {
139 Role: fantasy.MessageRoleUser,
140 Content: []fantasy.MessagePart{
141 fantasy.TextPart{Text: "Hello"},
142 },
143 },
144 }
145
146 messages, warnings := ToPromptFunc(prompt, "", "")
147
148 require.Empty(t, warnings)
149 require.Len(t, messages, 2)
150
151 // System message - unchanged
152 systemMsg := messages[0].OfSystem
153 require.NotNil(t, systemMsg)
154 require.Equal(t, "You are helpful.", systemMsg.Content.OfString.Value)
155
156 // User message - unchanged
157 userMsg := messages[1].OfUser
158 require.NotNil(t, userMsg)
159 require.Equal(t, "Hello", userMsg.Content.OfString.Value)
160 })
161
162 t.Run("should use last assistant TextPart only", func(t *testing.T) {
163 t.Parallel()
164
165 prompt := fantasy.Prompt{
166 {
167 Role: fantasy.MessageRoleUser,
168 Content: []fantasy.MessagePart{
169 fantasy.TextPart{Text: "Hello"},
170 },
171 },
172 {
173 Role: fantasy.MessageRoleAssistant,
174 Content: []fantasy.MessagePart{
175 fantasy.TextPart{Text: "First part. "},
176 fantasy.TextPart{Text: "Second part. "},
177 fantasy.TextPart{Text: "Third part."},
178 },
179 },
180 }
181
182 messages, warnings := ToPromptFunc(prompt, "", "")
183
184 require.Empty(t, warnings)
185 require.Len(t, messages, 2)
186
187 // Assistant message should use only the last TextPart (matching openai behavior)
188 assistantMsg := messages[1].OfAssistant
189 require.NotNil(t, assistantMsg)
190 require.Equal(t, "Third part.", assistantMsg.Content.OfString.Value)
191 })
192
193 t.Run("should include user messages with only unsupported attachments", func(t *testing.T) {
194 t.Parallel()
195
196 prompt := fantasy.Prompt{
197 {
198 Role: fantasy.MessageRoleUser,
199 Content: []fantasy.MessagePart{
200 fantasy.TextPart{Text: "Hello"},
201 },
202 },
203 {
204 Role: fantasy.MessageRoleUser,
205 Content: []fantasy.MessagePart{
206 fantasy.FilePart{
207 MediaType: "application/x-unsupported",
208 Data: []byte("unsupported data"),
209 },
210 },
211 },
212 {
213 Role: fantasy.MessageRoleUser,
214 Content: []fantasy.MessagePart{
215 fantasy.TextPart{Text: "After unsupported"},
216 },
217 },
218 }
219
220 messages, warnings := ToPromptFunc(prompt, "", "")
221
222 require.Len(t, warnings, 1)
223 require.Contains(t, warnings[0].Message, "not supported")
224 // Should have all 3 messages (matching openai behavior - don't skip empty content)
225 require.Len(t, messages, 3)
226
227 msg1 := messages[0].OfUser
228 require.NotNil(t, msg1)
229 require.Equal(t, "Hello", msg1.Content.OfString.Value)
230
231 // Second message has empty content (unsupported attachment was skipped)
232 msg2 := messages[1].OfUser
233 require.NotNil(t, msg2)
234 content2 := msg2.Content.OfArrayOfContentParts
235 require.Len(t, content2, 0)
236
237 msg3 := messages[2].OfUser
238 require.NotNil(t, msg3)
239 require.Equal(t, "After unsupported", msg3.Content.OfString.Value)
240 })
241
242 t.Run("should detect PDF file IDs using strings.HasPrefix", func(t *testing.T) {
243 t.Parallel()
244
245 prompt := fantasy.Prompt{
246 {
247 Role: fantasy.MessageRoleUser,
248 Content: []fantasy.MessagePart{
249 fantasy.TextPart{Text: "Check this PDF"},
250 fantasy.FilePart{
251 MediaType: "application/pdf",
252 Data: []byte("file-abc123xyz"),
253 Filename: "test.pdf",
254 },
255 },
256 },
257 }
258
259 messages, warnings := ToPromptFunc(prompt, "", "")
260
261 require.Empty(t, warnings)
262 require.Len(t, messages, 1)
263
264 userMsg := messages[0].OfUser
265 require.NotNil(t, userMsg)
266
267 content := userMsg.Content.OfArrayOfContentParts
268 require.Len(t, content, 2)
269
270 // Second content part should be file with file_id
271 filePart := content[1].OfFile
272 require.NotNil(t, filePart)
273 require.Equal(t, "file-abc123xyz", filePart.File.FileID.Value)
274 })
275}