fix(openai): address image route review findings

This commit is contained in:
song
2026-09-11 16:30:32 +08:00
parent 04c45e9dc2
commit 1067e89fab
7 changed files with 170 additions and 63 deletions
@@ -3020,7 +3020,7 @@ func (s *AccountTestService) testOpenAIImageAPIKey(c *gin.Context, ctx context.C
return nil
}
// testOpenAIImageOAuth tests OpenAI image generation using an OAuth account via Codex /responses API.
// OAuth 图片测试与正式转发共用 Codex Images / Responses 分流规则。
func (s *AccountTestService) testOpenAIImageOAuth(c *gin.Context, ctx context.Context, account *Account, modelID, prompt string) error {
credentialAccount := account
if account.IsShadow() {
@@ -3046,7 +3046,6 @@ func (s *AccountTestService) testOpenAIImageOAuth(c *gin.Context, ctx context.Co
c.Writer.Flush()
s.sendEvent(c, TestEvent{Type: "test_start", Model: modelID})
s.sendEvent(c, TestEvent{Type: "content", Text: "Calling Codex /responses image tool...\n"})
parsed := &OpenAIImagesRequest{
Endpoint: openAIImagesGenerationsEndpoint,
@@ -3055,14 +3054,19 @@ func (s *AccountTestService) testOpenAIImageOAuth(c *gin.Context, ctx context.Co
}
applyOpenAIImagesDefaults(parsed)
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Responses driver: %s; image model: %s\n", openAIImagesResponsesMainModelValue(), parsed.Model)})
responsesBody, err := buildOpenAIImagesResponsesRequest(parsed, parsed.Model)
upstreamModel := account.GetMappedModel(parsed.Model)
responsesBody, targetURL, err := buildOpenAIImagesOAuthPayload(parsed, upstreamModel)
if err != nil {
return s.sendErrorAndEnd(c, fmt.Sprintf("Failed to build image request: %s", err.Error()))
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, chatgptCodexAPIURL, bytes.NewReader(responsesBody))
direct := usesCodexDirectImages(upstreamModel)
if direct {
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Calling Codex /images/generations; image model: %s\n", upstreamModel)})
} else {
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Calling Codex /responses image tool; driver: %s; image model: %s\n", openAIImagesResponsesMainModelValue(), upstreamModel)})
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(responsesBody))
if err != nil {
return s.sendErrorAndEnd(c, "Failed to create request")
}
@@ -3084,6 +3088,10 @@ func (s *AccountTestService) testOpenAIImageOAuth(c *gin.Context, ctx context.Co
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("OpenAI-Beta", "responses=experimental")
if direct {
req.Header.Del("OpenAI-Beta")
req.Header.Set("Accept", "application/json")
}
canonical := resolveCodexOutboundIdentity("")
req.Header.Set("originator", canonical.originator)
if customUA := strings.TrimSpace(credentialAccount.GetOpenAIUserAgent()); customUA != "" {
@@ -3102,7 +3110,7 @@ func (s *AccountTestService) testOpenAIImageOAuth(c *gin.Context, ctx context.Co
}
resp, err := s.doOpenAIAccountTestUpstream(req, proxyURL, account, false)
if err != nil {
return s.sendErrorAndEnd(c, fmt.Sprintf("Responses API request failed: %s", err.Error()))
return s.sendErrorAndEnd(c, fmt.Sprintf("Image upstream request failed: %s", err.Error()))
}
defer func() {
if resp != nil && resp.Body != nil {
@@ -3114,7 +3122,7 @@ func (s *AccountTestService) testOpenAIImageOAuth(c *gin.Context, ctx context.Co
body = redactAgentIdentitySensitiveBodyForAccount(ctx, s.accountRepo, credentialAccount, body)
message := strings.TrimSpace(extractUpstreamErrorMessage(body))
if message == "" {
message = fmt.Sprintf("Responses API returned %d", resp.StatusCode)
message = fmt.Sprintf("Image upstream returned %d", resp.StatusCode)
}
return s.sendErrorAndEnd(c, message)
}
@@ -3125,18 +3133,26 @@ func (s *AccountTestService) testOpenAIImageOAuth(c *gin.Context, ctx context.Co
}
body = redactAgentIdentitySensitiveBodyForAccount(ctx, s.accountRepo, credentialAccount, body)
results, _, _, _, _, err := collectOpenAIImagesFromResponsesBody(body)
var results []openAIResponsesImageResult
if direct {
results, err = parseCodexDirectImagesResponse(body)
} else {
if upstreamErr := extractOpenAIImagesUpstreamError(body); upstreamErr != nil {
return s.sendErrorAndEnd(c, upstreamErr.clientMessage())
}
results, _, _, _, _, err = collectOpenAIImagesFromResponsesBody(body)
}
if err != nil {
return s.sendErrorAndEnd(c, fmt.Sprintf("Failed to parse image response: %s", err.Error()))
}
if len(results) == 0 {
if upstreamErr := extractOpenAIImagesUpstreamError(body); upstreamErr != nil {
return s.sendErrorAndEnd(c, upstreamErr.clientMessage())
}
if textErr := openAIImagesTextFallbackError(body); textErr != nil {
return s.sendErrorAndEnd(c, textErr.clientMessage())
}
return s.sendErrorAndEnd(c, "No images returned from responses API")
if direct {
return s.sendErrorAndEnd(c, "No images returned from Codex Images API")
}
return s.sendErrorAndEnd(c, "No images returned from Codex Responses API")
}
for _, item := range results {
@@ -2131,8 +2131,11 @@ func TestOpenAIGatewayServiceRecordUsage_OutputImageSizeWinsBeforeBillingAndPers
err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
Result: &OpenAIForwardResult{
RequestID: "resp_image_output_size",
Model: "gpt-image-2",
RequestID: "resp_image_output_size",
Model: "gpt-image-2",
Usage: OpenAIUsage{
ImageCacheReadTokens: 40,
},
ImageCount: 1,
ImageInputSize: "1024x1024",
ImageOutputSizes: []string{"3840x2160"},
@@ -2162,7 +2165,7 @@ func TestOpenAIGatewayServiceRecordUsage_OutputImageSizeWinsBeforeBillingAndPers
require.Equal(t, "3840x2160", *usageRepo.lastLog.ImageOutputSize)
require.NotNil(t, usageRepo.lastLog.ImageSizeSource)
require.Equal(t, ImageSizeSourceOutput, *usageRepo.lastLog.ImageSizeSource)
require.Equal(t, map[string]int{ImageBillingSize4K: 1}, usageRepo.lastLog.ImageSizeBreakdown)
require.Equal(t, map[string]int{ImageBillingSize4K: 1, "image_cache_read_tokens": 40}, usageRepo.lastLog.ImageSizeBreakdown)
require.InDelta(t, 0.44, usageRepo.lastLog.TotalCost, 1e-12)
require.InDelta(t, 0.44, usageRepo.lastLog.ActualCost, 1e-12)
}
@@ -367,6 +367,14 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
)
}
imageSizeBreakdown := cloneImageSizeBreakdown(result.ImageSizeBreakdown)
if result.Usage.ImageCacheReadTokens > 0 {
if imageSizeBreakdown == nil {
imageSizeBreakdown = make(map[string]int)
}
// Keep the image cache split in the existing usage_logs JSONB payload.
imageSizeBreakdown["image_cache_read_tokens"] = result.Usage.ImageCacheReadTokens
}
usageLog := &UsageLog{
UserID: user.ID,
APIKeyID: apiKey.ID,
@@ -394,7 +402,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
ImageInputSize: optionalTrimmedStringPtr(result.ImageInputSize),
ImageOutputSize: optionalTrimmedStringPtr(result.ImageOutputSize),
ImageSizeSource: optionalTrimmedStringPtr(result.ImageSizeSource),
ImageSizeBreakdown: result.ImageSizeBreakdown,
ImageSizeBreakdown: imageSizeBreakdown,
NativeCompactionV2: input.NativeCompactionV2,
}
isVideoUsage := isGrokVideoUsageResult(result, billingModels)
@@ -1145,3 +1153,14 @@ func (s *OpenAIGatewayService) UpdateCodexUsageSnapshotFromHeaders(ctx context.C
s.updateCodexUsageSnapshot(ctx, accountID, snapshot)
}
}
func cloneImageSizeBreakdown(input map[string]int) map[string]int {
if len(input) == 0 {
return nil
}
output := make(map[string]int, len(input))
for key, value := range input {
output[key] = value
}
return output
}
+12 -7
View File
@@ -692,7 +692,7 @@ func (s *OpenAIGatewayService) forwardOpenAIImagesAPIKey(
imageCount := parsed.N
var firstTokenMs *int
if parsed.Stream && isEventStreamResponse(resp.Header) {
streamUsage, streamCount, streamSizes, ttft, err := s.handleOpenAIImagesStreamingResponse(resp, c, startTime)
streamUsage, streamCount, streamSizes, ttft, err := s.handleOpenAIImagesStreamingResponse(resp, c, startTime, nil)
if err != nil {
if streamCount > 0 {
return &OpenAIForwardResult{
@@ -934,12 +934,8 @@ func (s *OpenAIGatewayService) handleOpenAIImagesStreamingResponse(
resp *http.Response,
c *gin.Context,
startTime time.Time,
directRequests ...*OpenAIImagesRequest,
direct *OpenAIImagesRequest,
) (OpenAIUsage, int, []string, *int, error) {
var direct *OpenAIImagesRequest
if len(directRequests) > 0 {
direct = directRequests[0]
}
responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
if contentType == "" {
@@ -972,13 +968,16 @@ func (s *OpenAIGatewayService) handleOpenAIImagesStreamingResponse(
if streamErr != nil {
return streamErr
}
if !seenSSEData || imageCounter.Count() < max(direct.N, 1) {
if !seenSSEData || imageCounter.Count() == 0 {
return newOpenAIUpstreamStreamReadError(ErrOpenAIUpstreamStreamTruncated)
}
return nil
}
processSSEData := func(dataBytes []byte) {
if streamErr != nil {
return
}
seenSSEData = true
fallbackBody.Reset()
fallbackBytes = 0
@@ -1000,12 +999,18 @@ func (s *OpenAIGatewayService) handleOpenAIImagesStreamingResponse(
}
if upstreamErr := openAIImagesUpstreamErrorFromSSEPayload(dataBytes); upstreamErr != nil {
streamErr = upstreamErr
if IsOpenAIImagesRetryableUpstreamError(upstreamErr) && imageCounter.Count() == 0 {
return
}
}
if !gjson.ValidBytes(dataBytes) {
streamErr = newOpenAIUpstreamStreamReadError(fmt.Errorf("invalid image stream JSON"))
return
}
eventType := gjson.GetBytes(dataBytes, "type").String()
if direct != nil && strings.TrimSpace(direct.Model) != "" {
dataBytes, _ = sjson.SetBytes(dataBytes, "model", strings.TrimSpace(direct.Model))
}
// 原生编辑事件可能仍使用 image_generation 前缀;对外维持既有编辑事件名。
if strings.HasPrefix(eventType, "image_generation.") && direct.IsEdits() {
eventType = strings.Replace(eventType, "image_generation.", "image_edit.", 1)
@@ -1,6 +1,8 @@
package service
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
@@ -11,6 +13,20 @@ import (
"github.com/tidwall/sjson"
)
type openAIImagesForceResponsesContextKey struct{}
func withOpenAIImagesForceResponses(ctx context.Context) context.Context {
return context.WithValue(ctx, openAIImagesForceResponsesContextKey{}, true)
}
func isOpenAIImagesForceResponses(ctx context.Context) bool {
if ctx == nil {
return false
}
forced, _ := ctx.Value(openAIImagesForceResponsesContextKey{}).(bool)
return forced
}
// 显式列出已接入的模型,不把未来模型或未知快照自动送到直调端点。
func usesCodexDirectImages(model string) bool {
switch strings.TrimSpace(model) {
@@ -35,47 +51,50 @@ func buildOpenAIImagesOAuthPayload(parsed *OpenAIImagesRequest, model string) ([
if strings.TrimSpace(parsed.Prompt) == "" {
return nil, "", fmt.Errorf("prompt is required")
}
// JSON 参数保持透传;multipart 则将已校验的字段与上传图片转换为 JSON。
body := []byte(`{}`)
// 只把已解析并校验的图片字段发送给上游,避免客户端注入任意 JSON 字段。
payload := make(map[string]any, 16)
payload["model"] = model
prompt := parsed.Prompt
if !parsed.Multipart && gjson.ValidBytes(parsed.Body) {
body = append([]byte(nil), parsed.Body...)
if rawPrompt := gjson.GetBytes(parsed.Body, "prompt").String(); rawPrompt != "" {
prompt = rawPrompt
}
}
body, _ = sjson.SetBytes(body, "model", model)
if !gjson.GetBytes(body, "prompt").Exists() {
body, _ = sjson.SetBytes(body, "prompt", parsed.Prompt)
}
for _, field := range []struct{ key, value string }{
payload["prompt"] = prompt
for _, field := range []struct {
key string
value string
}{
{"size", parsed.Size}, {"quality", parsed.Quality},
{"background", parsed.Background}, {"output_format", parsed.OutputFormat},
{"moderation", parsed.Moderation}, {"input_fidelity", parsed.InputFidelity},
{"style", parsed.Style},
} {
if field.value != "" && !gjson.GetBytes(body, field.key).Exists() {
body, _ = sjson.SetBytes(body, field.key, field.value)
if value := strings.TrimSpace(field.value); value != "" {
payload[field.key] = value
}
}
if parsed.N > 0 {
body, _ = sjson.SetBytes(body, "n", parsed.N)
if parsed.N > 1 {
payload["n"] = parsed.N
}
if parsed.OutputCompression != nil {
body, _ = sjson.SetBytes(body, "output_compression", *parsed.OutputCompression)
payload["output_compression"] = *parsed.OutputCompression
}
if parsed.PartialImages != nil {
body, _ = sjson.SetBytes(body, "partial_images", *parsed.PartialImages)
payload["partial_images"] = *parsed.PartialImages
}
// GPT Image 原生返回 base64;对外 url 格式由网关转换为 data URL。
body, _ = sjson.DeleteBytes(body, "response_format")
if parsed.Stream {
body, _ = sjson.SetBytes(body, "stream", true)
} else {
body, _ = sjson.DeleteBytes(body, "stream")
payload["stream"] = true
}
endpoint := "/images/generations"
if parsed.IsEdits() {
endpoint = "/images/edits"
images := make([]map[string]string, 0, len(parsed.InputImageURLs)+len(parsed.Uploads))
for _, url := range parsed.InputImageURLs {
images = append(images, map[string]string{"image_url": url})
for _, imageURL := range parsed.InputImageURLs {
if imageURL = strings.TrimSpace(imageURL); imageURL != "" {
images = append(images, map[string]string{"image_url": imageURL})
}
}
for _, upload := range parsed.Uploads {
url, err := openAIImageUploadToDataURL(upload)
@@ -87,8 +106,8 @@ func buildOpenAIImagesOAuthPayload(parsed *OpenAIImagesRequest, model string) ([
if len(images) == 0 {
return nil, "", fmt.Errorf("image input is required")
}
body, _ = sjson.SetBytes(body, "images", images)
mask := parsed.MaskImageURL
payload["images"] = images
mask := strings.TrimSpace(parsed.MaskImageURL)
if parsed.MaskUpload != nil {
var err error
mask, err = openAIImageUploadToDataURL(*parsed.MaskUpload)
@@ -97,9 +116,13 @@ func buildOpenAIImagesOAuthPayload(parsed *OpenAIImagesRequest, model string) ([
}
}
if mask != "" {
body, _ = sjson.SetBytes(body, "mask.image_url", mask)
payload["mask"] = map[string]string{"image_url": mask}
}
}
body, err := json.Marshal(payload)
if err != nil {
return nil, "", fmt.Errorf("marshal Codex Images request: %w", err)
}
return body, strings.TrimSuffix(chatgptCodexURL, "/responses") + endpoint, nil
}
@@ -149,8 +172,13 @@ func codexDirectImageURL(body []byte, path, outputFormat string) []byte {
}
func isOpenAIImagesMainModelError(status int, body []byte) bool {
return isOpenAICodexPlanGatedModelError(status, body) &&
strings.Contains(extractUpstreamErrorMessage(body), openAIImagesResponsesMainModel)
if !isOpenAICodexPlanGatedModelError(status, body) {
return false
}
message := extractUpstreamErrorMessage(body)
model := openAIImagesResponsesMainModelValue()
return strings.Contains(message, "'"+model+"'") ||
strings.Contains(message, `"`+model+`"`)
}
// Images 端点只输出图片;未提供输出分类时,output_tokens 全部是图片 token。
@@ -195,6 +223,12 @@ func (s *OpenAIGatewayService) handleCodexDirectImagesNonStreamingResponse(resp
observer.Observe(result.Model, true)
}
}
clientModel := strings.TrimSpace(parsed.Model)
if clientModel != "" {
for i := range gjson.GetBytes(body, "data").Array() {
body, _ = sjson.SetBytes(body, fmt.Sprintf("data.%d.model", i), clientModel)
}
}
for i, item := range gjson.GetBytes(body, "data").Array() {
if actualSize := detectOpenAIImageResultSize(item.Get("b64_json").String()); actualSize != "" {
body, _ = sjson.SetBytes(body, fmt.Sprintf("data.%d.size", i), actualSize)
@@ -216,6 +250,12 @@ func (s *OpenAIGatewayService) handleCodexDirectImagesNonStreamingResponse(resp
}
}
responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
c.Data(resp.StatusCode, "application/json", body)
contentType := "application/json"
if s.cfg != nil && !s.cfg.Security.ResponseHeaders.Enabled {
if upstreamType := strings.TrimSpace(resp.Header.Get("Content-Type")); upstreamType != "" {
contentType = upstreamType
}
}
c.Data(resp.StatusCode, contentType, body)
return usage, len(results), openAIResponsesImageResultSizes(results), nil
}
@@ -43,7 +43,7 @@ func TestCodexDirectImagesRouting(t *testing.T) {
require.NotEmpty(t, upstream.lastReq.Header.Get("User-Agent"))
require.Equal(t, model, gjson.GetBytes(upstream.lastBody, "model").String())
require.Equal(t, " 原样保留 prompt ", gjson.GetBytes(upstream.lastBody, "prompt").String())
require.True(t, gjson.GetBytes(upstream.lastBody, "extra.preserve").Bool())
require.False(t, gjson.GetBytes(upstream.lastBody, "extra").Exists())
for _, key := range []string{"tools", "reasoning", "instructions", "response_format", "stream"} {
require.False(t, gjson.GetBytes(upstream.lastBody, key).Exists(), key)
}
@@ -80,23 +80,33 @@ func TestCodexDirectImagesMappingBeforeRouting(t *testing.T) {
}
}
func TestCodexDirectImagesHTTPErrorDoesNotFallback(t *testing.T) {
for _, status := range []int{400, 401, 403, 429, 500, 502, 503} {
func TestCodexDirectImagesHTTPErrorFallbacksOnlyWhenEndpointUnavailable(t *testing.T) {
for _, status := range []int{400, 401, 403, 429, 500, 502, 503, 404, 405} {
t.Run(fmt.Sprint(status), func(t *testing.T) {
calls := 0
upstream := &codexModelsHTTPUpstreamStub{do: func(req *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
calls++
require.Equal(t, "/backend-api/codex/images/generations", req.URL.Path)
return &http.Response{StatusCode: status, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"error":{"type":"server_error","message":"image request rejected"}}`))}, nil
if calls == 1 {
require.Equal(t, "/backend-api/codex/images/generations", req.URL.Path)
return &http.Response{StatusCode: status, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"error":{"type":"server_error","message":"image request rejected"}}`))}, nil
}
require.Contains(t, req.URL.Path, "/backend-api/codex/responses")
return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": {"text/event-stream"}}, Body: io.NopCloser(strings.NewReader("data: {\"type\":\"response.completed\",\"response\":{\"output\":[{\"type\":\"image_generation_call\",\"result\":\"aGVsbG8=\"}]}}\n\n"))}, nil
}}
body := []byte(`{"model":"gpt-image-2.5-sunburst","prompt":"draw"}`)
c, _ := newOpenAIImagesTestContext(t, body)
svc := newOpenAIImagesTestService(upstream)
parsed, err := svc.ParseOpenAIImagesRequest(c, body)
require.NoError(t, err)
_, err = svc.ForwardImages(context.Background(), c, directImagesTestAccount(), body, parsed, "")
require.Error(t, err)
require.Equal(t, 1, calls)
result, err := svc.ForwardImages(context.Background(), c, directImagesTestAccount(), body, parsed, "")
if status == http.StatusNotFound || status == http.StatusMethodNotAllowed {
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, 2, calls)
} else {
require.Error(t, err)
require.Equal(t, 1, calls)
}
})
}
}
@@ -143,11 +153,15 @@ func TestCodexDirectImagesStreaming(t *testing.T) {
{"duplicate", "complete,complete", 1, false, false},
{"disconnect", "partial,complete", 1, false, true},
{"truncated", "partial", 0, true, false},
{"partial_requested_multiple", "complete", 1, false, false},
{"error", "error", 0, true, false},
{"partial_success", "complete,error", 1, true, false},
} {
t.Run(test.name, func(t *testing.T) {
body := []byte(`{"model":"gpt-image-2","prompt":"edit","images":[{"image_url":"data:image/png;base64,AA=="}],"stream":true,"response_format":"url"}`)
if test.name == "partial_requested_multiple" {
body = []byte(`{"model":"gpt-image-2","prompt":"edit","n":2,"images":[{"image_url":"data:image/png;base64,AA=="}],"stream":true,"response_format":"url"}`)
}
c, rec := newOpenAIImagesTestContext(t, body)
c.Request.URL.Path = "/v1/images/edits"
if test.disconnect {
@@ -383,7 +383,7 @@ func buildOpenAIImagesResponsesRequest(parsed *OpenAIImagesRequest, toolModel st
}
req := []byte(`{"instructions":"","stream":true,"reasoning":{"effort":"medium","summary":"auto"},"parallel_tool_calls":true,"include":["reasoning.encrypted_content"],"model":"","store":false,"tool_choice":{"type":"image_generation"}}`)
req, _ = sjson.SetBytes(req, "model", openAIImagesResponsesMainModel)
req, _ = sjson.SetBytes(req, "model", openAIImagesResponsesMainModelValue())
req, _ = sjson.SetBytes(req, "instructions", openAIImagesVerbatimPromptInstructions)
input := []byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`)
@@ -1803,7 +1803,7 @@ func (s *OpenAIGatewayService) forwardOpenAIImagesOAuth(
if err := validateOpenAIImagesModel(upstreamModel); err != nil {
return nil, err
}
direct := usesCodexDirectImages(upstreamModel)
direct := usesCodexDirectImages(upstreamModel) && !isOpenAIImagesForceResponses(ctx)
beginUpstreamResponseModelObservation(c)
SetOpsUpstreamModel(c, upstreamModel)
logger.LegacyPrintf(
@@ -1822,7 +1822,14 @@ func (s *OpenAIGatewayService) forwardOpenAIImagesOAuth(
return nil, err
}
responsesBody, targetURL, err := buildOpenAIImagesOAuthPayload(parsed, upstreamModel)
var responsesBody []byte
var targetURL string
if direct {
responsesBody, targetURL, err = buildOpenAIImagesOAuthPayload(parsed, upstreamModel)
} else {
responsesBody, err = buildOpenAIImagesResponsesRequest(parsed, upstreamModel)
targetURL = chatgptCodexURL
}
if err != nil {
return nil, err
}
@@ -1874,6 +1881,9 @@ func (s *OpenAIGatewayService) forwardOpenAIImagesOAuth(
respBody := s.readUpstreamErrorBody(resp)
_ = resp.Body.Close()
respBody = s.redactAgentIdentitySensitiveBody(upstreamCtx, account, respBody)
if direct && (resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusMethodNotAllowed) {
return s.forwardOpenAIImagesOAuth(withOpenAIImagesForceResponses(ctx), c, account, parsed, channelMappedModel)
}
if !agentIdentityTaskRecoveryWasTried(ctx) && s.isAgentIdentityAccount(ctx, account) && isAgentIdentityTaskInvalidHTTPResponse(resp.StatusCode, respBody) {
expectedTaskID := account.GetCredential("task_id")
if err := s.recoverAgentIdentityTask(ctx, account, expectedTaskID); err != nil {
@@ -1966,7 +1976,7 @@ func (s *OpenAIGatewayService) forwardOpenAIImagesOAuth(
if direct {
usage, imageCount, imageOutputSizes, err = s.handleCodexDirectImagesNonStreamingResponse(resp, c, parsed)
} else {
usage, imageCount, imageOutputSizes, err = s.handleOpenAIImagesOAuthNonStreamingResponse(resp, c, parsed.ResponseFormat, upstreamModel)
usage, imageCount, imageOutputSizes, err = s.handleOpenAIImagesOAuthNonStreamingResponse(resp, c, parsed.ResponseFormat, requestModel)
}
if err != nil {
return nil, s.handleOpenAIImagesOAuthResponseError(