on main help sign in
fix: skip image attachments in history when model doesn't support them (#2818)

When switching from a vision-capable model to one without image support mid-conversation, historical user messages containing image attachments would cause API errors. Now those attachments are filtered out from the prompt history based on the current model's SupportsImages capability. ๐Ÿ’˜ Generated with Crush Assisted-by: Kimi K2.6 via Crush <crush@charm.land>

commit6d95ecc5c7b530f88fa7503d3897a3456a252f53 authorAndrey Nering <andreynering@users.noreply.github.com> dateWed 6 May 2026, 17:05 (5 months ago) parent61f49b2
2 files changed+73โˆ’6
internal/agent/agent.go+26โˆ’4 internal/agent/agent_test.go+47โˆ’2
internal/agent/agent.go+26โˆ’4
@@ -245,7 +245,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
245245 defer cancel()
246246 defer a.activeRequests.Del(call.SessionID)
247247
248โˆ’ history, files := a.preparePrompt(msgs, call.Attachments...)
248+ history, files := a.preparePrompt(msgs, largeModel.CatwalkCfg.SupportsImages, call.Attachments...)
249249
250250 startTime := time.Now()
251251 a.eventPromptSent(call.SessionID)
@@ -643,7 +643,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
643643 return nil
644644 }
645645
646โˆ’ aiMsgs, _ := a.preparePrompt(msgs)
646+ aiMsgs, _ := a.preparePrompt(msgs, largeModel.CatwalkCfg.SupportsImages)
647647
648648 genCtx, cancel := context.WithCancel(ctx)
649649 a.activeRequests.Set(sessionID, cancel)
@@ -786,7 +786,7 @@ func (a *sessionAgent) createUserMessage(ctx context.Context, call SessionAgentC
786786 return msg, nil
787787}
788788
789โˆ’func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...message.Attachment) ([]fantasy.Message, []fantasy.FilePart) {
789+func (a *sessionAgent) preparePrompt(msgs []message.Message, supportsImages bool, attachments ...message.Attachment) ([]fantasy.Message, []fantasy.FilePart) {
790790 var history []fantasy.Message
791791 if !a.isSubAgent {
792792 history = append(history, fantasy.NewUserMessage(
@@ -830,7 +830,15 @@ If not, please feel free to ignore. Again do not mention this message to the use
830830 }
831831 continue
832832 }
833โˆ’ history = append(history, m.ToAIMessage()...)
833+ aiMsgs := m.ToAIMessage()
834+ if !supportsImages {
835+ for i := range aiMsgs {
836+ if aiMsgs[i].Role == fantasy.MessageRoleUser {
837+ aiMsgs[i].Content = filterFileParts(aiMsgs[i].Content)
838+ }
839+ }
840+ }
841+ history = append(history, aiMsgs...)
834842
835843 if m.Role == message.Assistant {
836844 if msg, ok := syntheticToolResultsForOrphanedCalls(m, knownToolResultIDs); ok {
@@ -854,6 +862,20 @@ If not, please feel free to ignore. Again do not mention this message to the use
854862 return history, files
855863}
856864
865+// filterFileParts removes fantasy.FilePart entries from a slice of message
866+// parts. Used to strip image attachments from historical user messages when
867+// the current model does not support them.
868+func filterFileParts(parts []fantasy.MessagePart) []fantasy.MessagePart {
869+ filtered := make([]fantasy.MessagePart, 0, len(parts))
870+ for _, part := range parts {
871+ if _, ok := fantasy.AsMessagePart[fantasy.FilePart](part); ok {
872+ continue
873+ }
874+ filtered = append(filtered, part)
875+ }
876+ return filtered
877+}
878+
857879// filterOrphanedToolResults converts a tool message to a fantasy.Message,
858880// dropping any tool result parts whose tool_call_id has no matching tool call
859881// in the known set. An orphaned result causes API validation to fail on every
internal/agent/agent_test.go+47โˆ’2
@@ -656,6 +656,51 @@ func BenchmarkBuildSummaryPrompt(b *testing.B) {
656656 }
657657}
658658
659+func TestPreparePrompt_FiltersImageAttachments(t *testing.T) {
660+ env := testEnv(t)
661+ sa := testSessionAgent(env, nil, nil, "test prompt")
662+ agent := sa.(*sessionAgent)
663+
664+ ctx := t.Context()
665+ sess, err := env.sessions.Create(ctx, "test")
666+ require.NoError(t, err)
667+
668+ // User message with text, a text attachment, and an image attachment.
669+ _, err = env.messages.Create(ctx, sess.ID, message.CreateMessageParams{
670+ Role: message.User,
671+ Parts: []message.ContentPart{
672+ message.TextContent{Text: "hello world"},
673+ message.BinaryContent{Path: "notes.txt", MIMEType: "text/plain", Data: []byte("important notes")},
674+ message.BinaryContent{Path: "image.png", MIMEType: "image/png", Data: []byte("fake-image-data")},
675+ },
676+ })
677+ require.NoError(t, err)
678+
679+ msgs, err := env.messages.List(ctx, sess.ID)
680+ require.NoError(t, err)
681+
682+ // When supportsImages is false, image attachments should be stripped.
683+ history, _ := agent.preparePrompt(msgs, false)
684+ // First message is the system reminder, second is the user message.
685+ require.Len(t, history, 2)
686+ require.Len(t, history[1].Content, 1)
687+ text, ok := fantasy.AsMessagePart[fantasy.TextPart](history[1].Content[0])
688+ require.True(t, ok)
689+ require.Contains(t, text.Text, "hello world")
690+ require.Contains(t, text.Text, "important notes")
691+
692+ // When supportsImages is true, image attachments should remain.
693+ history, _ = agent.preparePrompt(msgs, true)
694+ require.Len(t, history, 2)
695+ require.Len(t, history[1].Content, 2)
696+ text, ok = fantasy.AsMessagePart[fantasy.TextPart](history[1].Content[0])
697+ require.True(t, ok)
698+ require.Contains(t, text.Text, "hello world")
699+ file, ok := fantasy.AsMessagePart[fantasy.FilePart](history[1].Content[1])
700+ require.True(t, ok)
701+ require.Equal(t, "image.png", file.Filename)
702+}
703+
659704func TestPreparePrompt_OrphanedToolUse(t *testing.T) {
660705 env := testEnv(t)
661706 sa := testSessionAgent(env, nil, nil, "test prompt")
@@ -702,7 +747,7 @@ func TestPreparePrompt_OrphanedToolUse(t *testing.T) {
702747 msgs, err := env.messages.List(ctx, sess.ID)
703748 require.NoError(t, err)
704749
705โˆ’ history, _ := agent.preparePrompt(msgs)
750+ history, _ := agent.preparePrompt(msgs, true)
706751
707752 // The history must contain a synthetic tool result for the orphaned call.
708753 found := false
@@ -776,7 +821,7 @@ func TestPreparePrompt_OrphanedToolUseMixed(t *testing.T) {
776821 msgs, err := env.messages.List(ctx, sess.ID)
777822 require.NoError(t, err)
778823
779โˆ’ history, _ := agent.preparePrompt(msgs)
824+ history, _ := agent.preparePrompt(msgs, true)
780825
781826 // Should have a synthetic result only for the orphaned call.
782827 var syntheticCount int