diff --git a/backend/internal/api/handlers.go b/backend/internal/api/handlers.go index 07cf2e8..bd7036c 100644 --- a/backend/internal/api/handlers.go +++ b/backend/internal/api/handlers.go @@ -582,6 +582,7 @@ func GenerateWithImagesHandler(c *gin.Context) { "model_id": modelID, "aspect_ratio": req.AspectRatio, "resolution_level": req.ImageSize, + "quality": req.Quality, "count": req.Count, "reference_images": refImageBytes, // 传递 interface 列表,方便 Provider 类型断言 } diff --git a/backend/internal/api/multipart_helper.go b/backend/internal/api/multipart_helper.go index f2b4239..6645f3b 100644 --- a/backend/internal/api/multipart_helper.go +++ b/backend/internal/api/multipart_helper.go @@ -25,6 +25,7 @@ type MultipartRequest struct { Prompt string AspectRatio string ImageSize string + Quality string Count int Verbose bool PromptOptimizeMode string @@ -86,6 +87,14 @@ func ParseGenerateRequestFromMultipart(c *gin.Context) (*MultipartRequest, error req.ImageSize = string(data) return nil }) + p.Parser.Register("quality", func(reader io.Reader, header formstream.Header) error { + data, err := io.ReadAll(reader) + if err != nil { + return err + } + req.Quality = string(data) + return nil + }) p.Parser.Register("count", func(reader io.Reader, header formstream.Header) error { data, err := io.ReadAll(reader) if err != nil { @@ -172,6 +181,7 @@ func parseWithStandardLibrary(c *gin.Context) (*MultipartRequest, error) { Prompt: c.PostForm("prompt"), AspectRatio: c.PostForm("aspectRatio"), ImageSize: c.PostForm("imageSize"), + Quality: c.PostForm("quality"), Count: 1, } diff --git a/backend/internal/api/multipart_helper_test.go b/backend/internal/api/multipart_helper_test.go new file mode 100644 index 0000000..6edb145 --- /dev/null +++ b/backend/internal/api/multipart_helper_test.go @@ -0,0 +1,49 @@ +package api + +import ( + "bytes" + "mime/multipart" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestParseGenerateRequestFromMultipartIncludesQuality(t *testing.T) { + gin.SetMode(gin.TestMode) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + fields := map[string]string{ + "provider": "openai-image", + "model_id": "gpt-image-2", + "prompt": "edit prompt", + "aspectRatio": "1:1", + "imageSize": "2K", + "quality": "high", + "count": "1", + } + for key, value := range fields { + if err := writer.WriteField(key, value); err != nil { + t.Fatalf("WriteField %s: %v", key, err) + } + } + if err := writer.Close(); err != nil { + t.Fatalf("Close multipart writer: %v", err) + } + + request := httptest.NewRequest(http.MethodPost, "/api/generate-with-images", &body) + request.Header.Set("Content-Type", writer.FormDataContentType()) + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Request = request + + req, err := ParseGenerateRequestFromMultipart(c) + if err != nil { + t.Fatalf("ParseGenerateRequestFromMultipart: %v", err) + } + if req.Quality != "high" { + t.Fatalf("Quality = %q, want high", req.Quality) + } +} diff --git a/backend/internal/api/task_timeout_reconcile.go b/backend/internal/api/task_timeout_reconcile.go index 68b920e..2767801 100644 --- a/backend/internal/api/task_timeout_reconcile.go +++ b/backend/internal/api/task_timeout_reconcile.go @@ -41,6 +41,8 @@ func normalizeProviderForTimeout(providerName string) string { switch { case strings.HasPrefix(name, "gemini"): return "gemini" + case name == "openai-image": + return "openai-image" case strings.HasPrefix(name, "openai"): return "openai" default: diff --git a/backend/internal/api/task_timeout_reconcile_test.go b/backend/internal/api/task_timeout_reconcile_test.go new file mode 100644 index 0000000..ff5993e --- /dev/null +++ b/backend/internal/api/task_timeout_reconcile_test.go @@ -0,0 +1,23 @@ +package api + +import ( + "testing" + "time" +) + +func TestTaskTimeoutForProviderKeepsOpenAIImageConfig(t *testing.T) { + timeoutMap := map[string]time.Duration{ + "openai": 150 * time.Second, + "openai-image": 500 * time.Second, + } + + if got := normalizeProviderForTimeout("openai-image"); got != "openai-image" { + t.Fatalf("normalizeProviderForTimeout(openai-image) = %q, want openai-image", got) + } + if got := taskTimeoutForProvider("openai-image", timeoutMap); got != 500*time.Second { + t.Fatalf("openai-image timeout = %s, want 500s", got) + } + if got := taskTimeoutForProvider("openai-chat", timeoutMap); got != 150*time.Second { + t.Fatalf("openai-chat timeout = %s, want 150s", got) + } +} diff --git a/backend/internal/provider/openai_image.go b/backend/internal/provider/openai_image.go index 1ce6895..bb911b9 100644 --- a/backend/internal/provider/openai_image.go +++ b/backend/internal/provider/openai_image.go @@ -6,8 +6,12 @@ import ( "encoding/base64" "encoding/json" "fmt" + "image" "image-gen-service/internal/diagnostic" "image-gen-service/internal/model" + _ "image/gif" + _ "image/jpeg" + "image/png" "io" "math" "mime/multipart" @@ -17,6 +21,8 @@ import ( "strconv" "strings" "time" + + _ "golang.org/x/image/webp" ) type OpenAIImageProvider struct { @@ -414,20 +420,35 @@ func collectOpenAIImageReferences(raw interface{}) ([]openAIImageReference, erro if len(imgBytes) == 0 { continue } - mimeType := http.DetectContentType(imgBytes) - if mimeType != "image/png" { - return nil, fmt.Errorf("第 %d 张参考图不是有效的 PNG 图片,OpenAI Edit 仅支持 PNG 格式", idx+1) + pngBytes, err := normalizeOpenAIReferenceImagePNG(imgBytes) + if err != nil { + return nil, fmt.Errorf("第 %d 张参考图不是有效图片: %w", idx+1, err) } refs = append(refs, openAIImageReference{ Name: fmt.Sprintf("reference-%d.png", idx+1), - Content: imgBytes, - MIME: mimeType, + Content: pngBytes, + MIME: "image/png", }) } return refs, nil } +func normalizeOpenAIReferenceImagePNG(imgBytes []byte) ([]byte, error) { + if http.DetectContentType(imgBytes) == "image/png" { + return imgBytes, nil + } + img, _, err := image.Decode(bytes.NewReader(imgBytes)) + if err != nil { + return nil, err + } + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + func escapeMultipartFilename(name string) string { return strings.NewReplacer("\\", "\\\\", `"`, "\\\"").Replace(name) } diff --git a/backend/internal/provider/openai_image_test.go b/backend/internal/provider/openai_image_test.go index df308eb..29b18ca 100644 --- a/backend/internal/provider/openai_image_test.go +++ b/backend/internal/provider/openai_image_test.go @@ -1,9 +1,14 @@ package provider import ( + "bytes" "encoding/base64" "encoding/json" + "image" "image-gen-service/internal/model" + "image/color" + "image/jpeg" + "io" "net/http" "net/http/httptest" "strings" @@ -188,6 +193,7 @@ func TestOpenAIImageProviderGenerateWithReferenceUsesEdits(t *testing.T) { "model_id": "gpt-image-2-all", "aspect_ratio": "1:1", "resolution_level": "1K", + "quality": "high", "count": 1, "reference_images": []interface{}{refBytes, refBytes, refBytes}, }) @@ -200,6 +206,9 @@ func TestOpenAIImageProviderGenerateWithReferenceUsesEdits(t *testing.T) { if seenFields["model"] != "gpt-image-2-all" || seenFields["prompt"] != "edit prompt" || seenFields["size"] != "1280x1280" { t.Fatalf("unexpected fields: %+v", seenFields) } + if seenFields["quality"] != "high" { + t.Fatalf("quality = %q, want high", seenFields["quality"]) + } if _, ok := seenFields["input_fidelity"]; ok { t.Fatalf("input_fidelity should not be sent to edits: %+v", seenFields) } @@ -214,10 +223,45 @@ func TestOpenAIImageProviderGenerateWithReferenceUsesEdits(t *testing.T) { } } -func TestOpenAIImageProviderRejectsNonPNGReference(t *testing.T) { +func TestOpenAIImageProviderConvertsJPEGReferenceToPNG(t *testing.T) { + var jpegRef bytes.Buffer + img := image.NewRGBA(image.Rect(0, 0, 1, 1)) + img.Set(0, 0, color.White) + if err := jpeg.Encode(&jpegRef, img, nil); err != nil { + t.Fatalf("encode jpeg: %v", err) + } + + var seenImageContentType string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseMultipartForm(2 << 20); err != nil { + t.Fatalf("ParseMultipartForm: %v", err) + } + files := r.MultipartForm.File["image"] + if len(files) != 1 { + t.Fatalf("image files = %d, want 1", len(files)) + } + seenImageContentType = files[0].Header.Get("Content-Type") + file, err := files[0].Open() + if err != nil { + t.Fatalf("open image part: %v", err) + } + defer file.Close() + content, err := io.ReadAll(file) + if err != nil { + t.Fatalf("read image part: %v", err) + } + if http.DetectContentType(content) != "image/png" { + t.Fatalf("uploaded content type = %q, want image/png", http.DetectContentType(content)) + } + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "data": []map[string]string{{"b64_json": tinyPNGBase64}}, + }) + })) + defer server.Close() + p, err := NewOpenAIImageProvider(&model.ProviderConfig{ ProviderName: "openai-image", - APIBase: "http://example.test/v1", + APIBase: server.URL, APIKey: "test-key", TimeoutSeconds: 5, }) @@ -231,9 +275,19 @@ func TestOpenAIImageProviderRejectsNonPNGReference(t *testing.T) { "aspect_ratio": "1:1", "resolution_level": "1K", "count": 1, - "reference_images": []interface{}{[]byte("not an image")}, + "reference_images": []interface{}{jpegRef.Bytes()}, }) - if err == nil || !strings.Contains(err.Error(), "OpenAI Edit 仅支持 PNG 格式") { - t.Fatalf("Generate error = %v, want PNG validation error", err) + if err != nil { + t.Fatalf("Generate: %v", err) + } + if seenImageContentType != "image/png" { + t.Fatalf("image part Content-Type = %q, want image/png", seenImageContentType) + } +} + +func TestOpenAIImageProviderRejectsInvalidReference(t *testing.T) { + _, err := collectOpenAIImageReferences([]interface{}{[]byte("not an image")}) + if err == nil || !strings.Contains(err.Error(), "不是有效图片") { + t.Fatalf("collectOpenAIImageReferences error = %v, want invalid image error", err) } } diff --git a/backend/internal/worker/pool.go b/backend/internal/worker/pool.go index f34f272..f3d814d 100644 --- a/backend/internal/worker/pool.go +++ b/backend/internal/worker/pool.go @@ -480,13 +480,15 @@ func fetchProviderTimeout(providerName string) time.Duration { name := strings.TrimSpace(strings.ToLower(providerName)) if strings.HasPrefix(name, "gemini") { name = "gemini" + } else if name == "openai-image" { + name = "openai-image" } else if strings.HasPrefix(name, "openai") { name = "openai" } defaultTimeout := func(p string) time.Duration { switch p { - case "gemini", "openai": + case "gemini", "openai", "openai-image": return 500 * time.Second default: return 150 * time.Second diff --git a/backend/internal/worker/pool_test.go b/backend/internal/worker/pool_test.go new file mode 100644 index 0000000..a91eed2 --- /dev/null +++ b/backend/internal/worker/pool_test.go @@ -0,0 +1,44 @@ +package worker + +import ( + "testing" + "time" + + "image-gen-service/internal/model" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestFetchProviderTimeoutKeepsOpenAIImageConfig(t *testing.T) { + originalDB := model.DB + t.Cleanup(func() { + model.DB = originalDB + }) + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open test database: %v", err) + } + if err := db.AutoMigrate(&model.ProviderConfig{}); err != nil { + t.Fatalf("migrate provider config: %v", err) + } + model.DB = db + + configs := []model.ProviderConfig{ + {ProviderName: "openai", TimeoutSeconds: 150}, + {ProviderName: "openai-image", TimeoutSeconds: 500}, + } + for _, cfg := range configs { + if err := db.Create(&cfg).Error; err != nil { + t.Fatalf("create provider config %s: %v", cfg.ProviderName, err) + } + } + + if got := fetchProviderTimeout("openai-image"); got != 500*time.Second { + t.Fatalf("openai-image timeout = %s, want 500s", got) + } + if got := fetchProviderTimeout("openai"); got != 150*time.Second { + t.Fatalf("openai timeout = %s, want 150s", got) + } +} diff --git a/desktop/package-lock.json b/desktop/package-lock.json index c55d73c..b955431 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "nano-banana-pro-frontend", - "version": "2.8.1", + "version": "2.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nano-banana-pro-frontend", - "version": "2.8.1", + "version": "2.8.2", "license": "MIT", "dependencies": { "@tanstack/react-query": "^5.59.20", diff --git a/desktop/package.json b/desktop/package.json index 866be6b..9d1dece 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "nano-banana-pro-frontend", "private": true, - "version": "2.8.1", + "version": "2.8.2", "license": "MIT", "description": "大香蕉图片生成工具 - 批量图片生成应用前端", "type": "module", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 26d5067..9f6909b 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -744,7 +744,7 @@ dependencies = [ [[package]] name = "desktop" -version = "2.8.1" +version = "2.8.2" dependencies = [ "arboard", "image", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 42daf36..9175cce 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "desktop" -version = "2.8.1" +version = "2.8.2" description = "A Tauri App" authors = ["you"] edition = "2021" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 0c9c697..9ac6137 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "大香蕉 AI", - "version": "2.8.1", + "version": "2.8.2", "identifier": "com.dztool.banana", "build": { "beforeDevCommand": "npm run dev", diff --git a/desktop/src/components/ConfigPanel/BatchSettings.tsx b/desktop/src/components/ConfigPanel/BatchSettings.tsx index 298bc18..f0d0013 100644 --- a/desktop/src/components/ConfigPanel/BatchSettings.tsx +++ b/desktop/src/components/ConfigPanel/BatchSettings.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import { useConfigStore, getModelAspectRatios, - usesNativeImageSize, - supportsQualityControl, + isUsingNativeImageSize, + isQualityControlSupported, OPENAI_IMAGE_SIZE_OPTIONS, OPENAI_IMAGE_QUALITY_OPTIONS } from '../../store/configStore'; @@ -31,8 +31,8 @@ export function BatchSettings() { } = useConfigStore(); const supportedRatios = useMemo(() => getModelAspectRatios(imageModel), [imageModel]); - const useNativeSize = usesNativeImageSize(imageProvider); - const useQuality = supportsQualityControl(imageProvider); + const useNativeSize = isUsingNativeImageSize(imageProvider, imageModel); + const useQuality = isQualityControlSupported(imageProvider); useEffect(() => { if (useNativeSize) { @@ -87,18 +87,18 @@ export function BatchSettings() { - {useNativeSize ? ( - useQuality ? ( -
- - -
- ) : null - ) : ( + {useQuality ? ( +
+ + +
+ ) : null} + + {!useNativeSize ? (
- )} + ) : null} ); } diff --git a/desktop/src/components/ConfigPanel/ReferenceImageUpload.tsx b/desktop/src/components/ConfigPanel/ReferenceImageUpload.tsx index 0b07076..1141b21 100644 --- a/desktop/src/components/ConfigPanel/ReferenceImageUpload.tsx +++ b/desktop/src/components/ConfigPanel/ReferenceImageUpload.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState, useEffect, useCallback } from 'react'; import { ImagePlus, X, Image as ImageIcon, ChevronDown, ChevronUp, Sparkles, Loader2 } from 'lucide-react'; -import { useConfigStore, supportsReferenceImages } from '../../store/configStore'; +import { useConfigStore, isReferenceImageSupported } from '../../store/configStore'; import { useInternalDragStore, type InternalDragPayload } from '../../store/internalDragStore'; import { cn } from '../common/Button'; import { toast } from '../../store/toastStore'; @@ -156,7 +156,7 @@ export function ReferenceImageUpload() { // 图片反推提示词相关状态 const [isExtractingPrompt, setIsExtractingPrompt] = useState(false); const [extractingIndex, setExtractingIndex] = useState(null); - const allowReferenceImages = supportsReferenceImages(imageProvider); + const allowReferenceImages = isReferenceImageSupported(imageProvider); const prevAllowReferenceImagesRef = useRef(allowReferenceImages); // 计算文件 MD5(使用工具函数) diff --git a/desktop/src/hooks/useGenerate.ts b/desktop/src/hooks/useGenerate.ts index 1cfb7a8..527a72f 100644 --- a/desktop/src/hooks/useGenerate.ts +++ b/desktop/src/hooks/useGenerate.ts @@ -10,7 +10,7 @@ import { useHistoryStore } from '../store/historyStore'; import i18n from '../i18n'; import { getDiagnosticVerbose } from '../utils/diagnosticLogger'; import { getPromptOptimizeConfigIssue } from '../utils/promptOptimizeConfig'; -import { supportsReferenceImages, usesNativeImageSize } from '../store/configStore'; +import { isQualityControlSupported, isReferenceImageSupported, isUsingNativeImageSize } from '../store/configStore'; // 流式连接建立超时时间(毫秒)- 超过此时间未建立连接则启动轮询 // 本地后端通常不会推实时进度,过长会导致用户"卡住"的观感 @@ -409,27 +409,34 @@ export function useGenerate() { const requestedCount = Math.max(1, Number(config.count) || 1); const promptOptimizeMode = config.defaultPromptOptimizeMode || 'off'; const shouldAutoOptimizePrompt = promptOptimizeMode !== 'off'; - const useNativeSize = usesNativeImageSize(config.imageProvider); - const allowReferenceImages = supportsReferenceImages(config.imageProvider); + const useNativeSize = isUsingNativeImageSize(config.imageProvider, config.imageModel); + const useQuality = isQualityControlSupported(config.imageProvider); + const allowReferenceImages = isReferenceImageSupported(config.imageProvider); + const qualityParams = useQuality + ? { + quality: config.imageQuality, + } + : {}; const taskOptions = useNativeSize ? { size: config.imageNativeSize, - quality: config.imageQuality, + ...qualityParams, } : { aspectRatio: config.aspectRatio, imageSize: config.imageSize, + ...qualityParams, }; const buildImageParams = (count: number) => ({ prompt: config.prompt, count, ...(useNativeSize ? { size: config.imageNativeSize, - quality: config.imageQuality, } : { aspectRatio: config.aspectRatio, imageSize: config.imageSize, }), + ...qualityParams, verbose_logging: verboseLogging, ...(shouldAutoOptimizePrompt ? { prompt_optimize_mode: promptOptimizeMode, @@ -446,11 +453,13 @@ export function useGenerate() { formData.append('model_id', config.imageModel); if (useNativeSize) { formData.append('size', config.imageNativeSize); - formData.append('quality', config.imageQuality); } else { formData.append('aspectRatio', config.aspectRatio); formData.append('imageSize', config.imageSize); } + if (useQuality) { + formData.append('quality', config.imageQuality); + } formData.append('count', '1'); formData.append('verbose_logging', String(verboseLogging)); if (shouldAutoOptimizePrompt) { @@ -618,11 +627,13 @@ export function useGenerate() { formData.append('model_id', config.imageModel); if (useNativeSize) { formData.append('size', config.imageNativeSize); - formData.append('quality', config.imageQuality); } else { formData.append('aspectRatio', config.aspectRatio); formData.append('imageSize', config.imageSize); } + if (useQuality) { + formData.append('quality', config.imageQuality); + } formData.append('count', requestedCount.toString()); formData.append('verbose_logging', String(verboseLogging)); if (shouldAutoOptimizePrompt) { diff --git a/desktop/src/store/configStore.ts b/desktop/src/store/configStore.ts index d6b69c8..0a16442 100644 --- a/desktop/src/store/configStore.ts +++ b/desktop/src/store/configStore.ts @@ -9,6 +9,7 @@ const IMAGE_MODELS = { const OPENAI_IMAGE_MODELS = { GPT_IMAGE_1: { value: 'gpt-image-2-all', label: 'GPT Image 2 All' }, + GPT_IMAGE_2: { value: 'gpt-image-2', label: 'GPT Image 2' }, } as const; export const OPENAI_IMAGE_SIZE_OPTIONS = [ @@ -33,6 +34,7 @@ export const GEMINI_IMAGE_MODEL_OPTIONS = [ export const OPENAI_IMAGE_MODEL_OPTIONS = [ { value: OPENAI_IMAGE_MODELS.GPT_IMAGE_1.value, label: `${OPENAI_IMAGE_MODELS.GPT_IMAGE_1.label} (${OPENAI_IMAGE_MODELS.GPT_IMAGE_1.value})` }, + { value: OPENAI_IMAGE_MODELS.GPT_IMAGE_2.value, label: `${OPENAI_IMAGE_MODELS.GPT_IMAGE_2.label} (${OPENAI_IMAGE_MODELS.GPT_IMAGE_2.value})` }, ] as const; export const IMAGE_MODEL_OPTIONS = GEMINI_IMAGE_MODEL_OPTIONS; @@ -69,12 +71,23 @@ export const IMAGE_MODEL_CONFIG: Record = { }, [IMAGE_MODELS.PRO.value]: { aspectRatios: ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'] + }, + [OPENAI_IMAGE_MODELS.GPT_IMAGE_1.value]: { + aspectRatios: ['1:1', '1:4', '1:8', '2:3', '3:2', '3:4', '4:1', '4:3', '4:5', '5:4', '8:1', '9:16', '16:9', '21:9'] + }, + [OPENAI_IMAGE_MODELS.GPT_IMAGE_2.value]: { + aspectRatios: ['1:1', '1:4', '1:8', '2:3', '3:2', '3:4', '4:1', '4:3', '4:5', '5:4', '8:1', '9:16', '16:9', '21:9'] } }; -export const supportsReferenceImages = (provider: string): boolean => provider !== 'openai-image'; -export const usesNativeImageSize = (provider: string): boolean => provider === 'openai-image'; -export const supportsQualityControl = (provider: string): boolean => provider === 'openai-image'; +export const isUsingDynamicOpenAIImageSize = (provider: string, model?: string): boolean => ( + provider === 'openai-image' && String(model || '').toLowerCase().includes('gpt-image-2') +); +export const isReferenceImageSupported = (_provider: string): boolean => true; +export const isUsingNativeImageSize = (provider: string, model?: string): boolean => ( + provider === 'openai-image' && !isUsingDynamicOpenAIImageSize(provider, model) +); +export const isQualityControlSupported = (provider: string): boolean => provider === 'openai-image'; // Helper function to get supported aspect ratios for a model export const getModelAspectRatios = (model: string): string[] => {