v0.3.69 6abb410

openai_tool_images_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close