Skip to content

feat(codex): support gpt-image-2 image tools - #4481

Closed
jaredshuai wants to merge 1 commit into
QuantumNous:mainfrom
jaredshuai:codex/gpt-image-2-pr
Closed

feat(codex): support gpt-image-2 image tools#4481
jaredshuai wants to merge 1 commit into
QuantumNous:mainfrom
jaredshuai:codex/gpt-image-2-pr

Conversation

@jaredshuai

@jaredshuai jaredshuai commented Apr 26, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

为 Codex 渠道增加 gpt-image-2 图片生成与编辑支持,并补齐 Codex responses 路径所需的兼容处理。

主要变更:

  • Codex 标准 /v1/responses 请求中,将字符串 input 转为 Codex 可接受的 user message list。
  • Codex 标准 responses 上游请求强制 stream=true,并在下游非流式场景内部聚合 Codex SSE 为标准 Responses JSON。
  • Codex 非 2xx 响应使用专用错误解析,避免纯文本错误体被误解析成 JSON 后掩盖真实上游错误。
  • 增加 Codex /v1/images/generations/v1/images/edits 转换,将 OpenAI image 请求转为 Codex /backend-api/codex/responses + image_generation tool。
  • 支持 multipart edit 图片上传,并将 Codex SSE 图片结果聚合为 OpenAI image response。
  • 增加 Codex 原生 /backend-api/codex/responses / /responses/compact relay route。

实现范围限定在 Codex channel。gpt-image-2 仍需要通过正常渠道模型/abilities 配置启用,不绕过现有渠道选择、分组、倍率和计费逻辑。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

本地测试通过:

go test ./relay/channel/codex ./relay/constant
go test -p 1 -vet=off -run "^$" ./relay ./router ./controller ./dto ./common ./model

手动验证过的场景:

  • Codex /v1/responses 非流式请求返回 200。
  • Codex /v1/responses 流式请求返回 text/event-stream
  • Codex /v1/images/generations 使用 gpt-image-2 返回 200。
  • Codex /v1/images/edits 使用 multipart 图片上传返回 200。
  • 纯文本/非标准错误体不会再表现为 invalid character 'e' looking for beginning of value

Summary by CodeRabbit

  • New Features

    • Added support for the "gpt-image-2" image generation model.
    • Introduced image generation and image editing capabilities for enhanced creative workflows.
    • Extended image response data with additional metadata fields: background, output format, quality, size, and usage information.
  • Bug Fixes

    • Improved error handling with better message extraction from upstream services.

@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds support for gpt-image-2 image generation and editing through the Codex channel by exposing the Codex Responses API's image_generation tool as standard OpenAI image endpoints. The implementation includes request conversion, SSE response aggregation, error handling, and routing infrastructure.

Changes

Cohort / File(s) Summary
Model Registration
common/model.go, relay/channel/codex/constants.go
Add gpt-image-2 to image generation models list and extend Codex built-in model list with new image model and related constants (CodexImageModel, defaultImagesMainModel, imageGenerationTool).
DTO Extensions
dto/openai_image.go, dto/openai_response.go
Expand ImageResponse with new optional fields (background, output_format, quality, size, usage) and extend ResponsesOutput with response metadata fields (result, revised_prompt, output_format, background).
Codex Image Request/Response Handling
relay/channel/codex/image.go, relay/channel/codex/responses.go
New modules implementing complete image request relay (supporting both generation and edit modes with multipart upload and base64 conversion) and response aggregation (handling SSE streams, token usage tracking, and format conversion to OpenAI response structure).
Codex Adapter & Error Handling
relay/channel/codex/adaptor.go, relay/channel/codex/error.go
Extend adapter to support image requests via delegation to image builder, normalize Codex responses requests with tool/instruction handling, and introduce dedicated error extraction and truncation for Codex upstream errors.
Test Coverage & Constants
relay/channel/codex/adaptor_test.go, controller/channel-test.go
Add comprehensive test file covering request conversion, SSE aggregation, image/text output handling, tool call tracking, and multipart image upload; add model-detection predicate for Codex image generation tests.
Relay Mode & Routing
relay/constant/relay_mode.go, relay/constant/relay_mode_test.go, router/relay-router.go
Map /backend-api/codex/responses backend paths to new relay modes, add test validation, and expose two new POST endpoints for Codex responses and compact responses with appropriate middleware.
Handler Integration
relay/image_handler.go, relay/responses_handler.go
Route Codex API type requests to dedicated RelayErrorHandler, prevent request-body pass-through for Codex, disable field filtering for raw Codex responses, and branch error handling by API type.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Router
    participant ImageHandler
    participant CodexAdapter
    participant CodexBackend
    participant ResponseHandler
    participant ClientResponse

    Client->>Router: POST /v1/images/generations
    Router->>ImageHandler: Route to image handler
    ImageHandler->>CodexAdapter: ConvertImageRequest (Codex Responses format)
    CodexAdapter->>CodexBackend: POST /backend-api/codex/responses<br/>(with image_generation tool)
    CodexBackend-->>ResponseHandler: HTTP SSE stream response
    ResponseHandler->>ResponseHandler: Parse SSE events<br/>Aggregate output items<br/>Extract usage data
    ResponseHandler->>ResponseHandler: Convert to OpenAI<br/>ImageResponse format
    ResponseHandler-->>ClientResponse: JSON response with images
    ClientResponse-->>Client: 200 OK with image data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

