feat(translator): add partial and full image generation support in Codex-GPT and Codex-Gemini flows
- Introduced `LastImageHashByItemID` in Codex-GPT and `LastImageHashByID` in Codex-Gemini for deduplication of generated images. - Added support for handling `partial_image` and `image_generation_call` types, with inline data embedding for Gemini and URL payload conversion for GPT. - Extended unit tests to verify image handling in both streaming and non-streaming modes.
This commit is contained in:
@@ -33,3 +33,79 @@ func TestConvertCodexResponseToGemini_StreamEmptyOutputUsesOutputItemDoneMessage
|
||||
t.Fatalf("expected fallback content from response.output_item.done message; outputs=%q", outputs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToGemini_StreamPartialImageEmitsInlineData(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[]}`)
|
||||
var param any
|
||||
|
||||
chunk := []byte(`data: {"type":"response.image_generation_call.partial_image","item_id":"ig_123","output_format":"png","partial_image_b64":"aGVsbG8=","partial_image_index":0}`)
|
||||
out := ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, chunk, ¶m)
|
||||
if len(out) != 1 {
|
||||
t.Fatalf("expected 1 chunk, got %d", len(out))
|
||||
}
|
||||
|
||||
got := gjson.GetBytes(out[0], "candidates.0.content.parts.0.inlineData.data").String()
|
||||
if got != "aGVsbG8=" {
|
||||
t.Fatalf("expected inlineData.data %q, got %q; chunk=%s", "aGVsbG8=", got, string(out[0]))
|
||||
}
|
||||
|
||||
gotMime := gjson.GetBytes(out[0], "candidates.0.content.parts.0.inlineData.mimeType").String()
|
||||
if gotMime != "image/png" {
|
||||
t.Fatalf("expected inlineData.mimeType %q, got %q; chunk=%s", "image/png", gotMime, string(out[0]))
|
||||
}
|
||||
|
||||
out = ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, chunk, ¶m)
|
||||
if len(out) != 0 {
|
||||
t.Fatalf("expected duplicate image chunk to be suppressed, got %d", len(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToGemini_StreamImageGenerationCallDoneEmitsInlineData(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[]}`)
|
||||
var param any
|
||||
|
||||
out := ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, []byte(`data: {"type":"response.image_generation_call.partial_image","item_id":"ig_123","output_format":"png","partial_image_b64":"aGVsbG8=","partial_image_index":0}`), ¶m)
|
||||
if len(out) != 1 {
|
||||
t.Fatalf("expected 1 chunk, got %d", len(out))
|
||||
}
|
||||
|
||||
out = ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, []byte(`data: {"type":"response.output_item.done","item":{"id":"ig_123","type":"image_generation_call","output_format":"png","result":"aGVsbG8="}}`), ¶m)
|
||||
if len(out) != 0 {
|
||||
t.Fatalf("expected output_item.done to be suppressed when identical to last partial image, got %d", len(out))
|
||||
}
|
||||
|
||||
out = ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, []byte(`data: {"type":"response.output_item.done","item":{"id":"ig_123","type":"image_generation_call","output_format":"jpeg","result":"Ymll"}}`), ¶m)
|
||||
if len(out) != 1 {
|
||||
t.Fatalf("expected 1 chunk, got %d", len(out))
|
||||
}
|
||||
|
||||
got := gjson.GetBytes(out[0], "candidates.0.content.parts.0.inlineData.data").String()
|
||||
if got != "Ymll" {
|
||||
t.Fatalf("expected inlineData.data %q, got %q; chunk=%s", "Ymll", got, string(out[0]))
|
||||
}
|
||||
|
||||
gotMime := gjson.GetBytes(out[0], "candidates.0.content.parts.0.inlineData.mimeType").String()
|
||||
if gotMime != "image/jpeg" {
|
||||
t.Fatalf("expected inlineData.mimeType %q, got %q; chunk=%s", "image/jpeg", gotMime, string(out[0]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToGemini_NonStreamImageGenerationCallAddsInlineDataPart(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[]}`)
|
||||
|
||||
raw := []byte(`{"type":"response.completed","response":{"id":"resp_123","created_at":1700000000,"usage":{"input_tokens":1,"output_tokens":1},"output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]},{"type":"image_generation_call","output_format":"png","result":"aGVsbG8="}]}}`)
|
||||
out := ConvertCodexResponseToGeminiNonStream(ctx, "gemini-2.5-pro", originalRequest, nil, raw, nil)
|
||||
|
||||
got := gjson.GetBytes(out, "candidates.0.content.parts.1.inlineData.data").String()
|
||||
if got != "aGVsbG8=" {
|
||||
t.Fatalf("expected inlineData.data %q, got %q; chunk=%s", "aGVsbG8=", got, string(out))
|
||||
}
|
||||
|
||||
gotMime := gjson.GetBytes(out, "candidates.0.content.parts.1.inlineData.mimeType").String()
|
||||
if gotMime != "image/png" {
|
||||
t.Fatalf("expected inlineData.mimeType %q, got %q; chunk=%s", "image/png", gotMime, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user