Skip to content

feat: add OpenAI video provider failover - #5117

Draft
ViperCai wants to merge 36 commits into
QuantumNous:mainfrom
ViperCai:feature/openai-video-failover
Draft

feat: add OpenAI video provider failover#5117
ViperCai wants to merge 36 commits into
QuantumNous:mainfrom
ViperCai:feature/openai-video-failover

Conversation

@ViperCai

@ViperCai ViperCai commented May 26, 2026

Copy link
Copy Markdown

Summary

  • add OpenAI Video provider routing for Hongniao, Qilin, LK888, Runway and related model mappings
  • update upstream API docs with current recommended/verified video models
  • document the new Coolify-managed deployment on 192.129.209.36:3001

Verification

  • did not run local test/build per project instruction
  • built and deployed on the new server through Coolify
  • verified health at http://192.129.209.36:3001/api/status
  • real generation verified with grok-video-3 task task_kTOu1dhTCYZvSYtynESiQQz0rEqlHOjO; /content returned 200 video/mp4

Summary by CodeRabbit

  • New Features

    • Added OpenAI Video channel (type 58) with support for video generation from multiple upstream providers including Runway, LK888, XB Sora, and Qilin.
    • Added ListenHub image generation provider (type 59).
    • Added support for new image generation models including GPT-4 Image and Gemini image variants.
    • Extended image generation with support for image editing capabilities.
  • Documentation

    • Added comprehensive API guides for video and image generation workflows.
    • Added provider-specific integration documentation.

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Looking for one thing? Review this PR in Change Stack to search files, summaries, diffs, and code without losing your place.

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR introduces an OpenAI Video channel with multi-provider relay support, image-channel adaptors, and extensive documentation. It adds provider-based task submission/polling, endpoint-type-aware routing, controller model discovery enhancements, and integrates support for multiple upstream platforms (LK888, Runway, XB-Sora, Qilin, BLTCY, Apexer, XGAPI, NewAPI) alongside ListenHub and expanded SiliconFlow image handling.

Changes

OpenAI Video channel and provider relay

Layer / File(s) Summary
Channel and relay metadata foundations
constant/api_type.go, constant/channel.go, constant/context_key.go, relay/common/relay_info.go, dto/openai_image.go, dto/openai_video.go, common/endpoint_type.go, common/api_type.go, common/model.go, web/default/src/features/channels/constants.ts, web/classic/src/constants/channel.constants.js
Adds APITypeListenHub, ChannelTypeOpenAIVideo=58, ContextKeyChannelOther constants; extends ChannelBaseURLs, ChannelTypeNames, ImageGenerationModels; adds VideoURL and ExtraBody DTO fields; adds ChannelOther to ChannelMeta and RelayInfo.InitChannelMeta; updates frontend channel constants.
Distributor endpoint filtering and task metadata propagation
middleware/distributor.go, relay/common/relay_utils.go, controller/task_video.go, relay/relay_task.go, service/task_polling.go
Adds requiredEndpointType selection and validation in Distribute, endpoint-type-aware channel selection, stores channel.Other in context; expands ValidateMultipartDirect for frames models; adds channel_other, origin_model_name, upstream_model_name to FetchTask request payloads.
Controller model discovery and video proxy updates
controller/channel.go, controller/channel_upstream_update.go, controller/video_proxy.go, controller/xb_sora_models_test.go
Adds XB-Sora models API detection and URL construction, XB-Sora response parsing; extends FetchModels with X-API-Key header support; adds ChannelTypeOpenAIVideo handling to VideoProxy with conditional X-API-Key header, runway: URL rewriting, and SSRF bypass logic; includes XB-Sora model tests.
OpenAI Video task adaptor framework and wiring
relay/relay_adaptor.go, relay/channel/task/openaivideo/adaptor.go, relay/channel/task/openaivideo/constants.go
Adds GetTaskAdaptor case for ChannelTypeOpenAIVideo returning TaskAdaptor; implements TaskAdaptor with full lifecycle (Init, ValidateRequestAndSetAction, EstimateBilling, BuildRequestURL/Header/Body, DoRequest/Response, FetchTask, ParseTaskResult, ConvertToOpenAIVideo); includes JSON↔multipart conversion and result URL rewriting.
Provider selection and base routing framework
relay/channel/task/openaivideo/provider.go
Adds getProvider, getProviderByHint, getProviderByBaseURL, getProviderForRelayInfo, getProviderForTaskFetch entrypoints; implements URL/model matching helpers and statusToTaskStatus for upstream status normalization.
Provider implementations: Apexer, BLTCY, LK888, NewAPI, Runway, XB-Sora, XGAPI
relay/channel/task/openaivideo/apexerapi.go, relay/channel/task/openaivideo/bltcy.go, relay/channel/task/openaivideo/lk888.go, relay/channel/task/openaivideo/newapi.go, relay/channel/task/openaivideo/runway.go, relay/channel/task/openaivideo/xb_sora.go, relay/channel/task/openaivideo/xgapi.go
Each provider defines submit/query URL builders, response parsers (extracting task ID, status, progress, URL), and request normalization (model mapping, parameter remapping, duration/orientation/image handling); includes multipart vs JSON behavior flags and image model mapping.
Qilin provider tests and edge cases
relay/channel/task/openaivideo/qilin_test.go, relay/channel/task/openaivideo/qilin.go
Adds provider implementation for Qilin/Grok with duration-based model translation (locked-duration and long-duration variants), image-reference field normalization, and aspect-ratio to resolution mapping; includes unit tests for duration/aspect-ratio handling and query response parsing.
ListenHub image relay adaptor
relay/channel/listenhub/adaptor.go, relay/channel/listenhub/constants.go, relay/channel/listenhub/dto.go, relay/channel/listenhub/image.go
Adds ListenHub image relay adaptor with request conversion, provider selection (openai vs google), image configuration derivation, reference-image parsing from multiple input shapes including data: URIs and nested objects; implements response unmarshalling and error handling with token-usage estimation.
SiliconFlow image adaptor expansions
relay/channel/siliconflow/adaptor.go, relay/channel/siliconflow/constant.go, relay/channel/siliconflow/dto.go, relay/channel/siliconflow/relay-siliconflow.go
Updates ConvertImageRequest to merge extra_body, compute ImageSize conditionally, parse OutputFormat; routes image generation/edit to /v1/images/generations with JSON headers; converts SFImageRequest numeric fields to pointers; adds siliconflowImageHandler for response conversion; includes image-input parsing from multiple shapes and multipart upload handling.
Runtime and auxiliary behavior updates
relay/helper/stream_scanner.go, relay/channel/claude/relay-claude.go, .gitignore, web/classic/src/index.jsx
Adds DefaultStreamingTimeout constant and validates computed timeout; adds Claude file-content conversion with MIME detection and base64 handling; adds .gitignore entries for generated videos, temp files, and lockfiles; removes Semi UI stylesheet import.
Pricing model entries and frontend channel configuration
setting/ratio_setting/model_ratio.go, web/default/package.json, web/default/src/features/channels/lib/channel-type-config.ts, web/default/src/features/channels/lib/channel-utils.ts
Expands defaultModelPrice with many new video/image model pricing entries; adds @types/hast dev dependency; adds CHANNEL_TYPE_CONFIGS[58] with OpenAI relay hints (base URLs, API key label, models); adds channel 58 icon mapping.
OpenAPI and documentation rewrite
docs/openapi/relay.json, docs/api-usage.md, docs/grok-video-api.md, docs/lk888-video-api.md, docs/sora-video-api.md, docs/deployment.md
Adds /v1/videos JSON requestBody schema to OpenAPI; rewrites api-usage.md with video/image/text endpoint guide, provider routing, architecture overview, and integration checklist; adds provider-specific video guides (Grok, LK888, Sora) with examples and capability matrices; expands deployment.md with Coolify production setup, channel configurations, provider verification, and known issues.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • QuantumNous/new-api#1659: Both PRs modify controller/task_video.go's updateVideoSingleTask logic—this PR changes the upstream FetchTask payload and metadata fields passed to the adapter.
  • QuantumNous/new-api#2036: Both PRs update the SiliconFlow image generation flow by converting image inputs into SFImageRequest and routing them to /v1/images/generations with updated request/response handling.
  • QuantumNous/new-api#1988: Both PRs modify controller/video_proxy.go for the /v1/videos/{task_id}/content handler, adding OpenAIVideo-specific headers and URL rewriting logic.

