detached b446379

fix(provider): preserve projected user content blocks

commit
b44637937e02460785aacea405b87834fbab9710
author
brice · · 2026-08-09 09:54 UTC
parent
52c14bf
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +37 -0
M packages/provider/openai.go +7 -0
@@ -412,6 +412,13 @@ func buildOAIUserContent(blocks []Content, textOnly bool) interface{} {
412412 }
413413 }
414414 if textOnly || !hasImage {
415+ // Keep multiple text blocks structured. The core provider projection
416+ // adds message-time annotations as a separate block; collapsing that
417+ // block with the user text changes the wire representation of the
418+ // original prompt and can make consumers misclassify the request.
419+ if !textOnly && len(blocks) > 1 {
420+ return buildOAIContentBlocks(blocks, false)
421+ }
415422 var sb strings.Builder
416423 for _, b := range blocks {
417424 if tb, ok := b.(TextBlock); ok {
@@ -412,6 +412,13 @@ func buildOAIUserContent(blocks []Content, textOnly bool) interface{} {
412 }412 }
413 }413 }
414 if textOnly || !hasImage {414 if textOnly || !hasImage {
415+ // Keep multiple text blocks structured. The core provider projection
416+ // adds message-time annotations as a separate block; collapsing that
417+ // block with the user text changes the wire representation of the
418+ // original prompt and can make consumers misclassify the request.
419+ if !textOnly && len(blocks) > 1 {
420+ return buildOAIContentBlocks(blocks, false)
421+ }
415 var sb strings.Builder422 var sb strings.Builder
416 for _, b := range blocks {423 for _, b := range blocks {
417 if tb, ok := b.(TextBlock); ok {424 if tb, ok := b.(TextBlock); ok {
M packages/provider/provider_test.go +30 -0
@@ -110,6 +110,36 @@ func TestOpenAIErrorStatus(t *testing.T) {
110110 }
111111 }
112112
113+func TestOpenAIUserContentPreservesProjectedTextBlocks(t *testing.T) {
114+ c := NewOpenAI("x", "").(*openaiClient)
115+ wire, err := c.buildRequest(Request{
116+ Model: "gpt-5",
117+ Messages: []Message{{
118+ Role: RoleUser,
119+ Content: []Content{
120+ TextBlock{Text: "[message time: 2026-08-08T12:34:56-07:00]"},
121+ TextBlock{Text: "fail"},
122+ },
123+ }},
124+ })
125+ if err != nil {
126+ t.Fatal(err)
127+ }
128+ blocks, ok := wire.Messages[0].Content.([]interface{})
129+ if !ok {
130+ t.Fatalf("user content = %T, want structured text blocks", wire.Messages[0].Content)
131+ }
132+ if len(blocks) != 2 {
133+ t.Fatalf("user content blocks = %d, want 2", len(blocks))
134+ }
135+ if got := blocks[0].(oaiContentText).Text; got != "[message time: 2026-08-08T12:34:56-07:00]" {
136+ t.Fatalf("timestamp block = %q", got)
137+ }
138+ if got := blocks[1].(oaiContentText).Text; got != "fail" {
139+ t.Fatalf("user text block = %q", got)
140+ }
141+}
142+
113143 func TestMiniMaxM3CatalogAndRequest(t *testing.T) {
114144 for _, provider := range []string{"minimax", "minimax-cn"} {
115145 m, err := FindModel(provider, "MiniMax-M3")
@@ -110,6 +110,36 @@ func TestOpenAIErrorStatus(t *testing.T) {
110 }110 }
111 }111 }
112 112
113+func TestOpenAIUserContentPreservesProjectedTextBlocks(t *testing.T) {
114+ c := NewOpenAI("x", "").(*openaiClient)
115+ wire, err := c.buildRequest(Request{
116+ Model: "gpt-5",
117+ Messages: []Message{{
118+ Role: RoleUser,
119+ Content: []Content{
120+ TextBlock{Text: "[message time: 2026-08-08T12:34:56-07:00]"},
121+ TextBlock{Text: "fail"},
122+ },
123+ }},
124+ })
125+ if err != nil {
126+ t.Fatal(err)
127+ }
128+ blocks, ok := wire.Messages[0].Content.([]interface{})
129+ if !ok {
130+ t.Fatalf("user content = %T, want structured text blocks", wire.Messages[0].Content)
131+ }
132+ if len(blocks) != 2 {
133+ t.Fatalf("user content blocks = %d, want 2", len(blocks))
134+ }
135+ if got := blocks[0].(oaiContentText).Text; got != "[message time: 2026-08-08T12:34:56-07:00]" {
136+ t.Fatalf("timestamp block = %q", got)
137+ }
138+ if got := blocks[1].(oaiContentText).Text; got != "fail" {
139+ t.Fatalf("user text block = %q", got)
140+ }
141+}
142+
113 func TestMiniMaxM3CatalogAndRequest(t *testing.T) {143 func TestMiniMaxM3CatalogAndRequest(t *testing.T) {
114 for _, provider := range []string{"minimax", "minimax-cn"} {144 for _, provider := range []string{"minimax", "minimax-cn"} {
115 m, err := FindModel(provider, "MiniMax-M3")145 m, err := FindModel(provider, "MiniMax-M3")

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 477

↑↓ moveenter openesc close