ready to merge

Suggested reviewers

  • creamlike1024

Poem

🐰 A rabbit hops with glee,
Codex images now flow so free!
From gpt-image-2 requests come,
SSE streams are parsed and spun,
OpenAI format, the circle's done! 🎨✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(codex): support gpt-image-2 image tools' clearly and concisely summarizes the main change: adding gpt-image-2 image generation/editing support to the Codex channel.
Linked Issues check ✅ Passed The PR fully implements objectives from issue #4480: exposes gpt-image-2 via OpenAI image endpoints, converts requests to Codex /backend-api/codex/responses with image_generation tool, supports /v1/images/generations and /v1/images/edits with multipart uploads, aggregates SSE into OpenAI responses, and limits support to Codex channel only.
Out of Scope Changes check ✅ Passed All changes directly support gpt-image-2 image tool support in Codex: model additions, Codex adapter/responses/image handling, test coverage, constants, error handling, router endpoints, and relay mode mappings are all in scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
relay/channel/codex/error.go (2)

28-35: Consider bounding the upstream body read.

io.ReadAll(resp.Body) is unbounded — a misbehaving upstream could send a large/unbounded error body and force an equivalently large allocation just to extract a short error message (which you cap at 2048 chars anyway). Using io.LimitReader (e.g., a few hundred KB) would protect the relay process while still capturing enough body for extractCodexErrorMessage/truncateErrorMessage.

♻️ Proposed change
-	var responseBody []byte
-	if resp != nil && resp.Body != nil {
-		body, err := io.ReadAll(resp.Body)
-		if err == nil {
-			responseBody = body
-		}
-		service.CloseResponseBodyGracefully(resp)
-	}
+	var responseBody []byte
+	if resp != nil && resp.Body != nil {
+		const maxErrorBodyRead = 256 << 10 // 256 KiB
+		body, err := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodyRead))
+		if err == nil {
+			responseBody = body
+		}
+		service.CloseResponseBodyGracefully(resp)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/codex/error.go` around lines 28 - 35, The current unbounded
io.ReadAll on resp.Body can allocate arbitrarily large memory; change the read
to use an io.LimitReader so we only read a bounded number of bytes (e.g., 256KB)
into responseBody before calling extractCodexErrorMessage/truncateErrorMessage.
Locate the block that sets responseBody (checking resp != nil && resp.Body !=
nil) and replace the io.ReadAll(resp.Body) invocation with
io.ReadAll(io.LimitReader(resp.Body, 256*1024)), keeping the existing error
handling and the call to service.CloseResponseBodyGracefully(resp).

52-86: Edge case: error path may return a large raw object.

When the upstream payload has error as a non-empty object but no error.message (or any sibling path matches), the loop falls through to the last "error" path and returns result.Raw — i.e., the entire error JSON. truncateErrorMessage will cap this, but the resulting message can be cryptic (e.g., a JSON blob). Consider also probing common nested OpenAI/Codex shapes like error.error.message, errors.0.message, or simply skipping non-scalar matches for the bare "error" path so structured errors fall back to the raw body trim path. Optional, since truncation already prevents harm.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/codex/error.go` around lines 52 - 86, extractCodexErrorMessage
may return a large structured JSON when it matches the bare "error" path; update
the search logic in extractCodexErrorMessage to (1) prefer nested/common
Codex/OpenAI shapes by adding paths like "error.error.message",
"errors.0.message", "errors.#.message" (or similar common variants) before the
plain "error" entry, and (2) treat the plain "error" match as valid only for
scalar values—if result.Type is an object/array, skip it so the function falls
back to other candidates or the trimmed raw body instead of returning a cryptic
JSON blob.
relay/channel/codex/responses.go (1)