Suggested reviewers

  • seefs001
  • Calcium-Ion

🐰 A video channel hops into view with eight new providers in tow,
While Grok and Sora dance together in a unified, harmonious flow.
ListenHub joins the image relay, SiliconFlow expands its reach,
And docs bloom bright with examples—all the platforms now in teach! 🎬✨

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

@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: 17

🤖 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/channel.go`:
- Around line 207-214: The isXBSoraModelsAPI function is over-broadly treating
any URL ending with "/v1" or "/api/v1" as XB-Sora; change it to only detect
XB-Sora via explicit, unique indicators (e.g., substrings "xb-sora", "xbsora",
or known XB hostnames) and remove the generic suffix checks (HasSuffix(baseURL,
"/api/v1") and HasSuffix(baseURL, "/v1")). Update the function isXBSoraModelsAPI
so it normalizes the baseURL as before but returns true only when the baseURL
contains the XB-specific tokens ("xb-sora", "xbsora") or matches a whitelist of
XB-specific domains/hostnames; do not infer XB-Sora from generic "/v1" paths to
avoid misclassifying standard OpenAI-compatible providers.

In `@controller/video_proxy.go`:
- Around line 123-132: The code currently sets skipSSRFValidation = true for
rewritten runway: URLs which bypasses SSRF checks; instead remove or avoid
setting skipSSRFValidation and ensure the rewritten videoURL is validated via
ValidateURLWithFetchSetting (or explicitly call
ValidateURLWithFetchSetting(videoURL, ...) after composing videoURL) so the SSRF
policy still runs; apply the same change to the other runway handling block
around the later lines (the other occurrence uses the same skipSSRFValidation
flag) and keep references to channel.Type, constant.ChannelTypeOpenAIVideo,
videoURL, baseURL, skipSSRFValidation and ValidateURLWithFetchSetting to locate
and update the code.
- Around line 112-116: The OpenAIVideo proxy uses channel.Key but must prefer
the API key captured with the task; update the ChannelTypeOpenAIVideo branch so
when setting X-API-Key (inside the isHongniaoVideoChannel check) you use
task.PrivateData.Key if present/non-empty, falling back to channel.Key
otherwise; locate the case constant.ChannelTypeOpenAIVideo block (videoURL =
task.GetResultURL()) and replace the req.Header.Set("X-API-Key", channel.Key)
call accordingly, referencing task.PrivateData.Key as the primary source.

In `@controller/xb_sora_models_test.go`:
- Line 5: Replace direct uses of encoding/json in
controller/xb_sora_models_test.go with the project's wrapper: remove the
"encoding/json" import and replace any json.Unmarshal/json.Marshal calls with
common.Unmarshal/common.Marshal (or the appropriate wrapper names), e.g., update
test helpers or calls in functions like the test cases that currently call
json.Unmarshal to instead call common.Unmarshal and handle the returned
value/error the same way; ensure you import the common package and run tests to
confirm signatures match.

In `@docs/api-usage.md`:
- Around line 14-23: The file contains committed real-looking bearer tokens
(e.g., the literal string "sk-qZ9riqHVLChWVgVJXEkgYht3kVvVnnXS9xx9hVjzlcG7nKe9"
and the example header "Authorization: Bearer ...") — remove these secrets and
replace them with a placeholder (e.g., "sk-REDACTED" or "<YOUR_API_KEY_HERE>")
in the documentation and any example headers (the shown Authorization header),
then treat the exposed token as compromised: rotate/revoke it immediately and
update any CI/infra that used it; search the repo for the exact token string and
the Authorization example to ensure all occurrences (including the other
referenced spots) are redacted.

In `@docs/deployment.md`:
- Around line 175-176: The docs contain plaintext secrets (e.g., the sshpass
invocation with "-p 'm0HLTSun1xE4'" and exposed admin login credentials) —
remove all hardcoded passwords immediately, rotate/revoke the leaked
credentials, and replace them in the document with non-sensitive placeholders
(e.g., SSH_PASS, ADMIN_USER, ADMIN_PASS) that are retrieved from a secure source
at runtime (environment variables or a secret manager); update the examples to
show how to load secrets (env var or secret manager reference) and add a short
note instructing readers to fetch credentials securely and to confirm credential
rotation and notification to the security team after the leak is remediated.

In `@docs/grok-video-api.md`:
- Line 34: Replace the exposed test API key string
"sk-qZ9riqHVLChWVgVJXEkgYht3kVvVnnXS9xx9hVjzlcG7nKe9" in the documentation table
row labeled "测试 API Key" with a masked placeholder such as `YOUR_API_KEY` (or
`REDACTED`) and remove any real credential text; after changing the doc,
rotate/disable the exposed credential in your secrets manager and update any
environment variables that used it.

In `@docs/openapi/relay.json`:
- Around line 595-603: The schema for the seconds property is too narrow
(currently "seconds" typed as "string"); update the OpenAPI schema for the
seconds field so it accepts both numeric and string shapes used by callers
(e.g., change "type": "string" to a union like "type": ["string","integer"] or
an equivalent oneOf schema, and adjust the example if needed) so generated
clients and validation accept numeric inputs; target the "seconds" property in
docs/openapi/relay.json when making this change.

In `@relay/channel/claude/relay-claude.go`:
- Around line 382-420: The switch on mimeType inside the dto.ContentTypeFile
handling silently drops unsupported MIME types; update the logic in the handler
that builds claudeMediaMessages (the block that calls service.GetBase64Data and
switches on mimeType) to return a descriptive error when mimeType does not match
the handled cases ("application/pdf", "text/*", "image/*") instead of doing
nothing—include the offending mimeType and file.FileName (or other identifier)
in the error to aid debugging and ensure callers get notified of unsupported
file types.

In `@relay/channel/task/openaivideo/apexerapi.go`:
- Around line 35-38: The parsing currently only returns resp.ID and treats
responses with only resp.TaskID as failures; update the submit/parse logic in
apexerapi.go to accept resp.TaskID as a fallback (e.g., if resp.ID == "" use
resp.TaskID), return the non-empty value, and keep the error path that logs the
body when both are empty so valid upstream responses containing only task_id are
handled.

In `@relay/channel/task/openaivideo/newapi.go`:
- Around line 22-31: The response struct newapiQueryResponse is missing the
video URL field so parseQueryResponse never sets TaskInfo.Url; add a Url string
field to newapiQueryResponse with the correct JSON tag matching the API (e.g.,
`Url string `json:"url"` or `json:"result_url"` as the API returns) and update
parseQueryResponse to read that field and assign it to TaskInfo.Url on success
(handle empty/omitted values and preserve existing error/status handling);
repeat the same addition/update for the other similar struct(s) referenced in
the diff (the definitions around lines 58-78) so all success paths populate
TaskInfo.Url.

In `@relay/channel/task/openaivideo/provider.go`:
- Around line 158-163: The isXBSoraBaseURL function is too permissive by
treating any URL ending with "/v1" or "/api/v1" as XB-Sora; narrow the detection
to avoid misrouting by removing the generic HasSuffix checks and instead
validate the actual hostname or path tokens for XB-Sora-specific markers: update
isXBSoraBaseURL to parse the URL (use net/url.Parse), inspect u.Host and u.Path,
and return true only if the host or path contains the explicit identifiers
("xb-sora2", "xbsora2", "xb-sora", "xbsora") or matches a known XB-Sora path
pattern (e.g., path segments like "/xb-sora/v1" or "/api/v1/xb-sora"); ensure
you still call containsAny on the normalized host/path but do not treat a bare
"/v1" or "/api/v1" suffix as sufficient.

