Skip to content

Commit bb9d5ec

Browse files
fix(stream-translate): surface image_generation_call.partial_image as content chunks
1 parent c9989c5 commit bb9d5ec

3 files changed

Lines changed: 75 additions & 3 deletions

File tree

proxy/images_generations.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ import (
3636
// - moderation (string) "auto" / "low"
3737
// - response_format (string) "url" / "b64_json",默认 "url"(我们的扩展,
3838
// OpenAI gpt-image-1 官方只返回 b64_json)
39-
// - partial_images (int) 0-3(流式,本实现暂返回终稿)
39+
// - partial_images (int) 0-3(流式渐进预览,v1.7.50 起经 ChatCompletions
40+
// 翻译层透出为多张 markdown image content chunk;
41+
// /v1/images/generations 内部走非流式仍返回终稿)
4042
// - user (string) 终端用户标识(仅透传到日志)
4143
//
4244
// 响应格式(与 OpenAI gpt-image-1 对齐):

proxy/model_overrides_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,55 @@ func TestTranslateStreamChunk_NonImageOutputItemDoneIgnored(t *testing.T) {
216216
t.Fatalf("non-image output_item.done should return (nil, false), got (%v, %v)", chunk, done)
217217
}
218218
}
219+
220+
// 回归 v1.7.49 bug:metadata.image_partial_images 被透传到上游,但翻译层把
221+
// response.image_generation_call.partial_image 事件直接 drop 掉。
222+
// 修复后必须 emit 为 markdown image content chunk(和终稿同源路径)。
223+
func TestTranslateStreamChunk_PartialImageEmitsMarkdown(t *testing.T) {
224+
b64 := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
225+
event := []byte(`{"type":"response.image_generation_call.partial_image","item_id":"ig_x","output_index":0,"sequence_number":0,"partial_image_index":0,"partial_image_b64":"` + b64 + `"}`)
226+
227+
chunk, done := TranslateStreamChunk(event, "gpt-image-2", "chatcmpl-x", 0)
228+
if done {
229+
t.Fatal("partial_image must not finish the stream")
230+
}
231+
if chunk == nil {
232+
t.Fatal("expected content chunk for partial_image, got nil (regression: drop bug returned)")
233+
}
234+
s := string(chunk)
235+
if !strings.Contains(s, "data:image/png;base64,"+b64) {
236+
t.Fatalf("chunk missing markdown image; got %s", s)
237+
}
238+
if !strings.Contains(s, `"chat.completion.chunk"`) {
239+
t.Fatalf("not a chat.completion.chunk; got %s", s)
240+
}
241+
}
242+
243+
func TestStreamTranslator_PartialImageEmitsMarkdown(t *testing.T) {
244+
b64 := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
245+
event := []byte(`{"type":"response.image_generation_call.partial_image","partial_image_b64":"` + b64 + `"}`)
246+
247+
st := NewStreamTranslator("chatcmpl-x", "gpt-image-2", 0)
248+
chunk, done := st.Translate(event)
249+
if done {
250+
t.Fatal("partial_image must not finish the stream")
251+
}
252+
if chunk == nil {
253+
t.Fatal("expected content chunk, got nil (regression: drop bug returned)")
254+
}
255+
if !strings.Contains(string(chunk), "data:image/png;base64,"+b64) {
256+
t.Fatalf("chunk missing markdown image; got %s", string(chunk))
257+
}
258+
if st.HasToolCalls {
259+
t.Fatal("partial_image must not set HasToolCalls")
260+
}
261+
}
262+
263+
// partial_image 无 b64 时(理论上不会,但防御)必须静默忽略。
264+
func TestTranslateStreamChunk_PartialImageMissingB64Ignored(t *testing.T) {
265+
event := []byte(`{"type":"response.image_generation_call.partial_image","partial_image_index":0}`)
266+
chunk, done := TranslateStreamChunk(event, "gpt-image-2", "id", 0)
267+
if done || chunk != nil {
268+
t.Fatalf("missing partial_image_b64 should be silently ignored, got (%v, %v)", chunk, done)
269+
}
270+
}

proxy/translator.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,14 +1035,25 @@ func TranslateStreamChunk(eventData []byte, model string, chunkID string, create
10351035
}
10361036
return newErrorResponse(errMsg), true
10371037

1038+
case "response.image_generation_call.partial_image":
1039+
// 渐进式预览帧:客户端通过 metadata.image_partial_images = N
1040+
// 让上游在终稿之前发 N 张中间帧。每帧 partial_image_b64 字段都是
1041+
// 一张完整的 base64 PNG,按图像生成的 SSE 顺序到达。
1042+
// 这里和终稿走同样的落盘路径,作为 markdown image 注入 content delta;
1043+
// 客户端可以收到一系列 ![image](url1) ![image](url2) ![image](finalurl)。
1044+
if b64 := gjson.GetBytes(eventData, "partial_image_b64").String(); b64 != "" {
1045+
return newContentChunk(chunkID, model, created, imageMarkdownFromBase64(b64)), false
1046+
}
1047+
return nil, false
1048+
10381049
case "response.content_part.done",
10391050
"response.created", "response.in_progress",
10401051
"response.output_item.added", "response.content_part.added",
10411052
"response.reasoning_summary_text.done",
10421053
"response.reasoning.encrypted_content.delta", "response.reasoning.encrypted_content.done",
10431054
"response.reasoning_summary_part.added", "response.reasoning_summary_part.done",
10441055
"response.image_generation_call.in_progress", "response.image_generation_call.generating",
1045-
"response.image_generation_call.partial_image", "response.image_generation_call.completed":
1056+
"response.image_generation_call.completed":
10461057
return nil, false
10471058

10481059
default:
@@ -1147,14 +1158,21 @@ func (st *StreamTranslator) Translate(eventData []byte) ([]byte, bool) {
11471158
}
11481159
return newErrorResponse(errMsg), true
11491160

1161+
case "response.image_generation_call.partial_image":
1162+
// 渐进式预览帧:见 stateless Translate 路径的同名 case 说明。
1163+
if b64 := gjson.GetBytes(eventData, "partial_image_b64").String(); b64 != "" {
1164+
return newContentChunk(st.ChunkID, st.Model, st.Created, imageMarkdownFromBase64(b64)), false
1165+
}
1166+
return nil, false
1167+
11501168
case "response.content_part.done",
11511169
"response.created", "response.in_progress",
11521170
"response.content_part.added",
11531171
"response.reasoning_summary_text.done",
11541172
"response.reasoning.encrypted_content.delta", "response.reasoning.encrypted_content.done",
11551173
"response.reasoning_summary_part.added", "response.reasoning_summary_part.done",
11561174
"response.image_generation_call.in_progress", "response.image_generation_call.generating",
1157-
"response.image_generation_call.partial_image", "response.image_generation_call.completed":
1175+
"response.image_generation_call.completed":
11581176
return nil, false
11591177

11601178
default:

0 commit comments

Comments
 (0)