47-48: Stale Content-Length header may remain after aggregating SSE → JSON.

cloneResponseWithContentType deletes Transfer-Encoding but preserves all other headers. When upstream is SSE (no Content-Length), this is fine. However, if upstream ever sends a Content-Length (e.g., a JSON error returned via this path), the cloned response carries the original length while the body may differ in size. Consider also deleting Content-Length when the body is reconstructed.

♻️ Suggested refactor
 func cloneResponseWithContentType(resp *http.Response, contentType string) *http.Response {
 	if resp == nil {
 		return nil
 	}
 	cloned := *resp
 	cloned.Header = resp.Header.Clone()
 	cloned.Header.Set("Content-Type", contentType)
 	cloned.Header.Del("Transfer-Encoding")
+	cloned.Header.Del("Content-Length")
 	return &cloned
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/codex/responses.go` around lines 47 - 48, The cloned response
can carry a stale Content-Length after converting SSE to aggregated JSON; update
cloneResponseWithContentType (or immediately after creating jsonResp before
calling service.IOCopyBytesGracefully) to also remove the "Content-Length"
header (in addition to "Transfer-Encoding") so the response length isn't
misleading when the body is reconstructed/changed; ensure the header deletion
targets the exact "Content-Length" key on the http header map used by
cloneResponseWithContentType/jsonResp.
relay/channel/codex/image.go (2)

282-282: input_fidelity only sourced from multipart form.

setStringToolOption(tool, "input_fidelity", firstFormValue(form, "input_fidelity")) reads only from the multipart form. JSON-body image edit requests (when client sends JSON instead of multipart) won't propagate input_fidelity. If dto.ImageRequest has (or could expose) this field, consider sourcing from there as a fallback to keep parity between multipart and JSON edit paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/codex/image.go` at line 282, The call to
setStringToolOption(tool, "input_fidelity", firstFormValue(form,
"input_fidelity")) only reads input_fidelity from the multipart form and thus
misses JSON-body edit requests; update the code that builds the tool options
(the spot using setStringToolOption and firstFormValue) to fallback to
dto.ImageRequest.InputFidelity (or the appropriate field on dto.ImageRequest)
when firstFormValue(form, "input_fidelity") is empty/nil so both multipart and
JSON edit paths propagate input_fidelity; ensure you reference dto.ImageRequest
where available and keep precedence: form value first, then dto.ImageRequest,
then leave unset.

215-225: Octet-stream fallback may emit data:application/octet-stream;base64,... if detection fails.

If a client uploads a file with Content-Type: application/octet-stream and http.DetectContentType doesn't recognize it as an image (e.g., for less common formats), the resulting data URL uses application/octet-stream. Codex may reject this. Consider returning an explicit error, or defaulting to image/png when the content is small/unknown but the field is required to be an image (since this is in the image[] form field).

♻️ Suggested refactor
-	if mediaType == "" {
-		mediaType = "application/octet-stream"
-	}
+	if mediaType == "" || strings.EqualFold(mediaType, "application/octet-stream") {
+		return "", fmt.Errorf("unable to determine image media type for upload %q", fileHeader.Filename)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/codex/image.go` around lines 215 - 225, The code currently
falls back to "application/octet-stream" and returns a data URL even when
http.DetectContentType fails to recognize an image, which can cause Codex to
reject the upload; update the logic around mediaType in the function that builds
the data URL so that if fileHeader.Header.Get("Content-Type") is
"application/octet-stream" (or empty) and http.DetectContentType(data) does not
start with "image/", you either return a clear error (e.g., ErrInvalidImage)
instead of constructing a non-image data URL, or (if you must produce an image)
default mediaType to "image/png" for small/required image fields; adjust the
return path that currently uses mediaType and
base64.StdEncoding.EncodeToString(data) accordingly and ensure callers of this
function handle the new error or PNG default.
controller/channel-test.go (1)

46-50: Consider using the CodexImageModel constant.

relay/channel/codex already exposes CodexImageModel (used in adaptor.go and image.go). Hard-coding "gpt-image-2" here creates a second source of truth that can drift if the model identifier changes.

♻️ Suggested refactor
+	codexchannel "github.com/QuantumNous/new-api/relay/channel/codex"
@@
 func isCodexImageGenerationTestModel(channel *model.Channel, modelName string) bool {
 	return channel != nil &&
 		channel.Type == constant.ChannelTypeCodex &&
-		strings.EqualFold(strings.TrimSpace(modelName), "gpt-image-2")
+		strings.EqualFold(strings.TrimSpace(modelName), codexchannel.CodexImageModel)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/channel-test.go` around lines 46 - 50, Replace the hard-coded
model string in isCodexImageGenerationTestModel with the shared constant to
avoid duplicate sources of truth: import (or reference) the CodexImageModel
constant from the codex package and use it instead of the literal "gpt-image-2"
in the equality check inside isCodexImageGenerationTestModel so the function
compares strings.EqualFold(strings.TrimSpace(modelName), codex.CodexImageModel).
relay/channel/codex/adaptor.go (1)

153-178: Possible inconsistency: raw passthrough path defaults instructions to "" when no system prompt is set, but the existing logic only does this in a final step.

In the non-raw path (line 266-268), the empty instructions default applies after the system-prompt branch. In the raw path, the else at line 173-178 only runs when the outer condition (info != nil && info.ChannelMeta != nil && info.ChannelSetting.SystemPrompt != "") is false — but there is no fallthrough to also set the empty default if info.ChannelSetting.SystemPrompt != "" and len(instructions) == 0 was already handled inside the first branch. Actually verifying: line 156-160 already handles this case (when instructions doesn't exist, set to systemPrompt). So the logic is correct, but the structure mirrors the non-raw path inconsistently. No functional issue — just noting that the two paths express the same intent differently, which makes them harder to keep in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/codex/adaptor.go` around lines 153 - 178, Refactor the
conditional that sets "instructions" so the raw passthrough branch mirrors the
non-raw path: consolidate the logic around gjson.Get(out, "instructions"),
ChannelSetting.SystemPrompt and ChannelSetting.SystemPromptOverride (referencing
variables systemPrompt and instructions) so that when no system prompt exists
you explicitly set "instructions" to "" and when a system prompt exists you
either set it or prepend/append existing instructions per SystemPromptOverride;
this keeps the handling identical between the two paths and avoids divergent
branching structures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/channel/codex/image.go`:
- Around line 524-554: The buildImageAPIResponse function currently sets
dto.ImageData.Url to a data:...;base64,... string (via mimeTypeFromOutputFormat
and result.Result) when responseFormat == "url", which returns an embedded
base64 data URL rather than an OpenAI-hosted URL; update the codebase by adding
a clear comment immediately above buildImageAPIResponse (or above
dto.ImageData.Url field) stating that response_format="url" produces an embedded
data URL (not a hosted URL), can be very large for high-res outputs, and may
break client assumptions (e.g., attempting to fetch the URL or logging size
limits); if desired by product, optionally implement a different code path in
buildImageAPIResponse to return hosted URLs instead, otherwise document the
semantics so callers use B64Json when appropriate.

In `@relay/channel/codex/responses.go`:
- Around line 365-379: recordResponsesBuiltInToolUsageFromGJSON is dead code
because Codex Response objects don't include a "tools" field so the array check
always fails; either remove this function and any unused references to it (and
rely on recordResponsesBuiltInToolCall which already updates
ResponsesUsageInfo.BuiltInTools.CallCount), or keep it but add a clear comment
above recordResponsesBuiltInToolUsageFromGJSON explaining it's
defensive/placeholder for future API changes and why the current API doesn't
populate "tools" (referencing ResponsesUsageInfo.BuiltInTools and
recordResponsesBuiltInToolCall for context).

---

Nitpick comments:
In `@controller/channel-test.go`:
- Around line 46-50: Replace the hard-coded model string in
isCodexImageGenerationTestModel with the shared constant to avoid duplicate
sources of truth: import (or reference) the CodexImageModel constant from the
codex package and use it instead of the literal "gpt-image-2" in the equality
check inside isCodexImageGenerationTestModel so the function compares
strings.EqualFold(strings.TrimSpace(modelName), codex.CodexImageModel).

In `@relay/channel/codex/adaptor.go`:
- Around line 153-178: Refactor the conditional that sets "instructions" so the
raw passthrough branch mirrors the non-raw path: consolidate the logic around
gjson.Get(out, "instructions"), ChannelSetting.SystemPrompt and
ChannelSetting.SystemPromptOverride (referencing variables systemPrompt and
instructions) so that when no system prompt exists you explicitly set
"instructions" to "" and when a system prompt exists you either set it or
prepend/append existing instructions per SystemPromptOverride; this keeps the
handling identical between the two paths and avoids divergent branching
structures.

In `@relay/channel/codex/error.go`:
- Around line 28-35: The current unbounded io.ReadAll on resp.Body can allocate
arbitrarily large memory; change the read to use an io.LimitReader so we only
read a bounded number of bytes (e.g., 256KB) into responseBody before calling
extractCodexErrorMessage/truncateErrorMessage. Locate the block that sets
responseBody (checking resp != nil && resp.Body != nil) and replace the
io.ReadAll(resp.Body) invocation with io.ReadAll(io.LimitReader(resp.Body,
256*1024)), keeping the existing error handling and the call to
service.CloseResponseBodyGracefully(resp).
- Around line 52-86: extractCodexErrorMessage may return a large structured JSON
when it matches the bare "error" path; update the search logic in
extractCodexErrorMessage to (1) prefer nested/common Codex/OpenAI shapes by
adding paths like "error.error.message", "errors.0.message", "errors.#.message"
(or similar common variants) before the plain "error" entry, and (2) treat the
plain "error" match as valid only for scalar values—if result.Type is an
object/array, skip it so the function falls back to other candidates or the
trimmed raw body instead of returning a cryptic JSON blob.

In `@relay/channel/codex/image.go`:
- Line 282: The call to setStringToolOption(tool, "input_fidelity",
firstFormValue(form, "input_fidelity")) only reads input_fidelity from the
multipart form and thus misses JSON-body edit requests; update the code that
builds the tool options (the spot using setStringToolOption and firstFormValue)
to fallback to dto.ImageRequest.InputFidelity (or the appropriate field on
dto.ImageRequest) when firstFormValue(form, "input_fidelity") is empty/nil so
both multipart and JSON edit paths propagate input_fidelity; ensure you
reference dto.ImageRequest where available and keep precedence: form value
first, then dto.ImageRequest, then leave unset.
- Around line 215-225: The code currently falls back to
"application/octet-stream" and returns a data URL even when
http.DetectContentType fails to recognize an image, which can cause Codex to
reject the upload; update the logic around mediaType in the function that builds
the data URL so that if fileHeader.Header.Get("Content-Type") is
"application/octet-stream" (or empty) and http.DetectContentType(data) does not
start with "image/", you either return a clear error (e.g., ErrInvalidImage)
instead of constructing a non-image data URL, or (if you must produce an image)
default mediaType to "image/png" for small/required image fields; adjust the
return path that currently uses mediaType and
base64.StdEncoding.EncodeToString(data) accordingly and ensure callers of this
function handle the new error or PNG default.

In `@relay/channel/codex/responses.go`:
- Around line 47-48: The cloned response can carry a stale Content-Length after
converting SSE to aggregated JSON; update cloneResponseWithContentType (or
immediately after creating jsonResp before calling
service.IOCopyBytesGracefully) to also remove the "Content-Length" header (in
addition to "Transfer-Encoding") so the response length isn't misleading when
the body is reconstructed/changed; ensure the header deletion targets the exact
"Content-Length" key on the http header map used by
cloneResponseWithContentType/jsonResp.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f2a4a3e5-ddfa-4a95-b827-373567b2b6d0

📥 Commits

Reviewing files that changed from the base of the PR and between 34afe9b and 0bc23d4.

📒 Files selected for processing (15)
  • common/model.go
  • controller/channel-test.go
  • dto/openai_image.go
  • dto/openai_response.go
  • relay/channel/codex/adaptor.go
  • relay/channel/codex/adaptor_test.go
  • relay/channel/codex/constants.go
  • relay/channel/codex/error.go
  • relay/channel/codex/image.go
  • relay/channel/codex/responses.go
  • relay/constant/relay_mode.go
  • relay/constant/relay_mode_test.go
  • relay/image_handler.go
  • relay/responses_handler.go
  • router/relay-router.go

Comment on lines +524 to +554
func buildImageAPIResponse(results []imageCallResult, createdAt int64, usage *dto.Usage, firstMeta imageCallResult, responseFormat string) ([]byte, error) {
responseFormat = strings.ToLower(strings.TrimSpace(responseFormat))
if responseFormat == "" {
responseFormat = "b64_json"
}

imageResponse := dto.ImageResponse{
Created: createdAt,
Data: make([]dto.ImageData, 0, len(results)),
Background: firstMeta.Background,
OutputFormat: firstMeta.OutputFormat,
Quality: firstMeta.Quality,
Size: firstMeta.Size,
}
if usage != nil && usage.TotalTokens > 0 {
imageResponse.Usage = usage
}

for _, result := range results {
item := dto.ImageData{
RevisedPrompt: result.RevisedPrompt,
}
if responseFormat == "url" {
item.Url = "data:" + mimeTypeFromOutputFormat(result.OutputFormat) + ";base64," + result.Result
} else {
item.B64Json = result.Result
}
imageResponse.Data = append(imageResponse.Data, item)
}
return common.Marshal(imageResponse)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

response_format=url returns a base64 data: URL, not a hosted URL.

When the client requests response_format=url, the result is a data:image/<fmt>;base64,... URL embedding the full image. This differs from the OpenAI-hosted URL semantics clients may expect, and can become very large (multi-MB) for high-resolution outputs. Consider documenting this in the PR description or in a comment near buildImageAPIResponse, since some clients may treat Url and B64Json quite differently (e.g., trying to fetch the URL, logging, size limits).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/codex/image.go` around lines 524 - 554, The
buildImageAPIResponse function currently sets dto.ImageData.Url to a
data:...;base64,... string (via mimeTypeFromOutputFormat and result.Result) when
responseFormat == "url", which returns an embedded base64 data URL rather than
an OpenAI-hosted URL; update the codebase by adding a clear comment immediately
above buildImageAPIResponse (or above dto.ImageData.Url field) stating that
response_format="url" produces an embedded data URL (not a hosted URL), can be
very large for high-res outputs, and may break client assumptions (e.g.,
attempting to fetch the URL or logging size limits); if desired by product,
optionally implement a different code path in buildImageAPIResponse to return
hosted URLs instead, otherwise document the semantics so callers use B64Json
when appropriate.

Comment on lines +365 to +379
func recordResponsesBuiltInToolUsageFromGJSON(info *relaycommon.RelayInfo, response gjson.Result) {
if info == nil || info.ResponsesUsageInfo == nil || info.ResponsesUsageInfo.BuiltInTools == nil {
return
}
tools := response.Get("tools")
if !tools.IsArray() {
return
}
for _, tool := range tools.Array() {
buildToolInfo, ok := info.ResponsesUsageInfo.BuiltInTools[tool.Get("type").String()]
if ok && buildToolInfo != nil {
buildToolInfo.CallCount++
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for usages and existing patterns of CallCount / BuiltInTools to confirm intent
rg -nP --type=go -C3 '\bBuiltInTools\b'
echo "---"
rg -nP --type=go -C5 '\bCallCount\b'
echo "---"
# Find similar handlers for comparison (e.g., openai responses handler counting)
rg -nP --type=go -C5 'recordResponses.*Tool'

Repository: QuantumNous/new-api

Length of output: 25089


🏁 Script executed:

# Look for tests and documentation about Codex response.tools format
rg -nP --type=go "response\.completed" relay/channel/codex/ -A 5
echo "---"
# Search for any Codex API reference or comments
rg -nP --type=go -i "codex.*tool" relay/channel/codex/responses.go -B 2 -A 2
echo "---"
# Look at how recordResponsesBuiltInToolUsageFromGJSON is called and what responses it receives
rg -nP --type=go "recordResponsesBuiltInToolUsageFromGJSON" -B 10 -A 2

Repository: QuantumNous/new-api

Length of output: 10890


🏁 Script executed:

# Search for test data or samples that show response.tools field
rg -nP --type=go "\.tools" relay/channel/codex/ -B 3 -A 3
echo "---"
# Look at test fixtures to see actual response structure
rg -nP "response\.completed" relay/channel/codex/adaptor_test.go -A 5 | head -100
echo "---"
# Check if response.tools is ever populated or documented
rg -nP "tools.*array\|tools.*object" relay/channel/codex/ -i

Repository: QuantumNous/new-api

Length of output: 2663


🏁 Script executed:

# Check the implementation and structure of response object more carefully
cat -n relay/channel/codex/responses.go | sed -n '365,379p'
echo "---"
# Look for where tools might be added to response
rg -nP "response.*tools\|\"tools\"" relay/channel/codex/ --type=go
echo "---"
# Check if response.tools appears in any test data or is ever expected
rg -nP '\"tools\"' relay/channel/ --type=go

Repository: QuantumNous/new-api

Length of output: 1161


🏁 Script executed:

# Look at the adaptor_test.go context around line 83 to see what tools field represents
cat -n relay/channel/codex/adaptor_test.go | sed -n '75,95p'
echo "---"
# Check adaptor.go to understand the relationship between request tools and response tools
cat -n relay/channel/codex/adaptor.go | sed -n '135,150p'
echo "---"
# Look for any comments or documentation about response.tools in Codex integration
rg -nP -B 5 "response\.Get.*tools" relay/channel/codex/

Repository: QuantumNous/new-api

Length of output: 2022


🏁 Script executed:

# Find normalizeCodexResponsesTools to understand what response.tools contains
rg -nP "normalizeCodexResponsesTools" relay/channel/codex/ -A 20
echo "---"
# Search for any documentation or comments about Codex response structure
rg -nP "response\.completed" relay/channel/codex/ -B 2 -A 8 | head -80
echo "---"
# Check if there's any actual response sample with tools field from Codex
git log --all --full-history --source -- relay/channel/codex/responses.go | head -20

Repository: QuantumNous/new-api

Length of output: 10743


🏁 Script executed:

# Let's verify: in actual test data, does response.completed ever have a "tools" field?
rg -nP '\"response\.completed\"' relay/channel/codex/adaptor_test.go -A 1 | grep -E '"response|"tools"'
echo "---"
# Check if there are any real examples in other tests
rg -nP 'response\.completed' relay/channel/ --type=go -A 2 | grep -E 'tools|image_gen|tool_usage' | head -20
echo "---"
# Let's examine what happens when tools field doesn't exist
python3 << 'EOF'
import json
# Simulate the behavior with missing tools field
response_text = '{"id":"resp_1","object":"response","created_at":1,"output":[],"usage":{"input_tokens":2,"output_tokens":3}}'
response = json.loads(response_text)
tools = response.get("tools")
print(f"tools field exists: {tools is not None}")
print(f"tools value: {tools}")
print(f"is array: {isinstance(tools, list)}")
# The code does: if !tools.IsArray() return
# In Go, this would also return early for None
EOF

Repository: QuantumNous/new-api

Length of output: 2486


recordResponsesBuiltInToolUsageFromGJSON has dead code that suggests API misunderstanding.

This function attempts to iterate response.tools and increment CallCount, but Codex's response.completed objects never include a tools field. The array check !tools.IsArray() returns true immediately, causing the function to exit without counting anything.

Tool invocations are correctly tracked elsewhere via recordResponsesBuiltInToolCall on stream events (image_generation_call, web_search_call), which increments the same CallCount counter. The unused loop in this function suggests a misalignment with the actual Codex Responses API format.

Consider either:

  1. Removing this function entirely if it serves no purpose, or
  2. Adding a comment clarifying why it exists (defensive coding for future API changes).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/codex/responses.go` around lines 365 - 379,
recordResponsesBuiltInToolUsageFromGJSON is dead code because Codex Response
objects don't include a "tools" field so the array check always fails; either
remove this function and any unused references to it (and rely on
recordResponsesBuiltInToolCall which already updates
ResponsesUsageInfo.BuiltInTools.CallCount), or keep it but add a clear comment
above recordResponsesBuiltInToolUsageFromGJSON explaining it's
defensive/placeholder for future API changes and why the current API doesn't
populate "tools" (referencing ResponsesUsageInfo.BuiltInTools and
recordResponsesBuiltInToolCall for context).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(codex): 支持 gpt-image-2 图片生成与编辑

2 participants