In `@relay/channel/task/openaivideo/runway.go`:
- Around line 68-72: The success branch only reads resp.Result.FileURLs[0], so
ti.Url remains empty when upstream returns resp.Result.Files instead; update the
block in runway.go (around the code that sets ti.Url) to fall back to
resp.Result.Files (or the file URL field in those objects) when FileURLs is
empty: check resp.Result.FileURLs first, if empty iterate or check
resp.Result.Files for a non-empty trimmed URL (e.g., Files[0].URL or
equivalent), then set ti.Url = "runway:"+thatURL only when a non-empty value is
found.

In `@relay/channel/task/openaivideo/xb_sora_test.go`:
- Line 5: Remove the direct use of encoding/json in xb_sora_test.go: delete the
"encoding/json" import and replace the call json.Unmarshal(body, &req) with
common.Unmarshal(body, &req); also add/import the package alias common in the
test imports so the call to common.Unmarshal(body, &req) resolves, keeping the
same variables (body, req) and error handling unchanged.

In `@relay/channel/task/openaivideo/xb_sora.go`:
- Line 4: Change xbSoraResponseEnvelope.Data from json.RawMessage to a
wrapper-free type (e.g., any) and remove the direct encoding/json import; in
unwrapXBSoraNestedResponse, when you need to re-process the nested payload,
re-marshal the Data value using common.Marshal and then feed that bytes into
common.Unmarshal for the second-level decode (use the existing
common.Marshal/common.Unmarshal helpers). Update references in
xbSoraResponseEnvelope and unwrapXBSoraNestedResponse accordingly so no direct
encoding/json usage remains.

In `@relay/common/relay_utils.go`:
- Around line 161-167: The code currently only validates req.AspectRatio when
non-empty, allowing frames/components models to proceed without an aspect ratio;
update the isFramesModel branch (where isFramesModel(model) and
hasInputReference are checked) to require req.AspectRatio be present and equal
to either "9:16" or "16:9" — if missing or invalid, return
createTaskError(fmt.Errorf(...), "invalid_aspect_ratio", http.StatusBadRequest,
true); keep the existing TaskActionGenerate assignment logic (action =
constant.TaskActionGenerate) but perform the required-aspect_ratio check before
continuing.

In `@relay/relay_task.go`:
- Around line 440-446: adaptor.FetchTask is currently called with
channelModel.Key which can break for tasks created with rotated or alternate
keys; change the key argument to prefer task.PrivateData.Key (or
task.PrivateData.Get("Key")/the appropriate field) and fall back to
channelModel.Key when PrivateData.Key is empty/nil, so the call to
adaptor.FetchTask(..., key, ...) uses the task-stored key; update the call site
where FetchTask is invoked (referencing adaptor.FetchTask, channelModel.Key, and
task.PrivateData.Key) to compute key := task.PrivateData.Key; if empty use
channelModel.Key, then pass that key into FetchTask alongside
task.GetUpstreamTaskID(), task.Action, channelModel.Other,
task.Properties.OriginModelName, task.Properties.UpstreamModelName, and proxy.
🪄 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: 43ac26be-93da-4e07-b82c-eea2aab97914

📥 Commits

Reviewing files that changed from the base of the PR and between 65f8afe and e90a2c1.

📒 Files selected for processing (51)
  • .gitignore
  • common/endpoint_type.go
  • common/model.go
  • constant/channel.go
  • constant/context_key.go
  • controller/channel.go
  • controller/channel_upstream_update.go
  • controller/task_video.go
  • controller/video_proxy.go
  • controller/xb_sora_models_test.go
  • docs/api-usage.md
  • docs/deployment.md
  • docs/grok-video-api.md
  • docs/lk888-video-api.md
  • docs/openapi/relay.json
  • docs/sora-video-api.md
  • dto/openai_image.go
  • dto/openai_video.go
  • middleware/distributor.go
  • model/channel_cache.go
  • relay/channel/claude/relay-claude.go
  • relay/channel/gemini/constant.go
  • relay/channel/openai/constant.go
  • relay/channel/task/openaivideo/adaptor.go
  • relay/channel/task/openaivideo/apexerapi.go
  • relay/channel/task/openaivideo/bltcy.go
  • relay/channel/task/openaivideo/constants.go
  • relay/channel/task/openaivideo/lk888.go
  • relay/channel/task/openaivideo/newapi.go
  • relay/channel/task/openaivideo/provider.go
  • relay/channel/task/openaivideo/qilin.go
  • relay/channel/task/openaivideo/qilin_test.go
  • relay/channel/task/openaivideo/runway.go
  • relay/channel/task/openaivideo/xb_sora.go
  • relay/channel/task/openaivideo/xb_sora_test.go
  • relay/channel/task/openaivideo/xgapi.go
  • relay/common/relay_info.go
  • relay/common/relay_utils.go
  • relay/helper/stream_scanner.go
  • relay/relay_adaptor.go
  • relay/relay_task.go
  • service/task_polling.go
  • setting/ratio_setting/model_ratio.go
  • web/classic/src/constants/channel.constants.js
  • web/classic/src/helpers/render.jsx
  • web/classic/src/index.jsx
  • web/classic/vite.config.js
  • web/default/package.json
  • web/default/src/features/channels/constants.ts
  • web/default/src/features/channels/lib/channel-type-config.ts
  • web/default/src/features/channels/lib/channel-utils.ts
💤 Files with no reviewable changes (2)
  • model/channel_cache.go
  • web/classic/src/index.jsx

