openaicompat_test.go

  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}