diff --git a/backend/internal/service/image_generation_intent.go b/backend/internal/service/image_generation_intent.go index b6ef106509d..4b278481a01 100644 --- a/backend/internal/service/image_generation_intent.go +++ b/backend/internal/service/image_generation_intent.go @@ -173,6 +173,7 @@ func cloneRequestMapForImageIntent(body []byte) map[string]any { func resolveOpenAIResponsesImageBillingConfig(reqBody map[string]any, fallbackModel string) (string, string, error) { imageModel := "" imageSize := "" + imageQuality := "" hasImageTool := false if reqBody != nil { rawTools, _ := reqBody["tools"].([]any) @@ -184,11 +185,15 @@ func resolveOpenAIResponsesImageBillingConfig(reqBody map[string]any, fallbackMo hasImageTool = true imageModel = strings.TrimSpace(firstNonEmptyString(toolMap["model"])) imageSize = strings.TrimSpace(firstNonEmptyString(toolMap["size"])) + imageQuality = strings.TrimSpace(firstNonEmptyString(toolMap["quality"])) break } if imageSize == "" { imageSize = strings.TrimSpace(firstNonEmptyString(reqBody["size"])) } + if imageQuality == "" { + imageQuality = strings.TrimSpace(firstNonEmptyString(reqBody["quality"])) + } } if imageModel == "" && reqBody != nil { bodyModel := strings.TrimSpace(firstNonEmptyString(reqBody["model"])) @@ -202,7 +207,7 @@ func resolveOpenAIResponsesImageBillingConfig(reqBody map[string]any, fallbackMo if imageModel == "" { imageModel = strings.TrimSpace(fallbackModel) } - sizeTier := normalizeOpenAIImageSizeTier(imageSize) + sizeTier := normalizeOpenAIImageSizeTier(imageSize, imageQuality) return imageModel, sizeTier, nil } diff --git a/backend/internal/service/openai_images.go b/backend/internal/service/openai_images.go index afa94156867..9b7f5aeaba7 100644 --- a/backend/internal/service/openai_images.go +++ b/backend/internal/service/openai_images.go @@ -218,7 +218,7 @@ func (s *OpenAIGatewayService) ParseOpenAIImagesRequest(c *gin.Context, body []b if err := validateOpenAIImagesModel(req.Model); err != nil { return nil, err } - req.SizeTier = normalizeOpenAIImageSizeTier(req.Size) + req.SizeTier = normalizeOpenAIImageSizeTier(req.Size, req.Quality) req.RequiredCapability = classifyOpenAIImagesCapability(req) return req, nil } @@ -531,7 +531,24 @@ func isOpenAINativeImageOption(name string) bool { } } -func normalizeOpenAIImageSizeTier(size string) string { +// normalizeOpenAIImageSizeTier maps an image request to a billing tier +// ("1K"/"2K"/"4K") consumed by BillingService.CalculateImageCost and the +// Group.ImagePrice1K/2K/4K subscription pricing fields. +// +// gpt-image-2's image_tokens scale primarily with `quality`, not pixel size +// (low ≈ 1k tokens, medium ≈ 2k, high ≈ 4k), so an explicit quality value +// takes precedence. When quality is empty / "auto" / unrecognized we fall +// back to size-dimension classification for legacy callers (older +// gpt-image-1, dall-e flows). +func normalizeOpenAIImageSizeTier(size, quality string) string { + switch strings.ToLower(strings.TrimSpace(quality)) { + case "low": + return "1K" + case "medium": + return "2K" + case "high": + return "4K" + } trimmed := strings.TrimSpace(size) normalized := strings.ToLower(trimmed) switch normalized { diff --git a/backend/internal/service/openai_images_test.go b/backend/internal/service/openai_images_test.go index 45fb24e975e..28eea8c5e37 100644 --- a/backend/internal/service/openai_images_test.go +++ b/backend/internal/service/openai_images_test.go @@ -51,7 +51,7 @@ func TestOpenAIGatewayServiceParseOpenAIImagesRequest_JSON(t *testing.T) { require.Equal(t, "draw a cat", parsed.Prompt) require.True(t, parsed.Stream) require.Equal(t, "1024x1024", parsed.Size) - require.Equal(t, "1K", parsed.SizeTier) + require.Equal(t, "4K", parsed.SizeTier) require.Equal(t, OpenAIImagesCapabilityNative, parsed.RequiredCapability) require.False(t, parsed.Multipart) }