Comment thread controller/channel.go
Comment on lines +207 to +214
func isXBSoraModelsAPI(baseURL string) bool {
baseURL = strings.ToLower(strings.TrimRight(strings.TrimSpace(baseURL), "/"))
return strings.Contains(baseURL, "xb-sora2") ||
strings.Contains(baseURL, "xbsora2") ||
strings.Contains(baseURL, "xb-sora") ||
strings.Contains(baseURL, "xbsora") ||
strings.HasSuffix(baseURL, "/api/v1") ||
strings.HasSuffix(baseURL, "/v1")

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 | 🟠 Major | ⚡ Quick win

Over-broad XB-Sora detection can break non-XB video providers.

Classifying any base URL ending with /v1 or /api/v1 as XB-Sora will force X-API-Key + XB-specific parsing for standard OpenAI-compatible providers that still require Bearer auth.

💡 Proposed fix
 func isXBSoraModelsAPI(baseURL string) bool {
 	baseURL = strings.ToLower(strings.TrimRight(strings.TrimSpace(baseURL), "/"))
 	return strings.Contains(baseURL, "xb-sora2") ||
 		strings.Contains(baseURL, "xbsora2") ||
 		strings.Contains(baseURL, "xb-sora") ||
-		strings.Contains(baseURL, "xbsora") ||
-		strings.HasSuffix(baseURL, "/api/v1") ||
-		strings.HasSuffix(baseURL, "/v1")
+		strings.Contains(baseURL, "xbsora")
 }
🤖 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/channel.go` around lines 207 - 214, The isXBSoraModelsAPI function
is over-broadly treating any URL ending with "/v1" or "/api/v1" as XB-Sora;
change it to only detect XB-Sora via explicit, unique indicators (e.g.,
substrings "xb-sora", "xbsora", or known XB hostnames) and remove the generic
suffix checks (HasSuffix(baseURL, "/api/v1") and HasSuffix(baseURL, "/v1")).
Update the function isXBSoraModelsAPI so it normalizes the baseURL as before but
returns true only when the baseURL contains the XB-specific tokens ("xb-sora",
"xbsora") or matches a whitelist of XB-specific domains/hostnames; do not infer
XB-Sora from generic "/v1" paths to avoid misclassifying standard
OpenAI-compatible providers.

Comment thread controller/video_proxy.go
Comment on lines +112 to +116
case constant.ChannelTypeOpenAIVideo:
videoURL = task.GetResultURL()
if isHongniaoVideoChannel(baseURL, channel.Other) {
req.Header.Set("X-API-Key", channel.Key)
}

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 | 🟠 Major | ⚡ Quick win

Use task-scoped API key for OpenAIVideo proxy fetches.

This currently uses channel.Key, which can diverge from the key used at submission time and break follow-up fetches for existing tasks.

Suggested patch
 	case constant.ChannelTypeOpenAIVideo:
 		videoURL = task.GetResultURL()
 		if isHongniaoVideoChannel(baseURL, channel.Other) {
-			req.Header.Set("X-API-Key", channel.Key)
+			apiKey := task.PrivateData.Key
+			if apiKey == "" {
+				apiKey = channel.Key
+			}
+			req.Header.Set("X-API-Key", apiKey)
 		}

Based on learnings: "In async video task flows ... follow-up requests (polling, content download) reuse the authentication context captured at task submission time ... Prefer task.PrivateData.Key when available over channel.Key."

🤖 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/video_proxy.go` around lines 112 - 116, The OpenAIVideo proxy uses
channel.Key but must prefer the API key captured with the task; update the
ChannelTypeOpenAIVideo branch so when setting X-API-Key (inside the
isHongniaoVideoChannel check) you use task.PrivateData.Key if present/non-empty,
falling back to channel.Key otherwise; locate the case
constant.ChannelTypeOpenAIVideo block (videoURL = task.GetResultURL()) and
replace the req.Header.Set("X-API-Key", channel.Key) call accordingly,
referencing task.PrivateData.Key as the primary source.

Comment thread controller/video_proxy.go
Comment on lines +123 to +132
skipSSRFValidation := false
if channel.Type == constant.ChannelTypeOpenAIVideo && strings.HasPrefix(videoURL, "runway:") {
filePath := strings.TrimSpace(strings.TrimPrefix(videoURL, "runway:"))
if !strings.HasPrefix(filePath, "/files/") {
videoProxyError(c, http.StatusBadGateway, "server_error", "Invalid Runway file URL")
return
}
videoURL = strings.TrimRight(baseURL, "/") + filePath
skipSSRFValidation = true
}

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 | 🟠 Major | ⚡ Quick win

Do not bypass SSRF validation for rewritten runway: URLs.

The rewritten URL is still externally fetched; skipping ValidateURLWithFetchSetting creates a policy bypass path.

Suggested patch
-	skipSSRFValidation := false
 	if channel.Type == constant.ChannelTypeOpenAIVideo && strings.HasPrefix(videoURL, "runway:") {
 		filePath := strings.TrimSpace(strings.TrimPrefix(videoURL, "runway:"))
 		if !strings.HasPrefix(filePath, "/files/") {
 			videoProxyError(c, http.StatusBadGateway, "server_error", "Invalid Runway file URL")
 			return
 		}
 		videoURL = strings.TrimRight(baseURL, "/") + filePath
-		skipSSRFValidation = true
 	}
@@
-	if !skipSSRFValidation {
-		if err := common.ValidateURLWithFetchSetting(videoURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
-			logger.LogError(c.Request.Context(), fmt.Sprintf("Video URL blocked for task %s: %v", taskID, err))
-			videoProxyError(c, http.StatusForbidden, "server_error", fmt.Sprintf("request blocked: %v", err))
-			return
-		}
+	if err := common.ValidateURLWithFetchSetting(videoURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
+		logger.LogError(c.Request.Context(), fmt.Sprintf("Video URL blocked for task %s: %v", taskID, err))
+		videoProxyError(c, http.StatusForbidden, "server_error", fmt.Sprintf("request blocked: %v", err))
+		return
 	}

Also applies to: 148-153

🤖 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/video_proxy.go` around lines 123 - 132, The code currently sets
skipSSRFValidation = true for rewritten runway: URLs which bypasses SSRF checks;
instead remove or avoid setting skipSSRFValidation and ensure the rewritten
videoURL is validated via ValidateURLWithFetchSetting (or explicitly call
ValidateURLWithFetchSetting(videoURL, ...) after composing videoURL) so the SSRF
policy still runs; apply the same change to the other runway handling block
around the later lines (the other occurrence uses the same skipSSRFValidation
flag) and keep references to channel.Type, constant.ChannelTypeOpenAIVideo,
videoURL, baseURL, skipSSRFValidation and ValidateURLWithFetchSetting to locate
and update the code.


import (
"bytes"
"encoding/json"

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use common.Unmarshal instead of encoding/json in this test file.

This test directly imports/calls encoding/json; switch to the project JSON wrapper for consistency with repository rules.

Suggested patch
 import (
 	"bytes"
-	"encoding/json"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	"github.com/QuantumNous/new-api/common"
 	"github.com/QuantumNous/new-api/constant"
 	"github.com/QuantumNous/new-api/model"
 	"github.com/gin-gonic/gin"
 )
@@
-	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+	if err := common.Unmarshal(w.Body.Bytes(), &resp); err != nil {
 		t.Fatalf("unmarshal response: %v", err)
 	}

As per coding guidelines: "**/*.go: All JSON marshal/unmarshal operations MUST use wrapper functions from common/json.go... Do NOT directly import or call encoding/json in business code."

Also applies to: 100-102

🤖 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/xb_sora_models_test.go` at line 5, Replace direct uses of
encoding/json in controller/xb_sora_models_test.go with the project's wrapper:
remove the "encoding/json" import and replace any json.Unmarshal/json.Marshal
calls with common.Unmarshal/common.Marshal (or the appropriate wrapper names),
e.g., update test helpers or calls in functions like the test cases that
currently call json.Unmarshal to instead call common.Unmarshal and handle the
returned value/error the same way; ensure you import the common package and run
tests to confirm signatures match.

Comment thread docs/api-usage.md Outdated
Comment on lines +68 to +72
if ti.Status == model.TaskStatusSuccess && resp.Result != nil {
if len(resp.Result.FileURLs) > 0 && strings.TrimSpace(resp.Result.FileURLs[0]) != "" {
ti.Url = "runway:" + strings.TrimSpace(resp.Result.FileURLs[0])
}
}

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 | 🟠 Major | ⚡ Quick win

Add fallback to result.files when fileUrls is empty.

On Line 68-72, success handling only reads resp.Result.FileURLs[0]. If upstream returns only files, TaskInfo.Url stays empty even on success.

Proposed fix
  if ti.Status == model.TaskStatusSuccess && resp.Result != nil {
-   if len(resp.Result.FileURLs) > 0 && strings.TrimSpace(resp.Result.FileURLs[0]) != "" {
-     ti.Url = "runway:" + strings.TrimSpace(resp.Result.FileURLs[0])
-   }
+   if len(resp.Result.FileURLs) > 0 && strings.TrimSpace(resp.Result.FileURLs[0]) != "" {
+     ti.Url = "runway:" + strings.TrimSpace(resp.Result.FileURLs[0])
+   } else if len(resp.Result.Files) > 0 && strings.TrimSpace(resp.Result.Files[0]) != "" {
+     ti.Url = "runway:" + strings.TrimSpace(resp.Result.Files[0])
+   }
  }
🤖 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 `@relay/channel/task/openaivideo/runway.go` around lines 68 - 72, The success
branch only reads resp.Result.FileURLs[0], so ti.Url remains empty when upstream
returns resp.Result.Files instead; update the block in runway.go (around the
code that sets ti.Url) to fall back to resp.Result.Files (or the file URL field
in those objects) when FileURLs is empty: check resp.Result.FileURLs first, if
empty iterate or check resp.Result.Files for a non-empty trimmed URL (e.g.,
Files[0].URL or equivalent), then set ti.Url = "runway:"+thatURL only when a
non-empty value is found.


import (
"bytes"
"encoding/json"

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify there are no direct encoding/json marshal/unmarshal calls in openaivideo tests.
rg -n --type go '^\s*"encoding/json"|json\.(Marshal|Unmarshal|NewDecoder|NewEncoder)\s*\(' relay/channel/task/openaivideo

Repository: QuantumNous/new-api

Length of output: 295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show imports and the json.Unmarshal usage region
sed -n '1,120p' relay/channel/task/openaivideo/xb_sora_test.go
echo "----"
sed -n '200,270p' relay/channel/task/openaivideo/xb_sora_test.go

echo "==== common/json.go ===="
# Locate the wrapper definitions
COMMON_JSON_FILE="$(fd -a -t f 'json.go' common model relay | head -n 20 | rg 'common/json.go$' || true)"
if [ -z "${COMMON_JSON_FILE}" ]; then
  # fallback: try common/json.go specifically
  if [ -f common/json.go ]; then COMMON_JSON_FILE=common/json.go; fi
fi
if [ -z "${COMMON_JSON_FILE}" ]; then
  # last resort: find any json.go under common
  COMMON_JSON_FILE="$(fd -a -t f 'json.go' | rg '^common/' | head -n 1 || true)"
fi
echo "Using: ${COMMON_JSON_FILE}"
sed -n '1,220p' "${COMMON_JSON_FILE}"

Repository: QuantumNous/new-api

Length of output: 8081


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --type go 'encoding/json|json\.(Marshal|Unmarshal|NewDecoder|NewEncoder)\s*\(' relay/channel/task/openaivideo/xb_sora_test.go

Repository: QuantumNous/new-api

Length of output: 140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check existing usage of common.Unmarshal/common import patterns in the file
rg -n 'github\.com/QuantumNous/new-api/common|common\.Unmarshal|common\.Marshal|common\.DecodeJson' relay/channel/task/openaivideo/xb_sora_test.go || true

# Confirm common import path usage elsewhere (quick sample)
rg -n --type go 'github\.com/QuantumNous/new-api/common' relay/channel/task/openaivideo | head -n 20

Repository: QuantumNous/new-api

Length of output: 823


Replace direct encoding/json usage with common.Unmarshal in xb_sora_test.go

  • Remove the encoding/json import and change json.Unmarshal(body, &req) (line ~241) to common.Unmarshal(body, &req).
Proposed patch
 import (
 	"bytes"
-	"encoding/json"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
+	"github.com/QuantumNous/new-api/common"
 	"github.com/QuantumNous/new-api/model"
 	relaycommon "github.com/QuantumNous/new-api/relay/common"
 	"github.com/QuantumNous/new-api/service"
 	"github.com/gin-gonic/gin"
 )
@@
 			var req map[string]any
-			if err := json.Unmarshal(body, &req); err != nil {
+			if err := common.Unmarshal(body, &req); err != nil {
 				t.Fatalf("submit body is not json: %v", err)
 			}
🤖 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 `@relay/channel/task/openaivideo/xb_sora_test.go` at line 5, Remove the direct
use of encoding/json in xb_sora_test.go: delete the "encoding/json" import and
replace the call json.Unmarshal(body, &req) with common.Unmarshal(body, &req);
also add/import the package alias common in the test imports so the call to
common.Unmarshal(body, &req) resolves, keeping the same variables (body, req)
and error handling unchanged.

package openaivideo

import (
"encoding/json"

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="relay/channel/task/openaivideo/xb_sora.go"

echo "== Imports and json usage in $FILE =="
sed -n '1,120p' "$FILE" | cat -n

echo
echo "== Search for encoding/json usage =="
rg -n "encoding/json|json\.Marshal|json\.Unmarshal|json\.NewDecoder|json\.NewEncoder|json\.RawMessage" "$FILE" || true

echo
echo "== Check wrapper availability in common/json.go =="
if [ -f "common/json.go" ]; then
  sed -n '1,200p' common/json.go | cat -n
  rg -n "func (Marshal|Unmarshal|UnmarshalJsonStr|DecodeJson|GetJsonType)\\b" common/json.go || true
else
  echo "common/json.go not found at repo root"
fi

Repository: QuantumNous/new-api

Length of output: 6608


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="relay/channel/task/openaivideo/xb_sora.go"

echo "== RawMessage occurrences =="
rg -n "\bRawMessage\b" "$FILE" || true

echo
echo "== json. selector occurrences =="
rg -n "\bjson\." "$FILE" || true

echo
echo "== Show envelope handling / unwrap function area =="
rg -n "type xbSoraResponseEnvelope|unwrapXBSoraNestedResponse|Data:" "$FILE" -n --context=5 || true

Repository: QuantumNous/new-api

Length of output: 1615


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find RawMessage aliases in common/ =="
rg -n "type\s+.*RawMessage|RawMessage\s*=" common || true
rg -n "JsonRawMessageToString|GetJsonType" common/json.go || true

echo
echo "== Inspect unwrapXBSoraNestedResponse implementation (relevant section) =="
sed -n '430,520p' relay/channel/task/openaivideo/xb_sora.go | cat -n

Repository: QuantumNous/new-api

Length of output: 2629


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find any common RawMessage alias/type =="
rg -n "package common" -S common || true
rg -n "type\s+\w*RawMessage\s*=" -S . || true
rg -n "type\s+RawMessage\s*=" -S . || true

echo
echo "== Find other business code imports of encoding/json for RawMessage (context) =="
rg -n "\"encoding/json\"" -S relay | head -n 50 || true
rg -n "json\.RawMessage" -S relay | head -n 50 || true

Repository: QuantumNous/new-api

Length of output: 7922


Remove direct encoding/json import from relay/channel/task/openaivideo/xb_sora.go.
xb_sora.go imports encoding/json at line 4 to use json.RawMessage for xbSoraResponseEnvelope.Data, which violates the “no direct encoding/json import in business code” rule. Refactor Data to a wrapper-free type (e.g., any) and in unwrapXBSoraNestedResponse re-marshal via common.Marshal before the second common.Unmarshal.

🤖 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 `@relay/channel/task/openaivideo/xb_sora.go` at line 4, Change
xbSoraResponseEnvelope.Data from json.RawMessage to a wrapper-free type (e.g.,
any) and remove the direct encoding/json import; in unwrapXBSoraNestedResponse,
when you need to re-process the nested payload, re-marshal the Data value using
common.Marshal and then feed that bytes into common.Unmarshal for the
second-level decode (use the existing common.Marshal/common.Unmarshal helpers).
Update references in xbSoraResponseEnvelope and unwrapXBSoraNestedResponse
accordingly so no direct encoding/json usage remains.

Comment on lines +161 to +167
if isFramesModel(model) {
if hasInputReference {
action = constant.TaskActionGenerate
}
if req.AspectRatio != "" && !lo.Contains([]string{"9:16", "16:9"}, req.AspectRatio) {
return createTaskError(fmt.Errorf("aspect_ratio must be 9:16 or 16:9"), "invalid_aspect_ratio", http.StatusBadRequest, true)
}

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 | 🟠 Major | ⚡ Quick win

Require aspect_ratio for frames/components models.

Line 165 only validates when aspect_ratio is non-empty, so frames requests without this field bypass validation and may fail later upstream. Make it required and constrained to 9:16 or 16:9.

Suggested patch
 	if isFramesModel(model) {
 		if hasInputReference {
 			action = constant.TaskActionGenerate
 		}
-		if req.AspectRatio != "" && !lo.Contains([]string{"9:16", "16:9"}, req.AspectRatio) {
+		if req.AspectRatio == "" || !lo.Contains([]string{"9:16", "16:9"}, req.AspectRatio) {
 			return createTaskError(fmt.Errorf("aspect_ratio must be 9:16 or 16:9"), "invalid_aspect_ratio", http.StatusBadRequest, true)
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if isFramesModel(model) {
if hasInputReference {
action = constant.TaskActionGenerate
}
if req.AspectRatio != "" && !lo.Contains([]string{"9:16", "16:9"}, req.AspectRatio) {
return createTaskError(fmt.Errorf("aspect_ratio must be 9:16 or 16:9"), "invalid_aspect_ratio", http.StatusBadRequest, true)
}
if isFramesModel(model) {
if hasInputReference {
action = constant.TaskActionGenerate
}
if req.AspectRatio == "" || !lo.Contains([]string{"9:16", "16:9"}, req.AspectRatio) {
return createTaskError(fmt.Errorf("aspect_ratio must be 9:16 or 16:9"), "invalid_aspect_ratio", http.StatusBadRequest, true)
}
}
🤖 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 `@relay/common/relay_utils.go` around lines 161 - 167, The code currently only
validates req.AspectRatio when non-empty, allowing frames/components models to
proceed without an aspect ratio; update the isFramesModel branch (where
isFramesModel(model) and hasInputReference are checked) to require
req.AspectRatio be present and equal to either "9:16" or "16:9" — if missing or
invalid, return createTaskError(fmt.Errorf(...), "invalid_aspect_ratio",
http.StatusBadRequest, true); keep the existing TaskActionGenerate assignment
logic (action = constant.TaskActionGenerate) but perform the
required-aspect_ratio check before continuing.

Comment thread relay/relay_task.go
Comment on lines 440 to 446
resp, err := adaptor.FetchTask(baseURL, channelModel.Key, map[string]any{
"task_id": task.GetUpstreamTaskID(),
"action": task.Action,
"task_id": task.GetUpstreamTaskID(),
"action": task.Action,
"channel_other": channelModel.Other,
"origin_model_name": task.Properties.OriginModelName,
"upstream_model_name": task.Properties.UpstreamModelName,
}, proxy)

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 | 🟠 Major | ⚡ Quick win

Prefer task-stored key when realtime-fetching upstream task status.

Using channelModel.Key here can fail for tasks created with a rotated/alternate key; prefer task.PrivateData.Key with fallback.

Suggested patch
-	resp, err := adaptor.FetchTask(baseURL, channelModel.Key, map[string]any{
+	key := task.PrivateData.Key
+	if key == "" {
+		key = channelModel.Key
+	}
+	resp, err := adaptor.FetchTask(baseURL, key, map[string]any{
 		"task_id":             task.GetUpstreamTaskID(),
 		"action":              task.Action,
 		"channel_other":       channelModel.Other,
 		"origin_model_name":   task.Properties.OriginModelName,
 		"upstream_model_name": task.Properties.UpstreamModelName,
 	}, proxy)
🤖 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 `@relay/relay_task.go` around lines 440 - 446, adaptor.FetchTask is currently
called with channelModel.Key which can break for tasks created with rotated or
alternate keys; change the key argument to prefer task.PrivateData.Key (or
task.PrivateData.Get("Key")/the appropriate field) and fall back to
channelModel.Key when PrivateData.Key is empty/nil, so the call to
adaptor.FetchTask(..., key, ...) uses the task-stored key; update the call site
where FetchTask is invoked (referencing adaptor.FetchTask, channelModel.Key, and
task.PrivateData.Key) to compute key := task.PrivateData.Key; if empty use
channelModel.Key, then pass that key into FetchTask alongside
task.GetUpstreamTaskID(), task.Action, channelModel.Other,
task.Properties.OriginModelName, task.Properties.UpstreamModelName, and proxy.

ViperCai added 5 commits May 27, 2026 11:07
Includes:
- Channel list query refactoring (buildChannelListQuery)
- gjson-based fast model name extraction in distributor
- UpstreamRequestBodySize and hasRemovableDisabledField optimization
- Claude fcIdx offset fix (concurrent tool call collision)
- Multi-key auto-disabled channel cache eviction fix
- Frontend dependency upgrades (React 19.2.6, TS 6.0, shadcn 4.8)
- Channel editor UX rebuild
- Waffo Pancake gateway integration
- Security: compliance confirmation for paid features
- Performance: println→LogDebug, avoid eager formatting
api-usage.md:
- 更新测试 API Key 为无限额度 Key
- 验证日期 2026-05-24 → 2026-05-28
- 推荐模型从 4 个扩展到 5 个: +ss-sora-2, +veo3.1-4k
- grok-video-3 因 LK888 上游故障从推荐降级为尝试
- 新增全能视频2.0 不可用记录
- 更新所有 task_id、下载大小、Python/cURL 示例 API Key
- 路由实务重新排序体现当前可用性优先级
- grok-imagine-1.0-video 标注仅支持 5 种特定尺寸
- Runway 系列标记为未部署,Kling 3.0/O3 待就绪

deployment.md:
- 新增 2026-05-28 部署验证记录 (upstream merge/docker/全模型回归)
- Runway 适配器状态更新: 当前未配置

sora-video-api.md: xb-sora2 + ss-sora-2 回归验证
grok-video-api.md: grok-imagine-1.0-video 回归验证 + 尺寸约束
lk888-video-api.md: grok-video-3 上游故障状态

@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.

♻️ Duplicate comments (1)
docs/api-usage.md (1)

14-23: ⚠️ Potential issue | 🔴 Critical

Duplicate: Exposed API key in documentation.

This was flagged in a previous review. The test API key should be replaced with a placeholder to prevent unauthorized usage and follow security best practices.

Also applies to: 34-35, 43-44, 52-53, 63-64, 72-73, 718-719

🤖 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 `@docs/api-usage.md` around lines 14 - 23, The doc exposes a real test API key
string (EW93ybOP6Zr1axAPYNEu8VpehQzdTkZBTATszAGYEDiwpCmJ) and literal
Authorization header examples; replace every occurrence of that exact key and
the inline header example ("Authorization: Bearer ...") with a clear placeholder
like <YOUR_API_KEY> or {TEST_API_KEY} in the table cell (测试 API Key) and in all
code blocks and examples (e.g., the "Authorization: Bearer ..." snippet) to
avoid leaking credentials; search the file for the exact key string to update
all instances.
🤖 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.

Duplicate comments:
In `@docs/api-usage.md`:
- Around line 14-23: The doc exposes a real test API key string
(EW93ybOP6Zr1axAPYNEu8VpehQzdTkZBTATszAGYEDiwpCmJ) and literal Authorization
header examples; replace every occurrence of that exact key and the inline
header example ("Authorization: Bearer ...") with a clear placeholder like
<YOUR_API_KEY> or {TEST_API_KEY} in the table cell (测试 API Key) and in all code
blocks and examples (e.g., the "Authorization: Bearer ..." snippet) to avoid
leaking credentials; search the file for the exact key string to update all
instances.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a52772f-e7e5-4167-8ee7-9c3a57a026a8

📥 Commits

Reviewing files that changed from the base of the PR and between 42c4d46 and ae376ab.

📒 Files selected for processing (29)
  • common/api_type.go
  • common/endpoint_type.go
  • common/model.go
  • constant/api_type.go
  • constant/channel.go
  • controller/channel.go
  • controller/task_video.go
  • docs/api-usage.md
  • docs/deployment.md
  • docs/grok-video-api.md
  • docs/lk888-video-api.md
  • docs/sora-video-api.md
  • middleware/distributor.go
  • model/channel_cache.go
  • relay/channel/claude/relay-claude.go
  • relay/channel/listenhub/adaptor.go
  • relay/channel/listenhub/constants.go
  • relay/channel/listenhub/dto.go
  • relay/channel/listenhub/image.go
  • relay/channel/openai/constant.go
  • relay/common/relay_info.go
  • relay/helper/stream_scanner.go
  • relay/relay_adaptor.go
  • service/task_polling.go
  • web/classic/src/constants/channel.constants.js
  • web/default/package.json
  • web/default/src/features/channels/constants.ts
  • web/default/src/features/channels/lib/channel-type-config.ts
  • web/default/src/features/channels/lib/channel-utils.ts
✅ Files skipped from review due to trivial changes (7)
  • relay/channel/openai/constant.go
  • relay/channel/listenhub/constants.go
  • model/channel_cache.go
  • docs/sora-video-api.md
  • docs/lk888-video-api.md
  • docs/grok-video-api.md
  • docs/deployment.md
🚧 Files skipped from review as they are similar to previous changes (14)
  • common/model.go
  • controller/task_video.go
  • common/endpoint_type.go
  • web/default/package.json
  • web/classic/src/constants/channel.constants.js
  • service/task_polling.go
  • web/default/src/features/channels/lib/channel-type-config.ts
  • web/default/src/features/channels/constants.ts
  • constant/channel.go
  • relay/common/relay_info.go
  • relay/helper/stream_scanner.go
  • controller/channel.go
  • middleware/distributor.go
  • relay/channel/claude/relay-claude.go

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/deployment.md (1)

449-459: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify ListenHub status to avoid operational contradiction.

This section says the channel is manually disabled pending type=59 deployment, but other updated docs describe unified-entry verification as already completed. Please add explicit “historical snapshot as-of date” wording or update to current status to prevent wrong rollout decisions.

🤖 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 `@docs/deployment.md` around lines 449 - 459, Update the ListenHub status
paragraph to remove ambiguity: either add an explicit "historical snapshot
as-of" sentence (e.g., "Status as of 2026-06-01: ...") before the sentence
mentioning manual disablement of channel `listenhub-images` (ID 12,
`priority=120`, `type=59`) so readers know the date of that observation, or
replace the sentence with a current-status sentence that clearly states whether
`type=59` support has been merged and the channel is now enabled; ensure you
reference the channel name `listenhub-images`, the `type=59` deployment
dependency, and the Base URL `https://api.marswave.ai/openapi` so the
operational state is unambiguous.
🤖 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 `@relay/channel/siliconflow/dto.go`:
- Line 29: The OutputFormat field is currently a plain string which loses
absent-vs-explicit-empty semantics when re-marshaled; change OutputFormat to a
pointer type (*string) with the existing `json:"output_format,omitempty"` tag in
the DTO struct that declares OutputFormat, and update any code that reads or
forwards this field to handle nil (absent) vs non-nil (explicit) values
appropriately so upstream requests preserve presence semantics.

In `@relay/channel/siliconflow/relay-siliconflow.go`:
- Around line 59-65: The code returns a provider error using resp.StatusCode
which can be 200, causing an error payload with a success HTTP status; update
the error-return in the block that checks siliconflowResp.Images and
siliconflowResp.Message to pass a non-200 status to types.WithOpenAIError (for
example http.StatusBadGateway or map siliconflowResp.Code to a valid HTTP error
code) instead of resp.StatusCode, and ensure you fall back to a safe non-200
value if siliconflowResp.Code is missing/invalid; adjust the call that
constructs the types.OpenAIError (same block referencing siliconflowResp, resp,
and types.WithOpenAIError) accordingly.
- Around line 77-79: The response handling currently calls
info.PriceData.AddOtherRatio("n", float64(len(imageResponse.Data))) which
duplicates image-count billing already added by ImageHelper; remove that call
(or the whole if block using imageResponse.Data) so AddOtherRatio("n") is not
applied here. Locate the usage of imageResponse and the call to
info.PriceData.AddOtherRatio in relay-siliconflow.go and delete or disable that
AddOtherRatio invocation to prevent double-charging.

---

Outside diff comments:
In `@docs/deployment.md`:
- Around line 449-459: Update the ListenHub status paragraph to remove
ambiguity: either add an explicit "historical snapshot as-of" sentence (e.g.,
"Status as of 2026-06-01: ...") before the sentence mentioning manual
disablement of channel `listenhub-images` (ID 12, `priority=120`, `type=59`) so
readers know the date of that observation, or replace the sentence with a
current-status sentence that clearly states whether `type=59` support has been
merged and the channel is now enabled; ensure you reference the channel name
`listenhub-images`, the `type=59` deployment dependency, and the Base URL
`https://api.marswave.ai/openapi` so the operational state is unambiguous.
🪄 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: a8b5c934-58fd-41e3-b421-8a3197aea987

📥 Commits

Reviewing files that changed from the base of the PR and between ae376ab and 8783484.

📒 Files selected for processing (8)
  • .gitignore
  • common/model.go
  • docs/api-usage.md
  • docs/deployment.md
  • relay/channel/siliconflow/adaptor.go
  • relay/channel/siliconflow/constant.go
  • relay/channel/siliconflow/dto.go
  • relay/channel/siliconflow/relay-siliconflow.go
✅ Files skipped from review due to trivial changes (1)
  • .gitignore

NumInferenceSteps *uint `json:"num_inference_steps,omitempty"`
GuidanceScale *float64 `json:"guidance_scale,omitempty"`
Cfg *float64 `json:"cfg,omitempty"`
OutputFormat string `json:"output_format,omitempty"`

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 | ⚡ Quick win

Use pointer type for optional output_format to preserve presence semantics.

OutputFormat is optional request input but is modeled as plain string, which loses absent-vs-explicit-empty distinction during remarshal.

Suggested fix
-	OutputFormat      string   `json:"output_format,omitempty"`
+	OutputFormat      *string  `json:"output_format,omitempty"`

As per coding guidelines, “Optional scalar fields in request structs parsed from client JSON and re-marshaled to upstream providers MUST use pointer types with omitempty tags.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
OutputFormat string `json:"output_format,omitempty"`
OutputFormat *string `json:"output_format,omitempty"`
🤖 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 `@relay/channel/siliconflow/dto.go` at line 29, The OutputFormat field is
currently a plain string which loses absent-vs-explicit-empty semantics when
re-marshaled; change OutputFormat to a pointer type (*string) with the existing
`json:"output_format,omitempty"` tag in the DTO struct that declares
OutputFormat, and update any code that reads or forwards this field to handle
nil (absent) vs non-nil (explicit) values appropriately so upstream requests
preserve presence semantics.

Source: Coding guidelines

Comment on lines +59 to +65
if len(siliconflowResp.Images) == 0 && siliconflowResp.Message != "" {
return nil, types.WithOpenAIError(types.OpenAIError{
Message: siliconflowResp.Message,
Type: "siliconflow_error",
Code: fmt.Sprintf("%v", siliconflowResp.Code),
}, resp.StatusCode)
}

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 | 🟠 Major | ⚡ Quick win

Don’t propagate provider body-errors with HTTP 200 status.

When images is empty and message is present, this is treated as an error, but using resp.StatusCode can keep it at 200. That returns an error payload with success status.

Suggested fix
 	if len(siliconflowResp.Images) == 0 && siliconflowResp.Message != "" {
+		status := resp.StatusCode
+		if status == http.StatusOK {
+			status = http.StatusBadGateway
+		}
 		return nil, types.WithOpenAIError(types.OpenAIError{
 			Message: siliconflowResp.Message,
 			Type:    "siliconflow_error",
 			Code:    fmt.Sprintf("%v", siliconflowResp.Code),
-		}, resp.StatusCode)
+		}, status)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(siliconflowResp.Images) == 0 && siliconflowResp.Message != "" {
return nil, types.WithOpenAIError(types.OpenAIError{
Message: siliconflowResp.Message,
Type: "siliconflow_error",
Code: fmt.Sprintf("%v", siliconflowResp.Code),
}, resp.StatusCode)
}
if len(siliconflowResp.Images) == 0 && siliconflowResp.Message != "" {
status := resp.StatusCode
if status == http.StatusOK {
status = http.StatusBadGateway
}
return nil, types.WithOpenAIError(types.OpenAIError{
Message: siliconflowResp.Message,
Type: "siliconflow_error",
Code: fmt.Sprintf("%v", siliconflowResp.Code),
}, status)
}
🤖 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 `@relay/channel/siliconflow/relay-siliconflow.go` around lines 59 - 65, The
code returns a provider error using resp.StatusCode which can be 200, causing an
error payload with a success HTTP status; update the error-return in the block
that checks siliconflowResp.Images and siliconflowResp.Message to pass a non-200
status to types.WithOpenAIError (for example http.StatusBadGateway or map
siliconflowResp.Code to a valid HTTP error code) instead of resp.StatusCode, and
ensure you fall back to a safe non-200 value if siliconflowResp.Code is
missing/invalid; adjust the call that constructs the types.OpenAIError (same
block referencing siliconflowResp, resp, and types.WithOpenAIError) accordingly.

Comment on lines +77 to +79
if len(imageResponse.Data) > 0 {
info.PriceData.AddOtherRatio("n", float64(len(imageResponse.Data)))
}

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 | 🟠 Major | ⚡ Quick win

Remove OtherRatio("n") here to avoid image-count double billing.

This path is standard image relay; adding n again at response handling risks charging on priced requests.

Suggested fix
-	if len(imageResponse.Data) > 0 {
-		info.PriceData.AddOtherRatio("n", float64(len(imageResponse.Data)))
-	}

Based on learnings, in this repository’s standard image relay path the image count is already embedded by ImageHelper, and adding OtherRatios["n"] causes double-billing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(imageResponse.Data) > 0 {
info.PriceData.AddOtherRatio("n", float64(len(imageResponse.Data)))
}
🤖 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 `@relay/channel/siliconflow/relay-siliconflow.go` around lines 77 - 79, The
response handling currently calls info.PriceData.AddOtherRatio("n",
float64(len(imageResponse.Data))) which duplicates image-count billing already
added by ImageHelper; remove that call (or the whole if block using
imageResponse.Data) so AddOtherRatio("n") is not applied here. Locate the usage
of imageResponse and the call to info.PriceData.AddOtherRatio in
relay-siliconflow.go and delete or disable that AddOtherRatio invocation to
prevent double-charging.

Source: Learnings

ViperCai added 2 commits June 6, 2026 16:58
…failover

# Conflicts:
#	.gitignore
#	middleware/distributor.go
#	web/classic/src/helpers/render.jsx
#	web/classic/vite.config.js

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
relay/channel/claude/relay-claude.go (1)

455-455: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace direct encoding/json calls with common/json.go wrappers.

Three instances violate the coding guideline requiring use of common.Marshal() and common.Unmarshal() for application-level JSON operations. As per coding guidelines, all JSON marshal/unmarshal operations in business code must use the wrapper functions from common/json.go.

🔧 Proposed fixes

Line 455: Replace json.Unmarshal with common.Unmarshal

 					inputObj := make(map[string]any)
-					if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inputObj); err != nil {
+					if err := common.Unmarshal([]byte(toolCall.Function.Arguments), &inputObj); err != nil {
 						common.SysLog("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments))

Line 586: Replace json.Marshal with common.Marshal

 		case "tool_use":
-			args, _ := json.Marshal(message.Input)
+			args, _ := common.Marshal(message.Input)
 			tools = append(tools, dto.ToolCallResponse{

Line 965: Replace json.Marshal with common.Marshal

 		openaiResponse := ResponseClaude2OpenAI(&claudeResponse)
 		openaiResponse.Usage = buildOpenAIStyleUsageFromClaudeUsage(claudeInfo.Usage)
-		responseData, err = json.Marshal(openaiResponse)
+		responseData, err = common.Marshal(openaiResponse)
 		if err != nil {

Also applies to: 586-586, 965-965

🤖 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 `@relay/channel/claude/relay-claude.go` at line 455, Replace direct calls to
encoding/json with the project wrappers: change
json.Unmarshal([]byte(toolCall.Function.Arguments), &inputObj) to use
common.Unmarshal and change the two json.Marshal usages at the other sites to
common.Marshal; ensure you import the common package if not present and keep the
same error handling/variable names (e.g., the Unmarshal call involving
toolCall.Function.Arguments and inputObj, and the two Marshal calls at the other
locations) so the functions still return the same bytes/error values.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@relay/channel/claude/relay-claude.go`:
- Line 455: Replace direct calls to encoding/json with the project wrappers:
change json.Unmarshal([]byte(toolCall.Function.Arguments), &inputObj) to use
common.Unmarshal and change the two json.Marshal usages at the other sites to
common.Marshal; ensure you import the common package if not present and keep the
same error handling/variable names (e.g., the Unmarshal call involving
toolCall.Function.Arguments and inputObj, and the two Marshal calls at the other
locations) so the functions still return the same bytes/error values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ed80abc4-5881-44f2-b9a3-16de3ec8070e

📥 Commits

Reviewing files that changed from the base of the PR and between 8783484 and 61e1ddb.

📒 Files selected for processing (8)
  • .gitignore
  • docs/deployment.md
  • middleware/distributor.go
  • relay/channel/claude/relay-claude.go
  • relay/helper/stream_scanner.go
  • setting/ratio_setting/model_ratio.go
  • web/classic/src/index.jsx
  • web/default/package.json
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • docs/deployment.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • web/default/package.json
  • web/classic/src/index.jsx
  • setting/ratio_setting/model_ratio.go
  • relay/helper/stream_scanner.go
  • middleware/distributor.go

ViperCai added 14 commits June 6, 2026 17:07
…failover

# Conflicts:
#	relay/helper/stream_scanner.go
- channels hit with upstream insufficient-credit errors enter a short
  cooldown (QUOTA_ERROR_COOLDOWN_SECONDS, default 600s) and are skipped
  by channel selection until expiry; all-cooled fallback keeps service up
- quota error keywords moved to operation_setting as the configurable
  UpstreamQuotaErrorKeywords option so new downstream platforms can be
  covered without a release
- video task relay now fails over on upstream quota errors (400/402/403
  with quota keywords) instead of returning the error to the caller
- enabling a channel clears its quota cooldown
ViperCai added 11 commits June 11, 2026 02:40
- new manxiaobaiProvider for api.manxiaobai.online: standard OpenAI
  Video protocol (POST /v1/videos JSON, string seconds, landscape and
  portrait size normalization), registered in all provider dispatch
  points via 'manxiaobai' hint or base URL match
- register grok-imagine-video / grok-imagine-video-1.5-preview video
  models and gpt-image-2-1k/2k/4k image tiers with per-call prices
  (upstream cost noted in comments)
upstream OpenAI-style error bodies set errorCode to the upstream code
(e.g. insufficient_user_quota) instead of bad_response_status_code, so
the strict code check prevented quota cooldown from arming; match
keywords against message plus error code and exclude skip-retry local
errors instead
filter cooled channels within each bucket instead of removing them from
the candidate list, so a cooldown armed mid-request no longer shifts
bucket indexes and skips the next-best channel on retry; an empty
bucket falls through to lower-priority buckets, and the all-cooled case
still releases the target bucket as a passive probe
upstream /v1/videos JSON parsing drops the model field (Field required
from its grok backend); the documented multipart form path works, so
convert JSON bodies to form data like other form-based providers
standard OpenAI Video upstreams like manxiaobai return no direct link
in poll responses; fall back to GET {base}/v1/videos/{id}/content with
the channel bearer key instead of failing with an empty video URL
when a task succeeds without an upstream direct link, ResultURL holds
this service's own proxy address; treat it like an empty url and fetch
from the upstream auth content endpoint instead of fetching ourselves
- api-usage: bump last-verified to 2026-06-11, update gpt-image-2 and
  gemini dash-name routing chains with manxiaobai fallback, add new
  model prices and upstream costs, record grok-imagine-video task
  verification and download stats
- deployment: mark manxiaobai section activated, complete local change
  inventory with cooldown/quota/provider files, note 1.5-preview
  reference-image pre-upload as pending work
- failover review: close P1-4 reference-image single point, count
  manxiaobai as the 8th video provider
逐个实测全部下游中转站的余额接口(详见 docs/channel-balance-query.md),
实现按 base_url/Other 路由的余额 provider 注册表(先于 channel.Type switch),
三档语义:
- balance(真实余额):lk888 /v1/skills/balance、listenhub /openapi/v1/user/subscription、siliconflow
- spend_only(仅累计消费):new-api 套壳站无限额度 key 只能查 /v1/dashboard/billing/usage;
  配 setting.balance_query.mode=newapi_console(账密)可登录拿真实钱包余额
- console_only:hongniao 等仅 web 控制台可查

新增聚合接口 GET /api/channel/balance_overview(AdminAuth):按 (base_url,key) 去重,
一次查出所有下游余额;?cached=true 只读存储、?include_disabled=true 含禁用渠道。
单渠道查询响应增加 kind/unit/used/remaining/provider;结果落渠道 OtherInfo。

前端:余额列按三档区分展示(剩余/⚠仅消费/仅控制台)+ 单位;
EditChannel 增加“下游余额查询”配置表单(模式/账密/累计充值)。

附带修复 relay_retry_test.go 既有的未使用变量编译错误。
@Calcium-Ion
Calcium-Ion force-pushed the main branch 2 times, most recently from 51fdfc5 to 2b6f1df Compare August 30, 2026 15:03
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