anthropic_test.go

  1package anthropic
  2
  3import (
  4	"errors"
  5	"testing"
  6
  7	"charm.land/fantasy"
  8	"github.com/stretchr/testify/require"
  9)
 10
 11func TestToPrompt_DropsEmptyMessages(t *testing.T) {
 12	t.Parallel()
 13
 14	t.Run("should drop assistant messages with only reasoning content", func(t *testing.T) {
 15		t.Parallel()
 16
 17		prompt := fantasy.Prompt{
 18			{
 19				Role: fantasy.MessageRoleUser,
 20				Content: []fantasy.MessagePart{
 21					fantasy.TextPart{Text: "Hello"},
 22				},
 23			},
 24			{
 25				Role: fantasy.MessageRoleAssistant,
 26				Content: []fantasy.MessagePart{
 27					fantasy.ReasoningPart{
 28						Text: "Let me think about this...",
 29						ProviderOptions: fantasy.ProviderOptions{
 30							Name: &ReasoningOptionMetadata{
 31								Signature: "abc123",
 32							},
 33						},
 34					},
 35				},
 36			},
 37		}
 38
 39		systemBlocks, messages, warnings := toPrompt(prompt, true)
 40
 41		require.Empty(t, systemBlocks)
 42		require.Len(t, messages, 1, "should only have user message, assistant message should be dropped")
 43		require.Len(t, warnings, 1)
 44		require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
 45		require.Contains(t, warnings[0].Message, "dropping empty assistant message")
 46		require.Contains(t, warnings[0].Message, "neither user-facing content nor tool calls")
 47	})
 48
 49	t.Run("should drop assistant reasoning when sendReasoning disabled", func(t *testing.T) {
 50		t.Parallel()
 51
 52		prompt := fantasy.Prompt{
 53			{
 54				Role: fantasy.MessageRoleUser,
 55				Content: []fantasy.MessagePart{
 56					fantasy.TextPart{Text: "Hello"},
 57				},
 58			},
 59			{
 60				Role: fantasy.MessageRoleAssistant,
 61				Content: []fantasy.MessagePart{
 62					fantasy.ReasoningPart{
 63						Text: "Let me think about this...",
 64						ProviderOptions: fantasy.ProviderOptions{
 65							Name: &ReasoningOptionMetadata{
 66								Signature: "def456",
 67							},
 68						},
 69					},
 70				},
 71			},
 72		}
 73
 74		systemBlocks, messages, warnings := toPrompt(prompt, false)
 75
 76		require.Empty(t, systemBlocks)
 77		require.Len(t, messages, 1, "should only have user message, assistant message should be dropped")
 78		require.Len(t, warnings, 2)
 79		require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
 80		require.Contains(t, warnings[0].Message, "sending reasoning content is disabled")
 81		require.Equal(t, fantasy.CallWarningTypeOther, warnings[1].Type)
 82		require.Contains(t, warnings[1].Message, "dropping empty assistant message")
 83	})
 84
 85	t.Run("should drop truly empty assistant messages", func(t *testing.T) {
 86		t.Parallel()
 87
 88		prompt := fantasy.Prompt{
 89			{
 90				Role: fantasy.MessageRoleUser,
 91				Content: []fantasy.MessagePart{
 92					fantasy.TextPart{Text: "Hello"},
 93				},
 94			},
 95			{
 96				Role:    fantasy.MessageRoleAssistant,
 97				Content: []fantasy.MessagePart{},
 98			},
 99		}
100
101		systemBlocks, messages, warnings := toPrompt(prompt, true)
102
103		require.Empty(t, systemBlocks)
104		require.Len(t, messages, 1, "should only have user message")
105		require.Len(t, warnings, 1)
106		require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
107		require.Contains(t, warnings[0].Message, "dropping empty assistant message")
108	})
109
110	t.Run("should keep assistant messages with text content", func(t *testing.T) {
111		t.Parallel()
112
113		prompt := fantasy.Prompt{
114			{
115				Role: fantasy.MessageRoleUser,
116				Content: []fantasy.MessagePart{
117					fantasy.TextPart{Text: "Hello"},
118				},
119			},
120			{
121				Role: fantasy.MessageRoleAssistant,
122				Content: []fantasy.MessagePart{
123					fantasy.TextPart{Text: "Hi there!"},
124				},
125			},
126		}
127
128		systemBlocks, messages, warnings := toPrompt(prompt, true)
129
130		require.Empty(t, systemBlocks)
131		require.Len(t, messages, 2, "should have both user and assistant messages")
132		require.Empty(t, warnings)
133	})
134
135	t.Run("should keep assistant messages with tool calls", func(t *testing.T) {
136		t.Parallel()
137
138		prompt := fantasy.Prompt{
139			{
140				Role: fantasy.MessageRoleUser,
141				Content: []fantasy.MessagePart{
142					fantasy.TextPart{Text: "What's the weather?"},
143				},
144			},
145			{
146				Role: fantasy.MessageRoleAssistant,
147				Content: []fantasy.MessagePart{
148					fantasy.ToolCallPart{
149						ToolCallID: "call_123",
150						ToolName:   "get_weather",
151						Input:      `{"location":"NYC"}`,
152					},
153				},
154			},
155		}
156
157		systemBlocks, messages, warnings := toPrompt(prompt, true)
158
159		require.Empty(t, systemBlocks)
160		require.Len(t, messages, 2, "should have both user and assistant messages")
161		require.Empty(t, warnings)
162	})
163
164	t.Run("should drop assistant messages with invalid tool input", func(t *testing.T) {
165		t.Parallel()
166
167		prompt := fantasy.Prompt{
168			{
169				Role: fantasy.MessageRoleUser,
170				Content: []fantasy.MessagePart{
171					fantasy.TextPart{Text: "Hi"},
172				},
173			},
174			{
175				Role: fantasy.MessageRoleAssistant,
176				Content: []fantasy.MessagePart{
177					fantasy.ToolCallPart{
178						ToolCallID: "call_123",
179						ToolName:   "get_weather",
180						Input:      "{not-json",
181					},
182				},
183			},
184		}
185
186		systemBlocks, messages, warnings := toPrompt(prompt, true)
187
188		require.Empty(t, systemBlocks)
189		require.Len(t, messages, 1, "should only have user message")
190		require.Len(t, warnings, 1)
191		require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
192		require.Contains(t, warnings[0].Message, "dropping empty assistant message")
193	})
194
195	t.Run("should keep assistant messages with reasoning and text", func(t *testing.T) {
196		t.Parallel()
197
198		prompt := fantasy.Prompt{
199			{
200				Role: fantasy.MessageRoleUser,
201				Content: []fantasy.MessagePart{
202					fantasy.TextPart{Text: "Hello"},
203				},
204			},
205			{
206				Role: fantasy.MessageRoleAssistant,
207				Content: []fantasy.MessagePart{
208					fantasy.ReasoningPart{
209						Text: "Let me think...",
210						ProviderOptions: fantasy.ProviderOptions{
211							Name: &ReasoningOptionMetadata{
212								Signature: "abc123",
213							},
214						},
215					},
216					fantasy.TextPart{Text: "Hi there!"},
217				},
218			},
219		}
220
221		systemBlocks, messages, warnings := toPrompt(prompt, true)
222
223		require.Empty(t, systemBlocks)
224		require.Len(t, messages, 2, "should have both user and assistant messages")
225		require.Empty(t, warnings)
226	})
227
228	t.Run("should keep user messages with image content", func(t *testing.T) {
229		t.Parallel()
230
231		prompt := fantasy.Prompt{
232			{
233				Role: fantasy.MessageRoleUser,
234				Content: []fantasy.MessagePart{
235					fantasy.FilePart{
236						Data:      []byte{0x01, 0x02, 0x03},
237						MediaType: "image/png",
238					},
239				},
240			},
241		}
242
243		systemBlocks, messages, warnings := toPrompt(prompt, true)
244
245		require.Empty(t, systemBlocks)
246		require.Len(t, messages, 1)
247		require.Empty(t, warnings)
248	})
249
250	t.Run("should drop user messages without visible content", func(t *testing.T) {
251		t.Parallel()
252
253		prompt := fantasy.Prompt{
254			{
255				Role: fantasy.MessageRoleUser,
256				Content: []fantasy.MessagePart{
257					fantasy.FilePart{
258						Data:      []byte("not supported"),
259						MediaType: "application/pdf",
260					},
261				},
262			},
263		}
264
265		systemBlocks, messages, warnings := toPrompt(prompt, true)
266
267		require.Empty(t, systemBlocks)
268		require.Empty(t, messages)
269		require.Len(t, warnings, 1)
270		require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
271		require.Contains(t, warnings[0].Message, "dropping empty user message")
272		require.Contains(t, warnings[0].Message, "neither user-facing content nor tool results")
273	})
274
275	t.Run("should keep user messages with tool results", func(t *testing.T) {
276		t.Parallel()
277
278		prompt := fantasy.Prompt{
279			{
280				Role: fantasy.MessageRoleTool,
281				Content: []fantasy.MessagePart{
282					fantasy.ToolResultPart{
283						ToolCallID: "call_123",
284						Output:     fantasy.ToolResultOutputContentText{Text: "done"},
285					},
286				},
287			},
288		}
289
290		systemBlocks, messages, warnings := toPrompt(prompt, true)
291
292		require.Empty(t, systemBlocks)
293		require.Len(t, messages, 1)
294		require.Empty(t, warnings)
295	})
296
297	t.Run("should keep user messages with tool error results", func(t *testing.T) {
298		t.Parallel()
299
300		prompt := fantasy.Prompt{
301			{
302				Role: fantasy.MessageRoleTool,
303				Content: []fantasy.MessagePart{
304					fantasy.ToolResultPart{
305						ToolCallID: "call_456",
306						Output:     fantasy.ToolResultOutputContentError{Error: errors.New("boom")},
307					},
308				},
309			},
310		}
311
312		systemBlocks, messages, warnings := toPrompt(prompt, true)
313
314		require.Empty(t, systemBlocks)
315		require.Len(t, messages, 1)
316		require.Empty(t, warnings)
317	})
318
319	t.Run("should keep user messages with tool media results", func(t *testing.T) {
320		t.Parallel()
321
322		prompt := fantasy.Prompt{
323			{
324				Role: fantasy.MessageRoleTool,
325				Content: []fantasy.MessagePart{
326					fantasy.ToolResultPart{
327						ToolCallID: "call_789",
328						Output: fantasy.ToolResultOutputContentMedia{
329							Data:      "AQID",
330							MediaType: "image/png",
331						},
332					},
333				},
334			},
335		}
336
337		systemBlocks, messages, warnings := toPrompt(prompt, true)
338
339		require.Empty(t, systemBlocks)
340		require.Len(t, messages, 1)
341		require.Empty(t, warnings)
342	})
343}