feat(piagent): support image quality and output_format in image client
Add quality and output_format to ImageRequestOptions, plumb them through the JSON/multipart request bodies, generalize asset URL collection to non-image formats, and add GenerateLayeredDocument for PSD output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,8 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
)
|
||||
|
||||
func TestNewImageClientDefaultsToTenMinuteTimeout(t *testing.T) {
|
||||
@@ -145,36 +147,140 @@ func TestImageClientSendsExplicitSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageClientSendsImageCount(t *testing.T) {
|
||||
func TestImageClientSendsQuality(t *testing.T) {
|
||||
var requestBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":[{"url":"https://example.com/image-1.png"},{"url":"https://example.com/image-2.png"}]}`))
|
||||
_, _ = w.Write([]byte(`{"data":[{"url":"https://example.com/image.png"}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewImageClient(ImageClientOptions{BaseURL: server.URL, APIKey: "test-key"})
|
||||
image, err := client.Generate(context.Background(), "create two images", ImageRequestOptions{Model: "gpt-image-2", Count: 2})
|
||||
if err != nil {
|
||||
if _, err := client.Generate(context.Background(), "create an image", ImageRequestOptions{Model: "gpt-image-2", Quality: "high"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if requestBody["n"] != float64(2) {
|
||||
t.Fatalf("expected image request n=2, got %#v", requestBody["n"])
|
||||
}
|
||||
if requestBody["stream"] != nil {
|
||||
t.Fatalf("expected multi-image request to avoid stream, got %#v", requestBody["stream"])
|
||||
}
|
||||
if len(image.URLs) != 2 {
|
||||
t.Fatalf("expected two generated images, got %#v", image.URLs)
|
||||
if requestBody["quality"] != "high" {
|
||||
t.Fatalf("expected quality=high, got %#v", requestBody["quality"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageClientFallsBackToConcurrentSingleImageRequestsWhenCountUnsupported(t *testing.T) {
|
||||
func TestImageClientSendsOutputFormatAndParsesPSDBase64(t *testing.T) {
|
||||
var requestBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":[{"b64_json":"cHNkLWJ5dGVz"}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewImageClient(ImageClientOptions{BaseURL: server.URL, APIKey: "test-key"})
|
||||
image, err := client.Generate(context.Background(), "create layered file", ImageRequestOptions{Model: "gpt-image-2", OutputFormat: "psd"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if requestBody["output_format"] != "psd" {
|
||||
t.Fatalf("expected output_format=psd, got %#v", requestBody["output_format"])
|
||||
}
|
||||
if requestBody["stream"] != nil {
|
||||
t.Fatalf("expected psd output to avoid streaming, got %#v", requestBody["stream"])
|
||||
}
|
||||
if image.URL != "data:image/vnd.adobe.photoshop;base64,cHNkLWJ5dGVz" {
|
||||
t.Fatalf("expected psd data url, got %#v", image)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageClientGenerateLayeredDocumentUsesPSDOutput(t *testing.T) {
|
||||
var requestBody map[string]any
|
||||
var requestPath string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestPath = r.URL.Path
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":[{"url":"https://example.com/layers.psd"}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewImageClient(ImageClientOptions{BaseURL: server.URL, APIKey: "test-key", InputImageTransport: "url"})
|
||||
document, err := client.GenerateLayeredDocument(context.Background(), design.LayeredDocumentRequest{
|
||||
ImageURL: "https://example.com/source.png",
|
||||
Prompt: "separate layers",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if requestPath != "/v1/images/edits" {
|
||||
t.Fatalf("expected image edit endpoint, got %s", requestPath)
|
||||
}
|
||||
if requestBody["output_format"] != "psd" || requestBody["size"] != "auto" {
|
||||
t.Fatalf("expected psd auto request, got %#v", requestBody)
|
||||
}
|
||||
if document.Content != "https://example.com/layers.psd" || document.ContentType != "image/vnd.adobe.photoshop" {
|
||||
t.Fatalf("unexpected layered document: %#v", document)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageClientSplitsImageCountIntoStreamedSingleRequests(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
requestBodies := make([]map[string]any, 0, 2)
|
||||
requestKeys := make([]string, 0, 2)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.Header.Get("Idempotency-Key")
|
||||
imageIndex := strings.TrimPrefix(key, "batch-key-")
|
||||
if imageIndex == key {
|
||||
imageIndex = "unknown"
|
||||
}
|
||||
var requestBody map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mu.Lock()
|
||||
requestBodies = append(requestBodies, requestBody)
|
||||
requestKeys = append(requestKeys, key)
|
||||
mu.Unlock()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = fmt.Fprintf(w, `{"data":[{"url":"https://example.com/image-%s.png"}]}`, imageIndex)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewImageClient(ImageClientOptions{BaseURL: server.URL, APIKey: "test-key"})
|
||||
image, err := client.Generate(context.Background(), "create two images", ImageRequestOptions{Model: "gpt-image-2", Count: 2, IdempotencyKey: "batch-key"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(requestBodies) != 2 {
|
||||
t.Fatalf("expected two single-image requests, got %d", len(requestBodies))
|
||||
}
|
||||
for index, body := range requestBodies {
|
||||
if body["n"] != nil {
|
||||
t.Fatalf("expected split request %d to omit n, got %#v", index, body)
|
||||
}
|
||||
if body["stream"] != true {
|
||||
t.Fatalf("expected split request %d to keep stream=true, got %#v", index, body)
|
||||
}
|
||||
}
|
||||
keySet := map[string]bool{}
|
||||
for _, key := range requestKeys {
|
||||
keySet[key] = true
|
||||
}
|
||||
if !keySet["batch-key-1"] || !keySet["batch-key-2"] {
|
||||
t.Fatalf("expected suffixed idempotency keys, got %#v", requestKeys)
|
||||
}
|
||||
if len(image.URLs) != 2 || image.URLs[0] != "https://example.com/image-1.png" || image.URLs[1] != "https://example.com/image-2.png" {
|
||||
t.Fatalf("expected ordered generated images, got %#v", image.URLs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageClientSplitsImageCountRequestsConcurrently(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
var initialBody map[string]any
|
||||
fallbackBodies := make([]map[string]any, 0, 3)
|
||||
fallbackKeys := make([]string, 0, 3)
|
||||
activeFallbacks := 0
|
||||
@@ -188,13 +294,6 @@ func TestImageClientFallsBackToConcurrentSingleImageRequestsWhenCountUnsupported
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if requestBody["n"] != nil {
|
||||
mu.Lock()
|
||||
initialBody = requestBody
|
||||
mu.Unlock()
|
||||
http.Error(w, `{"error":{"message":"Unknown parameter: 'tools[0].n'.","param":"tools[0].n","type":"invalid_request_error"}}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
fallbackBodies = append(fallbackBodies, requestBody)
|
||||
@@ -229,22 +328,19 @@ func TestImageClientFallsBackToConcurrentSingleImageRequestsWhenCountUnsupported
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if initialBody["n"] != float64(3) {
|
||||
t.Fatalf("expected first request n=3, got %#v", initialBody["n"])
|
||||
}
|
||||
if len(image.URLs) != 3 {
|
||||
t.Fatalf("expected three fallback images, got %#v", image.URLs)
|
||||
t.Fatalf("expected three generated images, got %#v", image.URLs)
|
||||
}
|
||||
if len(fallbackBodies) != 3 {
|
||||
t.Fatalf("expected three single-image fallback requests, got %d", len(fallbackBodies))
|
||||
t.Fatalf("expected three single-image requests, got %d", len(fallbackBodies))
|
||||
}
|
||||
if maxActiveFallbacks < 2 {
|
||||
t.Fatalf("expected concurrent fallback requests, max active was %d", maxActiveFallbacks)
|
||||
t.Fatalf("expected concurrent split requests, max active was %d", maxActiveFallbacks)
|
||||
}
|
||||
keySet := map[string]bool{}
|
||||
for index, body := range fallbackBodies {
|
||||
if body["n"] != nil {
|
||||
t.Fatalf("expected fallback request %d to omit n, got %#v", index, body)
|
||||
t.Fatalf("expected split request %d to omit n, got %#v", index, body)
|
||||
}
|
||||
keySet[fallbackKeys[index]] = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user