feat(piagent): batch-generate images in one call when generators share a prompt
When multiple image-generator tasks carry the same brief, request all images from a single GPT Image call with Count=N instead of firing one call per image. Adds directImageBatchCount/Prompt detection, batch (incremental) generation paths, and folds the planned count into the idempotency key. Disables the streaming plan probe for multi-image requests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -270,6 +270,64 @@ func TestDesignAgentImageGeneratorModeCallsImageModelDirectly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesignAgentImageGeneratorModeBatchesSamePromptCount(t *testing.T) {
|
||||
runner := &recordingRunner{}
|
||||
creative := &recordingCreativeAgent{}
|
||||
imageGenerator := &fakeImageGenerator{}
|
||||
agent := NewDesignAgent(runner, imageGenerator, creative)
|
||||
var emitted []design.Node
|
||||
|
||||
plan, err := agent.PlanIncremental(context.Background(), design.AgentRequest{
|
||||
Project: design.Project{
|
||||
ID: "project-image-generator",
|
||||
Nodes: []design.Node{
|
||||
{
|
||||
ID: "generator-1",
|
||||
Type: design.NodeTypeImage,
|
||||
Tone: "image-generator",
|
||||
Status: "generating",
|
||||
LayerRole: "image-generator",
|
||||
Width: 512,
|
||||
Height: 512,
|
||||
},
|
||||
},
|
||||
},
|
||||
Prompt: "画一只蓝色小鸟",
|
||||
Mode: "image-generator",
|
||||
ImageModel: "gpt-image-2",
|
||||
ImageSize: "1024x1024",
|
||||
TaskPlan: design.AgentTaskPlan{
|
||||
Intent: "image",
|
||||
ImageCount: 2,
|
||||
ImageTasks: []design.AgentImageTask{
|
||||
{Title: "蓝色小鸟 方案 1", Brief: "画一只蓝色小鸟"},
|
||||
{Title: "蓝色小鸟 方案 2", Brief: "画一只蓝色小鸟"},
|
||||
},
|
||||
},
|
||||
}, func(result design.AgentImageGenerationResult) error {
|
||||
emitted = append(emitted, result.Node)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if runner.calls != 0 || creative.buildCalls != 0 {
|
||||
t.Fatalf("expected direct image generator mode, runner=%d build=%d", runner.calls, creative.buildCalls)
|
||||
}
|
||||
if imageGenerator.callCount() != 1 {
|
||||
t.Fatalf("expected one batched image request, got %d", imageGenerator.callCount())
|
||||
}
|
||||
if imageGenerator.opts.Count != 2 {
|
||||
t.Fatalf("expected image request n/count 2, got %#v", imageGenerator.opts)
|
||||
}
|
||||
if len(emitted) != 2 || len(plan.GeneratedNodes) != 2 {
|
||||
t.Fatalf("expected two generated nodes, emitted=%#v plan=%#v", emitted, plan.GeneratedNodes)
|
||||
}
|
||||
if plan.GeneratedNodes[0].ID != "generator-1" || plan.GeneratedNodes[1].ID == "generator-1" {
|
||||
t.Fatalf("expected first node to replace generator and second to be separate, got %#v", plan.GeneratedNodes)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *fakeImageGenerator) callCount() int {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
@@ -635,6 +693,7 @@ func TestGeneratedImageNodeFromImageGeneratorSlotBecomesRegularImage(t *testing.
|
||||
Tone: "image-generator",
|
||||
Status: "generating",
|
||||
LayerRole: "image-generator",
|
||||
ParentID: "generator-parent",
|
||||
Width: 320,
|
||||
Height: 240,
|
||||
},
|
||||
@@ -647,6 +706,9 @@ func TestGeneratedImageNodeFromImageGeneratorSlotBecomesRegularImage(t *testing.
|
||||
if node.LayerRole != "" || node.Tone != "visual" || node.Status != "success" {
|
||||
t.Fatalf("expected regular completed image node, got %#v", node)
|
||||
}
|
||||
if node.ParentID != "" {
|
||||
t.Fatalf("expected completed image generator node to clear parent id, got %#v", node)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesignAgentDoesNotRunFallbackAfterImageTimeout(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user