Skip to content

feat(relay): add OpenAI Batch API passthrough - #6730

Open
qiuxinyuan321 wants to merge 1 commit into
QuantumNous:mainfrom
qiuxinyuan321:codex/add-openai-batch-api
Open

feat(relay): add OpenAI Batch API passthrough#6730
qiuxinyuan321 wants to merge 1 commit into
QuantumNous:mainfrom
qiuxinyuan321:codex/add-openai-batch-api

Conversation

@qiuxinyuan321

@qiuxinyuan321 qiuxinyuan321 commented Aug 8, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

AI-assisted: the implementation, tests, review, and this description were prepared with Codex. The "人工确认" item remains unchecked intentionally.

📝 变更描述 / Description

Add native OpenAI-compatible File and Batch API passthrough so image models such as gpt-image-2 can be used through Batch JSONL requests to /v1/images/generations and /v1/images/edits.

This adds the core File/Batch workflow:

  • POST /v1/files, GET /v1/files/:id, GET /v1/files/:id/content, DELETE /v1/files/:id
  • POST /v1/batches, GET /v1/batches/:id, POST /v1/batches/:id/cancel
  • Full JSONL validation with unique custom_id, a single non-empty model, supported POST endpoints, and a 50,000-request limit
  • Per-user resource ownership that pins follow-up calls to the creating channel and multi-key credential
  • Upstream status, body, query, content type, and multi-value response header passthrough, excluding hop-by-hop headers
  • Global and per-OpenAI-channel UI switches, both disabled by default

🔒 Rollout and limitations

  • Requires both openai_batch_setting.enabled and the OpenAI channel's native_openai_batch opt-in.
  • Supports native OpenAI-compatible Batch upstreams only.
  • Channel model mapping is rejected because raw JSONL bodies are passed through unchanged.
  • New API does not currently settle Batch quota; upstream charges still apply.
  • Cross-channel list aggregation is intentionally not implemented, so GET /v1/files and GET /v1/batches remain unsupported.

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

Related to #6535 (OpenAI scope only). Gemini Batch API is outside this PR.

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 已搜索开放 PR,未发现 OpenAI Batch API 的重复实现。
  • Bug fix 说明: 不适用,本 PR 是新功能。
  • 变更理解: AI-assisted implementation; maintainer review is requested.
  • 范围聚焦: 本 PR 未包含与 File/Batch API 支持无关的代码改动。
  • 本地验证: 相关后端、前端测试、类型检查、构建和定向 lint 已通过。
  • 安全合规: staged diff 已检查,无敏感凭据或调试残留。

🧪 验证 / Testing

Passed:

  • go test ./model ./middleware ./controller ./router -count=1
  • go vet ./model ./middleware ./controller ./router
  • bun test src/features/system-settings/general/__tests__/system-behavior-settings.test.ts src/features/channels/lib/__tests__/openai-batch-settings.test.ts (2 pass, 0 fail)
  • bun run typecheck
  • bun run build:check
  • Targeted oxlint for every changed TypeScript/TSX file
  • 7 locale JSON files parsed and checked for all 4 new keys
  • git diff --check

Full-suite baseline notes:

  • go test ./... -count=1 still reproduces three failures on a clean origin/main worktree: the Windows HTTP/2 GOAWAY retry test and two service channel-affinity metric-count tests.
  • bun run format:check still reports five pre-existing files; none are modified by this PR.

📸 运行证明 / Proof of Work

  • Relevant Go packages: 4 packages passed
  • Frontend regression tests: 2 passed, 0 failed
  • Production frontend build: passed
  • Independent read-only review: no P0-P2 findings

Summary by CodeRabbit

  • New Features

    • Added native OpenAI Files and Batches API passthrough support.
    • Added resource tracking for uploaded files, batch outputs, and related operations.
    • Added channel-level control to enable native OpenAI Batch support.
    • Added a system setting to enable or disable OpenAI Batch API functionality.
  • Configuration

    • Added advanced channel and system settings with localized labels and guidance.
    • Batch quota settlement is not currently handled; upstream charges may still apply.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds native OpenAI Files and Batches passthrough. The change adds resource persistence, pinned channel and key routing, request validation, relay routes, cleanup, and frontend configuration.

Changes

Native OpenAI resource passthrough

