fix(provider): preserve projected user content blocks
- commit
- b44637937e02460785aacea405b87834fbab9710
- author
- brice · · 2026-08-09 09:54 UTC
- parent
- 52c14bf
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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{} { | |||
| 412 | 412 | } | |
| 413 | 413 | } | |
| 414 | 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 | 422 | var sb strings.Builder | |
| 416 | 423 | for _, b := range blocks { | |
| 417 | 424 | 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.Builder | 422 | 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) { | |||
| 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 | 143 | func TestMiniMaxM3CatalogAndRequest(t *testing.T) { | |
| 114 | 144 | for _, provider := range []string{"minimax", "minimax-cn"} { | |
| 115 | 145 | 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") | ||