1package provider
2
3import "testing"
4
5func TestOpenAICompatToolResultImagesFollowTextToolMessages(t *testing.T) {
6 c := NewOpenAICompat("commandcode", "token", "https://example.test/v1", "").(*openaiClient)
7
8 wire, err := c.buildRequest(Request{
9 Model: "vision-model",
10 Messages: []Message{
11 {Role: RoleUser, Content: []Content{TextBlock{Text: "read both images"}}},
12 {Role: RoleAssistant, Content: []Content{
13 ToolCallBlock{ID: "call-1", Name: "read"},
14 ToolCallBlock{ID: "call-2", Name: "read"},
15 }},
16 {Role: RoleTool, Content: []Content{
17 ToolResultBlock{CallID: "call-1", Content: []Content{
18 ImageBlock{MimeType: "image/png", Data: []byte("first")},
19 }},
20 }},
21 {Role: RoleTool, Content: []Content{
22 ToolResultBlock{CallID: "call-2", Content: []Content{
23 TextBlock{Text: "second image"},
24 ImageBlock{MimeType: "image/jpeg", Data: []byte("second")},
25 }},
26 }},
27 },
28 })
29 if err != nil {
30 t.Fatal(err)
31 }
32
33 wantRoles := []string{"user", "assistant", "tool", "tool", "user"}
34 if len(wire.Messages) != len(wantRoles) {
35 t.Fatalf("message count = %d, want %d: %+v", len(wire.Messages), len(wantRoles), wire.Messages)
36 }
37 for i, want := range wantRoles {
38 if got := wire.Messages[i].Role; got != want {
39 t.Fatalf("message %d role = %q, want %q", i, got, want)
40 }
41 }
42 if got, ok := wire.Messages[2].Content.(string); !ok || got != "(see attached image)" {
43 t.Fatalf("first tool content = %#v, want text placeholder", wire.Messages[2].Content)
44 }
45 if got, ok := wire.Messages[3].Content.(string); !ok || got != "second image" {
46 t.Fatalf("second tool content = %#v, want text", wire.Messages[3].Content)
47 }
48
49 parts, ok := wire.Messages[4].Content.([]interface{})
50 if !ok {
51 t.Fatalf("image message content type = %T, want []interface{}", wire.Messages[4].Content)
52 }
53 if len(parts) != 3 {
54 t.Fatalf("image message parts = %d, want prefix and two images", len(parts))
55 }
56 if text, ok := parts[0].(oaiContentText); !ok || text.Text != "Tool output included the following image content:" {
57 t.Fatalf("image message prefix = %#v", parts[0])
58 }
59 for i, part := range parts[1:] {
60 image, ok := part.(oaiContentImage)
61 if !ok || image.Type != "image_url" {
62 t.Fatalf("image part %d = %#v", i, part)
63 }
64 }
65}
66
67func TestOpenAICompatDoesNotDuplicatePersistedToolImageMirror(t *testing.T) {
68 c := NewOpenAICompat("custom", "token", "https://example.test/v1", "").(*openaiClient)
69 image := ImageBlock{MimeType: "image/png", Data: []byte("image")}
70
71 wire, err := c.buildRequest(Request{
72 Model: "vision-model",
73 Messages: []Message{
74 {Role: RoleAssistant, Content: []Content{
75 ToolCallBlock{ID: "call-1", Name: "read"},
76 }},
77 {Role: RoleTool, Content: []Content{
78 ToolResultBlock{CallID: "call-1", Content: []Content{image}},
79 }},
80 {Role: RoleUser, Content: []Content{
81 TextBlock{Text: "Tool output included the following image content:"},
82 image,
83 }},
84 },
85 })
86 if err != nil {
87 t.Fatal(err)
88 }
89
90 wantRoles := []string{"assistant", "tool", "user"}
91 if len(wire.Messages) != len(wantRoles) {
92 t.Fatalf("message count = %d, want %d: %+v", len(wire.Messages), len(wantRoles), wire.Messages)
93 }
94 for i, want := range wantRoles {
95 if got := wire.Messages[i].Role; got != want {
96 t.Fatalf("message %d role = %q, want %q", i, got, want)
97 }
98 }
99}
100
101func TestOpenAICompatToolResultErrorText(t *testing.T) {
102 c := NewOpenAICompat("custom", "token", "https://example.test/v1", "").(*openaiClient)
103
104 wire, err := c.buildRequest(Request{
105 Model: "vision-model",
106 Messages: []Message{
107 {Role: RoleAssistant, Content: []Content{
108 ToolCallBlock{ID: "call-1", Name: "read"},
109 }},
110 {Role: RoleTool, Content: []Content{
111 ToolResultBlock{CallID: "call-1", IsError: true, Content: []Content{
112 TextBlock{Text: "boom"},
113 }},
114 }},
115 },
116 })
117 if err != nil {
118 t.Fatal(err)
119 }
120 if len(wire.Messages) != 2 {
121 t.Fatalf("message count = %d, want 2: %+v", len(wire.Messages), len(wire.Messages))
122 }
123 if got, ok := wire.Messages[1].Content.(string); !ok || got != "boom [error]" {
124 t.Fatalf("tool content = %#v, want error text", wire.Messages[1].Content)
125 }
126}
127
128func TestOpenAICompatDeepSeekDropsToolImages(t *testing.T) {
129 c := NewOpenAICompat("deepseek", "token", "https://example.test/v1", "").(*openaiClient)
130
131 wire, err := c.buildRequest(Request{
132 Model: "deepseek-chat",
133 Messages: []Message{
134 {Role: RoleAssistant, Content: []Content{
135 ToolCallBlock{ID: "call-1", Name: "read"},
136 }},
137 {Role: RoleTool, Content: []Content{
138 ToolResultBlock{CallID: "call-1", Content: []Content{
139 TextBlock{Text: "caption"},
140 ImageBlock{MimeType: "image/png", Data: []byte("image")},
141 }},
142 }},
143 },
144 })
145 if err != nil {
146 t.Fatal(err)
147 }
148 if len(wire.Messages) != 2 {
149 t.Fatalf("message count = %d, want 2 (no image mirror): %+v", len(wire.Messages), len(wire.Messages))
150 }
151 if got, ok := wire.Messages[1].Content.(string); !ok || got != "caption" {
152 t.Fatalf("tool content = %#v, want text only", wire.Messages[1].Content)
153 }
154}
155
156func TestOpenAICompatOpenCodeGoDeepSeekDropsToolImages(t *testing.T) {
157 c := NewOpenAICompat(ProviderOpenCodeGo, "token", "https://example.test/v1", "").(*openaiClient)
158
159 wire, err := c.buildRequest(Request{
160 Model: "deepseek/deepseek-chat",
161 Messages: []Message{
162 {Role: RoleAssistant, Content: []Content{
163 ToolCallBlock{ID: "call-1", Name: "read"},
164 }},
165 {Role: RoleTool, Content: []Content{
166 ToolResultBlock{CallID: "call-1", Content: []Content{
167 TextBlock{Text: "caption"},
168 ImageBlock{MimeType: "image/png", Data: []byte("image")},
169 }},
170 }},
171 },
172 })
173 if err != nil {
174 t.Fatal(err)
175 }
176 if len(wire.Messages) != 2 {
177 t.Fatalf("message count = %d, want 2 (no image mirror): %+v", len(wire.Messages), len(wire.Messages))
178 }
179 if got, ok := wire.Messages[1].Content.(string); !ok || got != "caption" {
180 t.Fatalf("tool content = %#v, want text only", wire.Messages[1].Content)
181 }
182}
183
184func TestOpenAICompatUserImagesPassThrough(t *testing.T) {
185 c := NewOpenAICompat("custom", "token", "https://example.test/v1", "").(*openaiClient)
186 image := ImageBlock{MimeType: "image/png", Data: []byte("image")}
187
188 wire, err := c.buildRequest(Request{
189 Model: "vision-model",
190 Messages: []Message{
191 {Role: RoleUser, Content: []Content{
192 TextBlock{Text: "look"},
193 image,
194 }},
195 },
196 })
197 if err != nil {
198 t.Fatal(err)
199 }
200 if len(wire.Messages) != 1 {
201 t.Fatalf("message count = %d, want 1: %+v", len(wire.Messages), len(wire.Messages))
202 }
203 parts, ok := wire.Messages[0].Content.([]interface{})
204 if !ok || len(parts) != 2 {
205 t.Fatalf("user content = %#v, want text+image", wire.Messages[0].Content)
206 }
207 if _, ok := parts[1].(oaiContentImage); !ok {
208 t.Fatalf("second part = %#v, want image", parts[1])
209 }
210}