Layer / File(s) Summary
Configuration and frontend controls
constant/context_key.go, setting/operation_setting/*, relaykit/dto/*, web/src/features/channels/*, web/src/features/system-settings/*, web/src/i18n/locales/*
Adds native batch configuration, channel form support, system settings, context metadata, tests, and translations.
Resource storage and key resolution
model/openai_upstream_resource.go, model/channel.go, model/main.go, model/openai_upstream_resource_test.go
Stores user-scoped file and batch bindings with channel, key, and model metadata. Adds validated persistence, lookup, deletion, migrations, and key resolution.
Resource validation and channel routing
middleware/openai_upstream_resource.go, middleware/distributor.go, model/ability.go, model/channel_cache.go, middleware/openai_upstream_resource_test.go
Validates batch uploads and resource ownership. Resolves stored routing metadata and filters channels by native batch support.
Resource relay and route integration
controller/openai_upstream_resource.go, router/relay-router.go, router/openai_upstream_resource_test.go, controller/openai_upstream_resource_test.go
Relays OpenAI requests, filters response headers, persists creation responses, removes deleted bindings, and registers file and batch routes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PrepareOpenAIUpstreamResource
  participant Distribute
  participant RelayOpenAIUpstreamResource
  participant OpenAIUpstream
  Client->>PrepareOpenAIUpstreamResource: Submit file or batch request
  PrepareOpenAIUpstreamResource->>Distribute: Pass model and resource routing metadata
  Distribute->>RelayOpenAIUpstreamResource: Select channel and pinned key
  RelayOpenAIUpstreamResource->>OpenAIUpstream: Forward request
  OpenAIUpstream-->>RelayOpenAIUpstreamResource: Return response and resource IDs
  RelayOpenAIUpstreamResource-->>Client: Forward filtered response
Loading

Possibly related PRs

Poem

A rabbit hops through files and keys,
Batches bloom like clover leaves.
Channels pin the path just right,
Headers pass through clean and light.
“Native OpenAI!” sings the hare—
Resources safely travel there.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.76% 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 clearly identifies the primary feature: native OpenAI Batch API passthrough.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

Warning

⚠️ This pull request shows signs of AI-generated slop (ai_padded_prose). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@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 (3)
middleware/distributor.go (1)

49-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use an i18n key for the new abort message.

Every other abort in Distribute uses i18n.T(c, ...). This message is a hardcoded English string. Add a key in i18n/keys.go and translate it for consistency with the surrounding error responses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@middleware/distributor.go` around lines 49 - 52, The new abort message in
Distribute is hardcoded instead of using localization. Add a dedicated key to
i18n/keys.go, add its translations through the existing i18n mechanism, and pass
i18n.T(c, ...) to abortWithOpenAiMessage while preserving the current forbidden
response and message meaning.
model/channel_cache.go (1)

231-236: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Precompute native batch support like the Advanced Custom config.

SupportsNativeOpenAIBatch() calls GetOtherSettings(), which unmarshals the channel settings JSON on every call. This runs for every candidate channel on every File/Batch request. The Advanced Custom branch avoids this by reading the precomputed channel2advancedCustomConfig map that the cache build populates.

GetOtherSettings() also writes to channel.OtherSettings and calls channel.Save() when the JSON is malformed. The *Channel values in channelsIDM are shared across requests, so that fallback mutates shared cache state from concurrent request goroutines.

Add a channel2nativeOpenAIBatch map[int]bool populated during cache initialization and read it here.

Run the following script to confirm where the channel caches are built:

#!/bin/bash
rg -n -C5 'channel2advancedCustomConfig' model/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/channel_cache.go` around lines 231 - 236, Add a
channel2nativeOpenAIBatch map alongside channel2advancedCustomConfig, populate
it during channel cache initialization using each channel’s native batch
capability, and read this precomputed map in the IsOpenAIUpstreamResourcePath
branch instead of calling SupportsNativeOpenAIBatch(). Ensure the map is
initialized and refreshed with the existing cache lifecycle.
middleware/openai_upstream_resource_test.go (1)

364-400: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for the token-specific channel conflict.

Distribute now returns 403 when a token pins a channel that does not own the requested resource (middleware/distributor.go lines 49-52). No test covers that branch. Add a case that sets constant.ContextKeyTokenSpecificChannelId to a different channel id and asserts 403.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@middleware/openai_upstream_resource_test.go` around lines 364 - 400, Extend
TestPrepareOpenAIUpstreamResourceRejectsMissingPinnedKey with a token-specific
channel conflict case: set constant.ContextKeyTokenSpecificChannelId to a
different channel ID in the request context, invoke the same middleware route,
and assert that Distribute returns HTTP 403 without reaching the handled
callback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/openai_upstream_resource.go`:
- Around line 192-206: Update the bindOpenAIUpstreamResourceResponse failure
branch to log the upstream resource ID and bindErr before calling
openAIUpstreamResourceError and returning. Reuse the existing upstream resource
identifier from the handler’s response context, preserving the current 502
behavior.

In `@middleware/distributor.go`:
- Around line 181-193: Guard both the channel request-path validation and
SetupContextForSelectedChannel error handling in Distribute so they run only
when channel is non-nil. Preserve the existing behavior for selected channels
while allowing fetch-style routes with shouldSelectChannel == false and no
channel to continue without either abort.

---

Nitpick comments:
In `@middleware/distributor.go`:
- Around line 49-52: The new abort message in Distribute is hardcoded instead of
using localization. Add a dedicated key to i18n/keys.go, add its translations
through the existing i18n mechanism, and pass i18n.T(c, ...) to
abortWithOpenAiMessage while preserving the current forbidden response and
message meaning.

In `@middleware/openai_upstream_resource_test.go`:
- Around line 364-400: Extend
TestPrepareOpenAIUpstreamResourceRejectsMissingPinnedKey with a token-specific
channel conflict case: set constant.ContextKeyTokenSpecificChannelId to a
different channel ID in the request context, invoke the same middleware route,
and assert that Distribute returns HTTP 403 without reaching the handled
callback.

In `@model/channel_cache.go`:
- Around line 231-236: Add a channel2nativeOpenAIBatch map alongside
channel2advancedCustomConfig, populate it during channel cache initialization
using each channel’s native batch capability, and read this precomputed map in
the IsOpenAIUpstreamResourcePath branch instead of calling
SupportsNativeOpenAIBatch(). Ensure the map is initialized and refreshed with
the existing cache lifecycle.
🪄 Autofix

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 Plus

Run ID: 9e0e6099-3717-465d-94d5-bc67e1209df5

📥 Commits

Reviewing files that changed from the base of the PR and between 823e263 and cf22559.

📒 Files selected for processing (34)
  • constant/context_key.go
  • controller/openai_upstream_resource.go
  • controller/openai_upstream_resource_test.go
  • middleware/distributor.go
  • middleware/openai_upstream_resource.go
  • middleware/openai_upstream_resource_test.go
  • model/ability.go
  • model/channel.go
  • model/channel_cache.go
  • model/main.go
  • model/openai_upstream_resource.go
  • model/openai_upstream_resource_test.go
  • relaykit/dto/channel_settings.go
  • router/openai_upstream_resource_test.go
  • router/relay-router.go
  • setting/operation_setting/openai_batch_setting.go
  • web/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/src/features/channels/lib/__tests__/openai-batch-settings.test.ts
  • web/src/features/channels/lib/channel-form-errors.ts
  • web/src/features/channels/lib/channel-form.ts
  • web/src/features/channels/types.ts
  • web/src/features/system-settings/general/__tests__/system-behavior-settings.test.ts
  • web/src/features/system-settings/general/system-behavior-section.tsx
  • web/src/features/system-settings/general/system-behavior-settings.ts
  • web/src/features/system-settings/operations/index.tsx
  • web/src/features/system-settings/operations/section-registry.tsx
  • web/src/features/system-settings/types.ts
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/zh.json

Comment on lines +192 to +206
if shouldBind {
body, readErr := io.ReadAll(response.Body)
if readErr != nil {
openAIUpstreamResourceError(c, http.StatusBadGateway, "failed to read upstream response")
return
}
if bindErr := bindOpenAIUpstreamResourceResponse(c, body); bindErr != nil {
openAIUpstreamResourceError(c, http.StatusBadGateway, "failed to persist upstream resource binding")
return
}
copyOpenAIUpstreamResponseHeaders(c.Writer.Header(), response.Header)
c.Status(response.StatusCode)
_, _ = c.Writer.Write(body)
return
}

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Log the upstream resource id when binding fails.

The upstream file or batch already exists when bindOpenAIUpstreamResourceResponse fails. The handler returns 502 and discards the body, so the client never learns the upstream id. The resource then stays upstream with no way to reference or delete it through the gateway.

Log the upstream id and the binding error before returning, so operators can reconcile orphaned resources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/openai_upstream_resource.go` around lines 192 - 206, Update the
bindOpenAIUpstreamResourceResponse failure branch to log the upstream resource
ID and bindErr before calling openAIUpstreamResourceError and returning. Reuse
the existing upstream resource identifier from the handler’s response context,
preserving the current 502 behavior.

Comment thread middleware/distributor.go
Comment on lines +181 to +193
if !channelSupportsRequestPath(channel, c.Request.URL.Path, modelRequest.Model) {
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, "selected channel does not support this request path")
return
}
common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
SetupContextForSelectedChannel(c, channel, modelRequest.Model)
if setupErr := SetupContextForSelectedChannel(c, channel, modelRequest.Model); setupErr != nil {
statusCode := setupErr.StatusCode
if statusCode < http.StatusBadRequest || setupErr.GetErrorCode() == types.ErrorCodeChannelNoAvailableKey {
statusCode = http.StatusServiceUnavailable
}
abortWithOpenAiMessage(c, statusCode, setupErr.Error(), setupErr.GetErrorCode())
return
}

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.

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Guard the new checks when no channel was selected.

getModelRequest returns shouldSelectChannel == false for fetch-style routes (Midjourney task fetch, Suno fetch, video fetch by id, video remix). For those routes channel stays nil.

Line 181 now calls channelSupportsRequestPath(channel, ...), which returns false for a nil channel, so the request aborts with 503. Line 186 also now propagates the channel is nil error from SetupContextForSelectedChannel. Both aborts are new; the previous code ignored the setup error and continued. This breaks all task-fetch endpoints that route through Distribute().

Skip both checks when channel == nil.

🐛 Proposed fix
-		if !channelSupportsRequestPath(channel, c.Request.URL.Path, modelRequest.Model) {
-			abortWithOpenAiMessage(c, http.StatusServiceUnavailable, "selected channel does not support this request path")
-			return
-		}
-		common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
-		if setupErr := SetupContextForSelectedChannel(c, channel, modelRequest.Model); setupErr != nil {
-			statusCode := setupErr.StatusCode
-			if statusCode < http.StatusBadRequest || setupErr.GetErrorCode() == types.ErrorCodeChannelNoAvailableKey {
-				statusCode = http.StatusServiceUnavailable
-			}
-			abortWithOpenAiMessage(c, statusCode, setupErr.Error(), setupErr.GetErrorCode())
-			return
-		}
+		if channel != nil {
+			if !channelSupportsRequestPath(channel, c.Request.URL.Path, modelRequest.Model) {
+				abortWithOpenAiMessage(c, http.StatusServiceUnavailable, "selected channel does not support this request path")
+				return
+			}
+		}
+		common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
+		if channel != nil {
+			if setupErr := SetupContextForSelectedChannel(c, channel, modelRequest.Model); setupErr != nil {
+				statusCode := setupErr.StatusCode
+				if statusCode < http.StatusBadRequest || setupErr.GetErrorCode() == types.ErrorCodeChannelNoAvailableKey {
+					statusCode = http.StatusServiceUnavailable
+				}
+				abortWithOpenAiMessage(c, statusCode, setupErr.Error(), setupErr.GetErrorCode())
+				return
+			}
+		}

Run the following script to confirm which routes reach Distribute() without selecting a channel:

#!/bin/bash
# List routes that use Distribute and the shouldSelectChannel=false branches.
rg -n -C3 'shouldSelectChannel = false' middleware/distributor.go
rg -n 'middleware.Distribute\(\)' router/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@middleware/distributor.go` around lines 181 - 193, Guard both the channel
request-path validation and SetupContextForSelectedChannel error handling in
Distribute so they run only when channel is non-nil. Preserve the existing
behavior for selected channels while allowing fetch-style routes with
shouldSelectChannel == false and no channel to continue without either abort.

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.

1 participant