Skip to content

Add Sub2API async image channel - #4778

Closed
Fankouzu wants to merge 88 commits into
QuantumNous:mainfrom
Fankouzu:feature/sub2api-async-channel
Closed

Add Sub2API async image channel#4778
Fankouzu wants to merge 88 commits into
QuantumNous:mainfrom
Fankouzu:feature/sub2api-async-channel

Conversation

@Fankouzu

@Fankouzu Fankouzu commented May 11, 2026

Copy link
Copy Markdown

Summary

  • Add a Sub2API-async task channel that exposes OpenAI-compatible async image generation while running Sub2API's synchronous upstream image request in the background.
  • Register gpt-image-2 text-to-image and image-to-image models across backend metadata and both frontend channel selectors.
  • Return OpenAI-style image task results and enable generated image previews for base64 task outputs in both default and classic usage/task logs.

Test Plan

  • go test ./relay/channel/task/sub2api_async ./relay ./controller ./router ./model ./service ./common
  • cd web/default && bun run typecheck
  • cd web/classic && bun run build

Notes

  • classic build reports existing Browserslist/lottie/chunk-size warnings, but exits successfully.

Summary by CodeRabbit

  • New Features

    • Added support for four new providers (PingXingShiJie, KieAI, Fal.ai, Sub2API-async)
    • New async image generation endpoints and asset upload/retrieval flows
    • Improved task media previews: view generated images and videos with image preview modal
  • Documentation

    • Added Token Management API reference and provider-specific docs (PingXingShiJie, Fal.ai, etc.)
    • Added i18n entries for image preview and media results UI

Review Change Stack

Fankouzu and others added 30 commits April 9, 2026 11:14
Updated the AliVideoInput structure to include a new Media field for PixVerse media assets. Introduced the AliMedia type to handle different media types. Adjusted resolution options to support 360P and 540P. Implemented logic for PixVerse model handling in the billing calculation, ensuring accurate pricing based on model and audio settings. This enhancement improves the adaptability of the Ali adaptor for various video generation scenarios.
feat(AliAdaptor): enhance video input structure and add PixVerse support
…-latest

Chore/bump-lobehub-icons-visactor-latest
Content metadata items with type draft_task include a nested draft_task.id
field for Seedance draft-to-video. The previous ContentItem struct dropped
this field on JSON round-trip; add DraftTaskRef so upstream receives it.

Made-with: Cursor
…ontent

fix(doubao): forward Seedance draft_task content to Volcengine
Ark rejects draft_task mixed with other content types; the adaptor always
appended prompt as a text item. Omit that append when metadata.content
includes draft_task.

Made-with: Cursor
…-mix

fix(doubao): avoid mixing draft_task with prompt text for Seedance
…ubao)

- Add ChannelTypePingXingShiJie (58) with default Ark base URL
- New relay/channel/task/pingxingshijie package; register in GetTaskAdaptor
- Admin UI: CHANNEL_OPTIONS label 平行视界, icon same as Doubao video
- Channel test: unsupported like other async video task channels

Made-with: Cursor
…sync flows

- Route video/image/asset through PingXingShiJie base URL and unified envelope
- Add upstream_kind on tasks, FetchTask branching, and asset POST /v2/asset/status polling
- Extend /v1 routes for async images and assets; distributor and RelayTask fetch handling
- Add docs: API reference, compatibility, downstream curl pointer; ignore local vendor API md

Made-with: Cursor
…:task_id

Path2RelayMode maps any /v1/images/generations prefix to RelayModeImagesGenerations,
which is not registered in fetchRespBuilders, causing a nil respBuilder panic on
RelayTaskFetch. Prefer c.Get("relay_mode") when set so VideoFetchByID applies.

Also guard against missing fetch builder and add optional debug NDJSON logs to
.cursor/debug-1b0c95.log for verification.

Made-with: Cursor
…nd task id parsing

- Forward minimal upstream body for draft_task (no default generate_audio, no seconds->duration)
- Normalize 720p/1080p for seedance-1-5-pro; clear metadata draft flag when present
- Deep-extract video create task id from nested envelope/data-string responses
- Document draft_task downstream format; add contract tests

Made-with: Cursor
fix(pingxingshijie): Seedance 1.5 Pro draft_task upscale and video task id parsing
…nvelope, enable channel test

Made-with: Cursor
…completions and enhance envelope handling

- Changed upstream URL for text chat from /v1/chat/completions to /v2/chat/completions to align with API family.
- Improved response handling by normalizing PingXing envelope for error codes and messages.
- Updated documentation to reflect changes in API behavior and response structure.
…el-test

fix(pingxingshijie): channel 58 text relay uses /v2/chat/completions
Sync upstream changes into main after resolving conflicts and preserving local customizations.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Bring GitHub main's Pingxingshijie channel updates onto the upstream-synced main before pushing.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Fankouzu added 24 commits May 5, 2026 20:06
Clone KIE channel pattern for fal.ai integration:
- constants.go: model list (openai/gpt-image-2 t2i/edit),
  quality ratio weights (low/medium/high)
- adaptor.go: async task adaptor with dynamic URL
  {baseURL}/{model_name}, EstimateBilling by quality,
  result polling via {model}/requests/{id}
- constant/channel.go: ChannelTypeFalAI=60
- relay/relay_adaptor.go: GetTaskAdaptor wiring
- web: channel icon from lobehub
…ssic themes

- Classic theme: add Fal.ai entry to channel.constants.js and getChannelIcon() in render.jsx
- Default theme: update label from FalAI to Fal.ai in constants.ts
- Icon: https://icons.lobehub.com/components/fal
… channel icon

- Classic theme: import Fal from @lobehub/icons/es/Fal (not in barrel export)
- Use <Fal.Color size={iconSize} /> instead of <img> tag
- Display label: Fal.ai
Replace hardcoded quality ratios (1/9/35 for t2i, 1/4/15 for i2i)
with 18-entry size×quality matrices based on fal.ai actual pricing.

- constants.go: pricingMatrix (t2i) + pricingMatrixEdit (i2i)
- adaptor.go: EstimateBilling uses size+quality to look up multiplier
- channel.go: DefaultBaseURL → queue.fal.run

All multipliers relative to base: 1024×1024 low t2i = $0.006 = 1×
ModelPrice of 0.042 (≈USD base × 7) gives 0% deviation across all 6 sizes × 3 qualities.
Includes API reference, parameter defaults, billing explanation,
and 36-entry pricing table (18 t2i + 18 i2i) in RMB.
feat(channel): fal.ai async task channel
Extend falResultResponse with Detail, Error, Logs fields.
ParseTaskResult default branch checks error fields before images.
FetchTask no longer swallows HTTP error responses.
Guard against infinite polling when upstream returns errors
without a "status" field (content violations, etc.)
fal.ai returns "detail" as an array (FastAPI validation errors)
not a string. Change Detail from string to json.RawMessage
and add extractDetailMessage() to handle both formats.
fix(fal): detect error responses and stop polling on failure
The OpenAI image endpoint remains synchronous, so this adds a separate task-only channel for Sub2API-compatible async image jobs. The new adaptor follows the existing Kie-style createTask and recordInfo flow and is exposed only through the existing async image route.

Constraint: Cloudflare returns 524 when synchronous GPT image calls exceed two minutes

Constraint: Keep /v1/images/generations behavior on OpenAI channels unchanged

Rejected: Auto-convert the synchronous image endpoint to async | would change client response semantics

Confidence: high

Scope-risk: moderate

Directive: Do not route this channel through synchronous image helpers without rechecking client compatibility

Tested: go test ./relay/channel/task/sub2api_async ./relay ./common ./service

Tested: cd web/default && bun run i18n:sync && bun run build && bun run typecheck

Not-tested: Live Sub2API upstream end-to-end credentials
The async Sub2API channel was already added to backend and the default frontend, but the classic frontend keeps a separate channel option list and icon renderer. Add the same channel type there so deployments using either frontend can configure the async image channel consistently.\n\nConstraint: Classic and default frontends maintain independent channel metadata.\nRejected: Rely on backend-only channel registration | the classic UI would still omit the option from its picker.\nConfidence: high\nScope-risk: narrow\nDirective: Keep channel type 61 visible in both frontend variants when changing provider lists.\nTested: cd web/default && bun run build\nTested: cd web/classic && bun run build\nTested: git diff --check\nNot-tested: Browser manual click-through of the classic channel form.
The frontends use local channel labels, but channel model helpers depend on backend model metadata. Sub2API-async is task-only, so it needs the same explicit model mapping path as Kie instead of inheriting the generic OpenAI model list.\n\nConstraint: Sub2API-async only supports the two GPT image async task models.\nRejected: Treat type 61 as generic OpenAI for model metadata | it hides the relevant task models behind an unrelated OpenAI model list.\nConfidence: high\nScope-risk: narrow\nDirective: Task-only channels must be registered in channelId2Models explicitly, alongside their task adaptor model list.\nTested: go test ./controller ./relay/channel/task/sub2api_async ./relay ./common\nTested: cd web/default && bun run build\nTested: cd web/classic && bun run build\nTested: git diff --check\nNot-tested: Authenticated browser click-through after login.
The classic frontend imports OpenAI as a component, not as a namespace with a Color export. Rendering OpenAI.Color for the Sub2API-async channel produced React error QuantumNous#130 when the add-channel modal listed channel options.\n\nConstraint: @lobehub/icons exports differ between providers in the classic bundle.\nRejected: Keep using OpenAI.Color | it resolves to undefined in the classic frontend import shape.\nConfidence: high\nScope-risk: narrow\nDirective: Use the same OpenAI component form as the existing OpenAI/Azure/Codex channel icons unless the import is changed to a namespace-compatible export.\nTested: cd web/classic && bun run build\nTested: git diff --check\nNot-tested: Authenticated browser modal click-through after login.
The OpenAI-compatible async image endpoint belongs with the /v1 image relay routes. Keeping it only in the video router made the route easy to miss and caused 404s when that router registration was not the path being exercised.\n\nConstraint: POST /v1/images/generations/async must be available as the public async image submission endpoint.\nRejected: Leave the route under video-router only | the endpoint is image-specific and previous debugging showed it is too easy to miss there.\nConfidence: high\nScope-risk: narrow\nDirective: Keep OpenAI-compatible image routes together in relay-router unless a future route registry centralizes all /v1 endpoints.\nTested: go test ./router ./controller ./relay/channel/task/sub2api_async ./relay\nTested: git diff --check\nNot-tested: Authenticated upstream Sub2API task submission with a real token/key.
The Sub2API-async channel must call the existing async image endpoint instead of the Kie job API. The adaptor now submits OpenAI-style async image payloads to /v1/images/generations/async, polls /v1/images/generations/{task_id}, and parses OpenAI-style task responses while keeping legacy Kie-shaped parsing as a fallback.

Constraint: Sub2API-async is async-only and must not fall back to the synchronous image endpoint.

Rejected: Keep using /api/v1/jobs/createTask | that path caused upstream Railway 404 responses for the configured Sub2API host.

Confidence: high

Scope-risk: narrow

Directive: Do not reintroduce Kie createTask paths into Sub2API-async unless the channel contract changes.

Tested: go test ./controller ./relay/channel/task/sub2api_async ./relay ./router ./common; git diff --check

Not-tested: Successful live async submission, because the currently configured upstream host returns 404 for /v1/images/generations/async.
Sub2API only exposes the OpenAI-compatible synchronous image endpoint, while clients need an async new-api endpoint to avoid Cloudflare 524 during long image generation. The task submit path now returns a local task immediately, inserts it, and then runs the synchronous upstream /v1/images/generations call in the background. Task fetch reads the local task state and result.

Constraint: Upstream Sub2API has no /v1/images/generations/async endpoint.

Constraint: Client-facing Sub2API-async must remain async-only via POST /v1/images/generations/async.

Rejected: Call upstream /v1/images/generations/async | upstream returns 404 because that route does not exist.

Rejected: Fall back to synchronous client-facing relay | would keep the Cloudflare 524 failure mode.

Confidence: high

Scope-risk: moderate

Directive: Keep Sub2API-async as an async bridge over the synchronous upstream image API unless the upstream gains a real task API.

Tested: go test ./controller ./relay/channel/task/sub2api_async ./relay ./router ./model ./service ./common; git diff --check; local POST returned task in 6ms and background task reached IN_PROGRESS with upstream payload stored.

Not-tested: Successful live image completion, because the configured upstream account remained pending/limited during smoke tests.
Sub2API-async can receive synchronous OpenAI image responses with b64_json. Returning that as a data URL inside the task polling JSON made completed task responses multi-megabyte and caused clients to appear empty or stalled. Store and expose a local content proxy URL instead, and let the proxy stream stored OpenAI image payloads as image content.

Constraint: Upstream Sub2API remains synchronous and may return b64_json image data.

Rejected: Return b64_json directly from the task polling endpoint | keeps polling responses large and does not match the existing local content proxy pattern.

Confidence: high

Scope-risk: moderate

Directive: Keep task polling responses small; route stored binary/base64 payloads through content endpoints.

Tested: go test ./relay ./controller ./relay/channel/task/sub2api_async ./router ./model ./service ./common

Tested: curl GET /v1/images/generations/task_OQ19Pkbd2bTUEwxtlvpF6LjAiEMqlH5y returned 352 bytes with local content URL

Tested: curl GET /v1/videos/task_OQ19Pkbd2bTUEwxtlvpF6LjAiEMqlH5y/content returned image/png content
Sub2API-async task polling should mirror the upstream image result contract for base64 image outputs. The previous local content proxy approach produced a /v1/videos/... URL and did not satisfy clients that expect the upstream base64 payload from image task polling. Convert stored OpenAI image b64_json, and stored image data URLs from prior rows, into a b64_json field in the task response.

Constraint: Upstream Sub2API remains synchronous and may return either b64_json or a data:image URL inside the OpenAI images response.

Rejected: Keep /v1/videos/{task_id}/content proxy URL | wrong endpoint family for image generation task polling and not the requested response contract.

Confidence: high

Scope-risk: moderate

Directive: Do not map Sub2API image task base64 results to video content URLs; return b64_json from /v1/images/generations/{task_id}.

Tested: go test ./relay/channel/task/sub2api_async ./relay ./controller ./router ./model ./service ./common

Tested: curl GET /v1/images/generations/task_OQ19Pkbd2bTUEwxtlvpF6LjAiEMqlH5y returned b64_json and no url
Clients polling /v1/images/generations/{task_id} expect image outputs under the OpenAI images data array. Returning a top-level b64_json made some clients treat the completed task as having no image data even though the HTTP response body was present. Keep the task metadata, but place successful image outputs under data[0].b64_json or data[0].url.

Constraint: Completed Sub2API-async image responses can be large when the upstream returns base64.

Rejected: Top-level b64_json | not compatible with OpenAI image response parsers that look under data[].

Confidence: high

Scope-risk: narrow

Directive: Preserve data[] output shape for Sub2API image task polling results.

Tested: go test ./relay/channel/task/sub2api_async ./relay ./controller ./router ./model ./service ./common

Tested: curl GET /v1/images/generations/task_OQ19Pkbd2bTUEwxtlvpF6LjAiEMqlH5y returned data[0].b64_json and no top-level url
Sub2API-async image tasks store generated images as OpenAI-style b64_json or data:image URLs in task data. The task log preview extractors only recognized HTTP image URLs, so completed gpt-image-2 tasks had no generated result preview in the details column. Teach both default and classic frontends to derive image preview data URLs from b64_json and existing data:image payloads.

Constraint: gpt-image-2 task results can be base64-only; no stable external image URL exists.

Rejected: Add another backend video/content proxy URL | it reintroduced the wrong /v1/videos endpoint for image task results.

Confidence: high

Scope-risk: narrow

Directive: Keep task log media extraction compatible with OpenAI images data[].b64_json results.

Tested: bun run typecheck (web/default)

Tested: bun run build (web/default)

Tested: bun run build (web/classic; existing Browserslist/lottie/chunk warnings only)
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2ea5ffb-f0fa-4759-a460-dc20854f4b03

📥 Commits

Reviewing files that changed from the base of the PR and between 0510b7d and 041f99f.

📒 Files selected for processing (23)
  • controller/channel-test.go
  • controller/relay.go
  • docs/fal-ai-channel.md
  • dto/openai_image.go
  • middleware/distributor.go
  • relay/channel/openai/adaptor.go
  • relay/channel/openai/relay-openai.go
  • relay/channel/task/ali/adaptor.go
  • relay/channel/task/doubao/adaptor.go
  • relay/channel/task/fal/adaptor.go
  • relay/channel/task/pingxingshijie/adaptor.go
  • relay/channel/task/sub2api_async/adaptor.go
  • relay/channel/task/sub2api_async/adaptor_test.go
  • service/file_service.go
  • web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx
  • web/classic/src/helpers/render.jsx
  • web/classic/src/i18n/locales/en.json
  • web/classic/src/i18n/locales/fr.json
  • web/classic/src/i18n/locales/ja.json
  • web/classic/src/i18n/locales/ru.json
  • web/classic/src/i18n/locales/vi.json
  • web/classic/src/i18n/locales/zh-CN.json
  • web/classic/src/i18n/locales/zh-TW.json
✅ Files skipped from review due to trivial changes (7)
  • web/classic/src/i18n/locales/ru.json
  • web/classic/src/i18n/locales/fr.json
  • web/classic/src/i18n/locales/zh-TW.json
  • web/classic/src/i18n/locales/en.json
  • docs/fal-ai-channel.md
  • web/classic/src/i18n/locales/vi.json
  • web/classic/src/i18n/locales/zh-CN.json
🚧 Files skipped from review as they are similar to previous changes (14)
  • controller/relay.go
  • relay/channel/openai/adaptor.go
  • web/classic/src/i18n/locales/ja.json
  • relay/channel/task/doubao/adaptor.go
  • service/file_service.go
  • web/classic/src/helpers/render.jsx
  • middleware/distributor.go
  • controller/channel-test.go
  • relay/channel/task/sub2api_async/adaptor_test.go
  • relay/channel/task/sub2api_async/adaptor.go
  • relay/channel/task/ali/adaptor.go
  • web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx
  • relay/channel/task/fal/adaptor.go
  • relay/channel/task/pingxingshijie/adaptor.go

Walkthrough

Adds PingXingShiJie, KieAI, FalAI, and Sub2API-async channels with task adaptors, polling/upstream-kind plumbing, new async image/asset routes, Volcengine v2 routing and envelope unwrapping, DTO/model updates, UI image/video preview support, docs, and tests.

Changes

Async Media Task Channels and UI

Layer / File(s) Summary
Channel IDs & API mapping
constant/channel.go, common/api_type.go
Adds channel types 58–61, base URLs, display names, and maps new channels to API types.
Model registry & adaptors registration
controller/model.go, relay/relay_adaptor.go
Registers task channel model lists and returns new TaskAdaptor implementations for new channels.
PingXingShiJie adaptor & helpers
relay/channel/task/pingxingshijie/...
Full adaptor: submit/fetch/parse, envelope unwrapping, upstream-kind, draft-upscale, asset flow, ID extraction, conversion to OpenAI-compatible payloads, and tests.
Kie / Fal / Sub2API adaptors
relay/channel/task/kie/..., relay/channel/task/fal/..., relay/channel/task/sub2api_async/...
Adapters implement create/fetch/parse, billing estimation, conversions, background-worker async flow (sub2api_async), and tests.
Relay/Controller/Models/DTOs
relay/*, controller/*, model/*, dto/*, middleware/distributor.go
Adds UpstreamKind, TaskDto.UpstreamKind, TaskPrivateData.UpstreamKind/ResultURL handling, TaskSubmitReq JSON parsing, distributor routing for images/assets, quota ratio helpers, and GetTaskByID.
Volcengine adaptor
relay/channel/volcengine/adaptor.go
PingXingShiJie v2 endpoint selection and sync envelope normalization/unwrapping on non-stream responses.
Routers
router/relay-router.go, router/video-router.go
Adds POST /v1/images/generations/async, GET /v1/images/generations/:task_id, POST /v1/assets/upload, GET /v1/assets/:task_id.
Web Classic UI
web/classic/...
Image preview modal, ContentModal image support, hook state, channel icons/options, and i18n strings.
Web Default UI
web/default/...
extractTaskMediaResults, TaskMediaResultsDialog, TaskLog type updates, lobe-icon external URL handling, and tests.
Docs & Tests
docs/*, **/*_test.go, web/default/tests/*
Adds PingXingShiJie/fal docs, token management doc, many unit tests covering adaptors, envelopes, parsing, routing, and UI tests.
Misc
dto/openai_request.go, dto/openai_image.go, service/file_service.go
Improved MIME detection for files, ImageRequest.Stream → *bool, base64 load MIME sniffing.

Estimated code review effort
🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • QuantumNous/new-api#1326: Also modifies ChannelType2APIType mappings; overlaps with new channel API type routing.
  • QuantumNous/new-api#2985: Touches async task adaptor, polling, and task model plumbing similar to this PR’s task flow changes.
  • QuantumNous/new-api#1274: Adds new channels and adaptor interfaces; related to adding Fal/Kie/PingXingShiJie/Sub2API-async.

Suggested reviewers

  • seeefs001
  • Calcium-Ion
  • creamlike1024

Poem

I thump my paw—new channels hop in line,
Seeds of dreams and frames refine.
Images bloom, videos spin,
Tasks await, we fetch and win.
UI pops—preview divine! 🥕✨

✨ 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: 19

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (9)
docs/token-management-api-reference.md-76-78 (1)

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

Add languages to unlabeled fenced blocks to satisfy MD040

At Line 76, Line 136, Line 197, Line 256, Line 294, Line 390, Line 520, Line 560, Line 609, Line 671, and Line 743, fenced code blocks are missing a language tag. This triggers markdownlint and is easy to fix.

Proposed patch pattern
-```
+```http
 GET /api/token/

- +http
GET /api/token/search


-```
+```http
GET /api/token/:id

... apply the same ```http labeling to the other endpoint-only fences.

</details>


Also applies to: 136-138, 197-199, 256-258, 294-296, 390-392, 520-522, 560-562, 609-611, 671-673, 743-745

<details>
<summary>🤖 Prompt for AI Agents</summary>

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/token-management-api-reference.md around lines 76 - 78, Several fenced
code blocks that contain HTTP endpoint lines (e.g., the blocks with "GET
/api/token/", "GET /api/token/search", "GET /api/token/:id" and other
endpoint-only fences) are missing a language tag and trigger MD040; update each
unlabeled triple-backtick fence around these endpoint examples to use the http
language (i.e., change tohttp) so markdownlint is satisfied, and apply
the same change to the other endpoint-only fences referenced in the comment (the
blocks at the other listed locations).


</details>

</blockquote></details>
<details>
<summary>web/default/src/i18n/locales/ru.json-3912-3912 (1)</summary><blockquote>

`3912-3912`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Translate `Zoom` for Russian locale.**

Line 3912 is still English (`"Zoom"`), which breaks locale consistency for RU users. Consider using `"Масштаб"` (or your preferred RU product term).

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/default/src/i18n/locales/ru.json` at line 3912, The Russian locale entry
for the key "Zoom" is still in English; update the value for the JSON key "Zoom"
in ru.json to the Russian translation (e.g., "Масштаб" or your chosen RU term)
so the locale remains consistent, ensuring you only change the string value for
the existing "Zoom" key and keep the surrounding JSON structure intact.
```

</details>

</blockquote></details>
<details>
<summary>web/default/src/i18n/locales/zh.json-3790-3790 (1)</summary><blockquote>

`3790-3790`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Translation inconsistency: missing possessive marker "的".**

Line 3790 translates "View generated results" as "查看生成结果", but line 3789 uses "查看生成的媒体结果" for "View generated media results" (with the possessive marker "的"). For consistency, this should be "查看生成的结果" to match the pattern.

As per coding guidelines: maintain naming consistency across translation keys.




<details>
<summary>Suggested fix</summary>

```diff
-    "View generated results": "查看生成结果",
+    "View generated results": "查看生成的结果",
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/default/src/i18n/locales/zh.json` at line 3790, The translation for the
key "View generated results" is inconsistent with the adjacent key "View
generated media results" — change the value from "查看生成结果" to "查看生成的结果" so it
includes the possessive marker "的" and matches the pattern used by "查看生成的媒体结果".
```

</details>

</blockquote></details>
<details>
<summary>web/default/src/i18n/locales/zh.json-1643-1643 (1)</summary><blockquote>

`1643-1643`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Translation inconsistency: missing possessive marker "的".**

Line 1643 translates "Generated Results" as "生成结果", but the existing translation at line 1644 uses "生成的图像" for "Generated image" (with the possessive marker "的"). For consistency with the established pattern and proper Chinese grammar, this should be "生成的结果".

As per coding guidelines: maintain naming consistency across translation keys.




<details>
<summary>Suggested fix</summary>

```diff
-    "Generated Results": "生成结果",
+    "Generated Results": "生成的结果",
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/default/src/i18n/locales/zh.json` at line 1643, Update the translation
value for the JSON key "Generated Results" from "生成结果" to "生成的结果" to match the
possessive pattern used elsewhere (e.g., "生成的图像") and maintain naming/grammar
consistency; locate the "Generated Results" entry in zh.json and replace its
string value accordingly.
```

</details>

</blockquote></details>
<details>
<summary>model/task_result_url.go-20-35 (1)</summary><blockquote>

`20-35`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Fragile URL classification heuristic may cause false positives.**

The `looksLikeImageAssetURL` function uses string-matching heuristics that could match unintended URLs:

**Line 31 concern:**
```go
strings.Contains(lower, "image") && strings.Contains(lower, "generation")
```

This would incorrectly match:
- `https://example.com/image/generation/report.pdf`
- `https://generation.image.com/data.json`
- `https://cdn.com/images/generation-stats/file.txt`

The intent appears to be catching Seedream-style signed URLs without extensions, but the current logic is too broad.




<details>
<summary>🛡️ Suggested refinements</summary>

Consider tightening the heuristic:

```diff
 func looksLikeImageAssetURL(u string) bool {
 	lower := strings.ToLower(strings.TrimSpace(u))
 	if !strings.HasPrefix(lower, "http") {
 		return false
 	}
 	if strings.Contains(lower, ".jpeg") || strings.Contains(lower, ".jpg") ||
 		strings.Contains(lower, ".png") || strings.Contains(lower, ".webp") ||
 		strings.Contains(lower, ".gif") {
 		return true
 	}
-	// Seedream and similar APIs may omit extension in signed URLs
-	if strings.Contains(lower, "seedream") || strings.Contains(lower, "image") && strings.Contains(lower, "generation") {
+	// Provider-specific patterns for URLs without extensions
+	if strings.Contains(lower, "seedream") {
+		return true
+	}
+	// More specific check for image generation endpoints
+	if strings.Contains(lower, "/image") && (strings.Contains(lower, "/generate") || strings.Contains(lower, "/generation/")) {
 		return true
 	}
 	return false
 }
```

Alternatively, maintain a list of known provider URL patterns or use the `Content-Type` header if available.

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/task_result_url.go` around lines 20 - 35, The heuristic in
looksLikeImageAssetURL is too broad and the clause strings.Contains(lower,
"image") && strings.Contains(lower, "generation") yields false positives; narrow
it by requiring more specific patterns or boundaries (e.g., require known
provider tokens like "seedream" or a combined path/query pattern such as an
"image" path segment adjacent to "generation" or a hyphenated token like
"image-generation"), or better yet maintain a whitelist of trusted provider URL
substrings and/or fall back to checking the response Content-Type when
available; update the conditional in looksLikeImageAssetURL to use these
stricter checks (or a regex that enforces path/word boundaries) instead of the
current loose contains-based test.
```

</details>

</blockquote></details>
<details>
<summary>web/classic/src/helpers/render.jsx-65-65 (1)</summary><blockquote>

`65-65`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Address inconsistent import style for the Fal icon.**

The `Fal` icon is imported differently from all other icons in this file: it uses a default import from `@lobehub/icons/es/Fal` (line 65), while icons on lines 32-64 use named imports from `@lobehub/icons` directly. The code works as-is—line 417 correctly uses `Fal.Color`—but this inconsistency warrants clarification.

Determine whether:
1. The `/es/Fal` path is required due to the library's export structure for Fal (in which case, this is intentional and should be documented)
2. Fal should be imported as a named export like the others: `import { Fal } from '@lobehub/icons';`

If option 2 is valid, standardize the import to match the pattern used for other icons.

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/classic/src/helpers/render.jsx` at line 65, Import of the Fal icon is
inconsistent: currently it's default-imported from '@lobehub/icons/es/Fal' while
other icons are named imports from '@lobehub/icons'; verify whether Fal is
exported as a named export from '@lobehub/icons' and if so replace the default
'/es/Fal' import with a named import (import { Fal } from '@lobehub/icons') to
match the pattern used by other icons (references: the Fal identifier and its
usage Fal.Color), otherwise add a brief inline comment near the Fal import
explaining that the '/es/Fal' path is required due to the library's export
structure so future readers understand the exception.
```

</details>

</blockquote></details>
<details>
<summary>web/default/tests/channel-utils.test.ts-8-27 (1)</summary><blockquote>

`8-27`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Add coverage for channel type 61 mapping**

This suite validates 58–60 but skips 61, which was also added in the same mapping change. Add one assertion for `getChannelTypeLabel(61)` and `getChannelTypeIcon(61)` to prevent drift.

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/default/tests/channel-utils.test.ts` around lines 8 - 27, Add a test case
covering channel type 61 by asserting getChannelTypeLabel(61) and
getChannelTypeIcon(61) return the expected label and URL defined in your channel
mapping; locate the mapping used by getChannelTypeLabel/getChannelTypeIcon and
add two expects similar to the existing tests for 58–60 that check the label and
favicon/logo URL for type 61.
```

</details>

</blockquote></details>
<details>
<summary>web/default/src/features/usage-logs/components/dialogs/task-media-results-dialog.tsx-23-27 (1)</summary><blockquote>

`23-27`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Handle clipboard rejections.**

`navigator.clipboard.writeText()` fails on denied permissions and non-secure contexts. Right now the action silently does nothing, which makes the new “Copy Link” button look broken.
 

<details>
<summary>Suggested fix</summary>

```diff
-function copyUrl(url: string, successMessage: string): void {
-  void navigator.clipboard.writeText(url).then(() => {
-    toast.success(successMessage)
-  })
+function copyUrl(
+  url: string,
+  successMessage: string,
+  errorMessage: string
+): void {
+  void navigator.clipboard
+    .writeText(url)
+    .then(() => {
+      toast.success(successMessage)
+    })
+    .catch(() => {
+      toast.error(errorMessage)
+    })
 }
```

```diff
-              onClick={() => copyUrl(result.url, t('Copied'))}
+              onClick={() =>
+                copyUrl(result.url, t('Copied'), t('Failed to copy'))
+              }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In
`@web/default/src/features/usage-logs/components/dialogs/task-media-results-dialog.tsx`
around lines 23 - 27, The copyUrl function currently ignores failures from
navigator.clipboard.writeText causing silent failures; modify copyUrl(url:
string, successMessage: string) to handle rejected promises by adding a .catch
handler that logs the error (console.error) and shows a user-facing failure
toast (e.g., toast.error("Could not copy link")) and, optionally, implement a
secure-fallback: if navigator.clipboard is unavailable or writeText rejects,
attempt a DOM fallback (create a temporary textarea, select,
document.execCommand('copy')) and report success/failure via the same toast
calls; keep the function name copyUrl and ensure both success and failure paths
call toast so the UI reflects the result.
```

</details>

</blockquote></details>
<details>
<summary>relay/channel/task/pingxingshijie/unwrap_task_data_test.go-22-39 (1)</summary><blockquote>

`22-39`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_

**Test asserts nothing about the envelope shape it claims to verify.**

`TestConvertToOpenAIVideo_EnvelopeShapeWithoutInnerTask` only checks that the call doesn't error and returns parseable JSON — it never reads any field from `out`. The test name promises shape verification (status, progress, id, etc.). Add assertions for the key fields you expect the OpenAI-compatible response to contain so this guards against silent regressions.

<details>
<summary>✅ Minimal assertions to add</summary>

```diff
     var out map[string]any
     if err := common.Unmarshal(b, &out); err != nil {
         t.Fatal(err)
     }
+    if out["id"] != "task_test" {
+        t.Fatalf("id: got %#v", out["id"])
+    }
+    if out["status"] != model.TaskStatus(model.TaskStatusInProgress).ToVideoStatus() {
+        t.Fatalf("status: got %#v", out["status"])
+    }
+    if out["progress"] != "30%" {
+        t.Fatalf("progress: got %#v", out["progress"])
+    }
 }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/pingxingshijie/unwrap_task_data_test.go` around lines 22 -
39, The test TestConvertToOpenAIVideo_EnvelopeShapeWithoutInnerTask currently
only checks for no error and valid JSON; update it to assert expected envelope
fields returned by a.ConvertToOpenAIVideo: after unmarshalling into out
(map[string]any) assert the task id (e.g., out["task_id"] or out["id"]) equals
"task_test", assert status equals model.TaskStatusInProgress (or the string
value produced), assert progress equals "30%", assert the Data payload or its
parsed inner fields (e.g., data.code == 0 and data.msg == "ok") are present, and
assert properties.origin_model_name exists and equals "m"; use the existing
TestConvertToOpenAIVideo_EnvelopeShapeWithoutInnerTask,
TaskAdaptor.ConvertToOpenAIVideo, and the out map to locate where to add these
checks.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (19)</summary><blockquote>

<details>
<summary>web/classic/src/i18n/locales/en.json (1)</summary><blockquote>

`2357-2358`: _💤 Low value_

**Consider aligning image error message wording with the existing video error pattern.**

The new image error message uses "could not be loaded" and provides inline explanation, while the similar video error at line 2563 uses "cannot be played" and ends with a colon (suggesting a list follows). For better consistency in user-facing error messages:

**Option 1 (Match video pattern):**
```diff
-"图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "The image could not be loaded in this browser (often due to CORS or hotlink protection).",
+"图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "The image cannot be loaded in this browser, possibly because:",
```

**Option 2 (Keep as-is but improve consistency):**
```diff
-"图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "The image could not be loaded in this browser (often due to CORS or hotlink protection).",
+"图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "The image cannot be loaded in this browser (likely due to CORS or hotlink protection).",
```

Line 2357 ("Click to preview image") is consistent with line 2359's music preview pattern ✓

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/classic/src/i18n/locales/en.json` around lines 2357 - 2358, Update the
image error translation string key "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。" to mirror the
video error pattern by changing the value to use "cannot" and a trailing colon
(e.g., "The image cannot be loaded in this browser:"), so it matches the video
message style used elsewhere; locate the same key in en.json and replace the
current sentence "The image could not be loaded in this browser (often due to
CORS or hotlink protection)." with the new consistent phrasing.
```

</details>

</blockquote></details>
<details>
<summary>docs/fal-ai-channel.md (1)</summary><blockquote>

`192-217`: _⚡ Quick win_

**Consider integrating the size matrix with price tables.**

The "画幅矩阵" (aspect ratio matrix) section lists all supported size combinations but is disconnected from the pricing information above. This creates a documentation maintenance issue where size changes must be updated in multiple places.

**Recommendation:** 
- Merge this matrix into the extended price tables (lines 152-189) to create a single source of truth
- Or reference this matrix from the pricing section to clarify which combinations are supported

This would improve documentation consistency and reduce duplication.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/fal-ai-channel.md` around lines 192 - 217, The "画幅矩阵" section currently
duplicates supported sizes and should be consolidated with the pricing info:
either merge the matrix rows into the existing extended price tables so each
price row includes the corresponding aspect-ratio/clarity/size entries, or
replace the duplicated matrix with a clear cross-reference from the pricing
section to the "画幅矩阵" heading; update the heading "画幅矩阵" and the extended price
tables so there is one canonical table (or a single link) describing supported
aspect ratios, clarity (1K/2K/4K) and pixel dimensions to avoid divergence.
```

</details>

</blockquote></details>
<details>
<summary>web/default/src/i18n/locales/fr.json (1)</summary><blockquote>

`641-641`: _🏗️ Heavy lift_

**Use hierarchical i18n keys for these new entries.**

These newly added keys are raw English phrases. Please migrate them to hierarchical semantic keys and update call sites/locales accordingly to stay aligned with the i18n key convention.

<details>
<summary>💡 Example key shape for this block</summary>

```diff
-    "Click to preview image": "Cliquer pour prévisualiser l’image",
+    "usageLogs.media.clickToPreviewImage": "Cliquer pour prévisualiser l’image",

-    "Generated Results": "Résultats générés",
+    "usageLogs.media.generatedResults": "Résultats générés",

-    "Generated video": "Vidéo générée",
+    "usageLogs.media.generatedVideo": "Vidéo générée",

-    "No generated media results": "Aucun média généré",
+    "usageLogs.media.noGeneratedResults": "Aucun média généré",

-    "View generated media results": "Voir les médias générés",
+    "usageLogs.media.viewGeneratedMediaResults": "Voir les médias générés",

-    "View generated results": "Voir les résultats générés",
+    "usageLogs.media.viewGeneratedResults": "Voir les résultats générés",

-    "Sub2API-async": "Sub2API-async"
+    "channels.sub2apiAsync.label": "Sub2API-async"
```
</details>

 

As per coding guidelines `web/default/src/i18n/**/*.{ts,tsx,json}`: Use hierarchical and semantically clear translation key names such as `dashboard.overview.title` and maintain naming consistency.


Also applies to: 1643-1645, 2259-2259, 3789-3790, 3913-3913

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/default/src/i18n/locales/fr.json` at line 641, Replace the raw English
JSON key "Click to preview image" with a hierarchical semantic key (for example
image.preview.tooltip or media.image.preview.click) in this locale file and in
all other locale files, then update all call sites that reference the old string
key to use the new hierarchical key (search for the literal "Click to preview
image" across the codebase). Ensure the French translation value remains the
same under the new key and apply the same pattern for the other raw-phrase
entries noted (around the other ranges) so keys follow the project's naming
convention (e.g., media.*, image.*, preview.*).
```

</details>

</blockquote></details>
<details>
<summary>model/task_result_url.go (1)</summary><blockquote>

`37-56`: _⚡ Quick win_

**Hardcoded "url" field name limits flexibility.**

The `walkFirstImageLikeURL` function only extracts URLs from fields named exactly `"url"` (line 40). Many providers use different field names:
- `imageUrl`, `image_url`
- `downloadUrl`, `download_url`
- `assetUrl`, `asset_url`
- `resultUrl`, `result_url`

This limitation could cause valid image URLs to be missed during extraction.




<details>
<summary>♻️ Proposed fix for field name flexibility</summary>

```diff
 func walkFirstImageLikeURL(v any) string {
 	switch x := v.(type) {
 	case map[string]any:
-		if u, ok := x["url"].(string); ok && strings.HasPrefix(u, "http") && looksLikeImageAssetURL(u) {
-			return u
+		// Check common URL field names
+		urlFields := []string{"url", "imageUrl", "image_url", "downloadUrl", "download_url", "assetUrl", "asset_url"}
+		for _, field := range urlFields {
+			if u, ok := x[field].(string); ok && strings.HasPrefix(u, "http") && looksLikeImageAssetURL(u) {
+				return u
+			}
 		}
 		for _, vv := range x {
 			if s := walkFirstImageLikeURL(vv); s != "" {
 				return s
 			}
 		}
```

This makes the extraction more resilient to provider API variations.

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/task_result_url.go` around lines 37 - 56, The walkFirstImageLikeURL
function currently only checks for the literal "url" key; update it to accept
common variants (e.g., "imageUrl", "image_url", "downloadUrl", "download_url",
"assetUrl", "asset_url", "resultUrl", "result_url") and also consider a
case-insensitive/underscore-normalized match before validating
strings.HasPrefix(u, "http") and looksLikeImageAssetURL(u). Modify the
map-handling branch in walkFirstImageLikeURL to iterate candidate key names (or
normalize keys) and test each for a string URL match, then fall back to the
existing recursive descent if none match.
```

</details>

</blockquote></details>
<details>
<summary>web/default/src/lib/lobe-icon.tsx (1)</summary><blockquote>

`100-111`: _💤 Low value_

**Consider making border-radius configurable for external icons.**

The hardcoded `rounded-full` class (line 107) applies circular borders to all external icon URLs. While this works for most provider logos, some may be designed as squares or rectangles (e.g., Kie.ai).




If visual consistency becomes an issue, consider:
1. Adding a prop to control border style
2. Using `rounded` (small radius) instead of `rounded-full` as a safer default
3. Letting the image's natural shape show with `rounded-none`

For now, the current implementation is acceptable—flag this only if specific logos look distorted.

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/default/src/lib/lobe-icon.tsx` around lines 100 - 111, The external icon
image currently forces a circular crop via the hardcoded 'rounded-full' class on
the <img> returned when normalizeExternalIconUrl(trimmedName) yields a URL; make
the border radius configurable by adding a prop (e.g., iconBorderStyle or
imageRadius) to the LobeIcon component with a sensible default (prefer 'rounded'
instead of 'rounded-full'), then apply that prop value to the img className
instead of the fixed 'rounded-full' so callers can choose 'rounded-none',
'rounded', 'rounded-full', etc.; update the component signature and any call
sites that render LobeIcon to accept or forward the new prop.
```

</details>

</blockquote></details>
<details>
<summary>relay/channel/task/pingxingshijie/kind.go (1)</summary><blockquote>

`17-26`: _⚡ Quick win_

**Consider more precise path matching to avoid false positives.**

The current implementation uses `strings.Contains` for path matching, which could inadvertently match unintended paths. For example:
- `/some/prefix/v1/images/generations/suffix` would incorrectly return `UpstreamKindImage`
- `/custom/path/v1/assets/upload/extra` would incorrectly return `UpstreamKindAsset`

While this may work in the current routing context, using more precise matching improves robustness.




<details>
<summary>🔧 Consider using prefix-based matching</summary>

```diff
 func UpstreamKindFromPath(path string) string {
-	if strings.Contains(path, "/v1/assets/upload") {
+	if strings.HasPrefix(path, "/v1/assets/upload") {
 		return UpstreamKindAsset
 	}
-	// POST /v1/images/generations/async or GET /v1/images/generations/:task_id
-	if strings.Contains(path, "/v1/images/generations/") {
+	if strings.HasPrefix(path, "/v1/images/generations/") {
 		return UpstreamKindImage
 	}
 	return UpstreamKindVideo
 }
```

This ensures the paths are matched from the beginning, preventing false positives from arbitrary path segments.

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/pingxingshijie/kind.go` around lines 17 - 26, The function
UpstreamKindFromPath uses strings.Contains which can match substrings anywhere;
replace those checks with prefix-based matching (e.g., strings.HasPrefix) or an
anchored check so only paths beginning with "/v1/assets/upload" map to
UpstreamKindAsset and only paths beginning with "/v1/images/generations/" map to
UpstreamKindImage, leaving the fallback to UpstreamKindVideo unchanged; update
the checks in UpstreamKindFromPath to use strings.HasPrefix (or an equivalent
anchored/regex check) for the two constants UpstreamKindAsset and
UpstreamKindImage to avoid false positives.
```

</details>

</blockquote></details>
<details>
<summary>relay/channel/task/fal/constants.go (1)</summary><blockquote>

`59-70`: _💤 Low value_

**Verify intended fallback for unknown models in `getPricingMultiplier`.**

The implicit fallback uses `pricingMatrix` (text-to-image) for any model that isn't `ModelGPTImage2I2I`. With only two registered models this is fine today, but if a future model is added to `ModelList` without being added to the pricing matrices, callers will receive t2i pricing silently instead of `(0, false)`. Consider either matching exact known models or asserting the model is registered.




<details>
<summary>♻️ Optional defensive change</summary>

```diff
 func getPricingMultiplier(model, size, quality string) (float64, bool) {
-	matrix := pricingMatrix
-	if model == ModelGPTImage2I2I {
+	var matrix map[string]map[string]float64
+	switch model {
+	case ModelGPTImage2T2I:
+		matrix = pricingMatrix
+	case ModelGPTImage2I2I:
 		matrix = pricingMatrixEdit
+	default:
+		return 0, false
 	}
 	qmap, ok := matrix[size]
 	if !ok {
 		return 0, false
 	}
 	m, ok := qmap[quality]
 	return m, ok
 }
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/fal/constants.go` around lines 59 - 70,
getPricingMultiplier currently defaults to pricingMatrix for any model not equal
to ModelGPTImage2I2I, which can silently apply t2i rates for future unknown
models; update the function to explicitly handle known models (e.g., switch/case
or a map of supported model keys) and return (0, false) for any unrecognized
model instead of falling back to pricingMatrix, referencing the existing symbols
getPricingMultiplier, ModelGPTImage2I2I, pricingMatrix, and pricingMatrixEdit to
locate and adjust the branching logic.
```

</details>

</blockquote></details>
<details>
<summary>relay/channel/task/pingxingshijie/constants.go (1)</summary><blockquote>

`18-18`: _💤 Low value_

**Consider declaring `ChannelName` as a `const`.**

`ChannelName` is a fixed identifier never reassigned. Using `var` allows accidental mutation; sibling identifiers like `seedance20Model` and `AssetPlaceholderModel` are already `const`. This also matches the convention used in `relay/channel/task/kie/constants.go` and `relay/channel/task/fal/constants.go`.




<details>
<summary>♻️ Proposed change</summary>

```diff
-var ChannelName = "pingxingshijie-video"
+const ChannelName = "pingxingshijie-video"
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/pingxingshijie/constants.go` at line 18, ChannelName is a
fixed identifier declared with var; change its declaration to a constant by
replacing the var with const (i.e., make ChannelName a const string) so it
cannot be mutated and matches sibling constants like seedance20Model and
AssetPlaceholderModel and the pattern used in other channels (e.g., kie, fal).
```

</details>

</blockquote></details>
<details>
<summary>relay/channel/volcengine/adaptor.go (1)</summary><blockquote>

`425-427`: _💤 Low value_

**Redundant reset when `len(body) == 0`.**

When `body` is empty, you still wrap it in `io.NopCloser(bytes.NewReader(body))`. The original body has already been closed at line 408, but wrapping an empty buffer is essentially a no-op for downstream readers. Either skip the reset (downstream openai.Adaptor will treat empty body appropriately) or just always set `resp.Body` to the NopCloser after the read regardless of length, to keep the logic uniform.




<details>
<summary>♻️ Suggested simplification</summary>

```diff
 		if len(body) > 0 {
 			inner, bizCode, bizMsg := taskpxsj.NormalizePingXingOpenAIShapedSyncBody(body)
 			if bizCode != 0 {
 				return nil, types.NewErrorWithStatusCode(
 					fmt.Errorf("%s", bizMsg),
 					types.ErrorCodeBadResponseBody,
 					taskpxsj.HTTPStatusForPingXingBizCode(bizCode),
 				)
 			}
 			body = inner
-			resp.Body = io.NopCloser(bytes.NewReader(body))
-			resp.ContentLength = int64(len(body))
-			resp.Header.Del("Content-Encoding")
-		} else {
-			resp.Body = io.NopCloser(bytes.NewReader(body))
 		}
+		resp.Body = io.NopCloser(bytes.NewReader(body))
+		resp.ContentLength = int64(len(body))
+		resp.Header.Del("Content-Encoding")
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/volcengine/adaptor.go` around lines 425 - 427, The reset of
resp.Body using io.NopCloser(bytes.NewReader(body)) when len(body)==0 is
redundant; either skip reassigning resp.Body for empty bodies or always assign
the NopCloser to keep behavior uniform. Update the code around the resp.Body
assignment (the branch that currently does "if len(body) == 0 { /*noop*/ } else
{ resp.Body = io.NopCloser(bytes.NewReader(body)) }") to either remove the
conditional and unconditionally set resp.Body =
io.NopCloser(bytes.NewReader(body)), or add an early return/skip so you don't
wrap an empty body; ensure downstream callers that read resp.Body still receive
a valid io.ReadCloser when non-empty and that no double-close occurs.
```

</details>

</blockquote></details>
<details>
<summary>relay/channel/task/pingxingshijie/envelope.go (1)</summary><blockquote>

`46-61`: _💤 Low value_

**Edge case: top-level `code` exists but `data` is absent on success.**

When `code == 0` but `Data` is empty/`null`, the function returns the original `body` unchanged. The original body still has the envelope wrapper (`{"code":0,"msg":"ok"}`), which is then passed through to the OpenAI handler. This will likely fail to parse as an OpenAI-shaped response. Consider whether this fallthrough is intentional or should return an error/empty result.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/pingxingshijie/envelope.go` around lines 46 - 61, The
function NormalizePingXingOpenAIShapedSyncBody currently falls through and
returns the original envelope when env.Code==0 but env.Data is empty/"null";
change it to treat that as an invalid/missing-data case: inside
NormalizePingXingOpenAIShapedSyncBody (after checking env.Code == 0) detect if
len(env.Data) == 0 || string(env.Data) == "null" and return nil with a non-zero
business code and descriptive message (e.g., use env.text() or a constant like
-1 / "empty data in success envelope") instead of returning the original body so
the caller won't attempt to parse the wrapper as an OpenAI-shaped response.
Ensure you reference APIEnvelope, env.Data, env.Code and env.text() in the
change.
```

</details>

</blockquote></details>
<details>
<summary>service/task_polling.go (1)</summary><blockquote>

`452-458`: _⚡ Quick win_

**Consider using a constant for "image" string.**

The hardcoded string `"image"` should be extracted to a package-level constant for consistency and to avoid typos across the codebase. This pattern appears in multiple locations (controller, service, model extraction) where upstream kind is checked.



<details>
<summary>♻️ Refactor suggestion</summary>

Define a constant in a shared package (e.g., `constant` or `model`):

```go
const (
    UpstreamKindImage = "image"
    UpstreamKindVideo = "video"
)
```

Then use it here:

```diff
-} else if task.PrivateData.UpstreamKind == "image" {
+} else if task.PrivateData.UpstreamKind == constant.UpstreamKindImage {
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@service/task_polling.go` around lines 452 - 458, The code uses the literal
"image" when checking task.PrivateData.UpstreamKind; add a package-level
constant like UpstreamKindImage (e.g., in model or a shared constants package)
and replace the literal with that constant in this file (where the if checks
task.PrivateData.UpstreamKind), and update other occurrences across the codebase
(controller, service, model extraction) to use the same constant so comparisons
(and any uses in model.ExtractImageURLFromJSONBytes or taskcommon.BuildProxyURL
call sites) are consistent and typo-safe.
```

</details>

</blockquote></details>
<details>
<summary>web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx (2)</summary><blockquote>

`35-36`: _💤 Low value_

**`Image` from `lucide-react` shadows the global `Image` constructor.**

Importing `Image` directly shadows the DOM `HTMLImageElement` constructor in this module scope. If anything in this file (now or later) tries `new Image()` for preloading, it will fail at runtime. Aliasing on import is a one-line, friction-free safety net:

<details>
<summary>♻️ Suggested alias</summary>

```diff
-  Image,
+  Image as ImageIcon,
 } from 'lucide-react';
```
And update the JSX site:
```diff
-          <Tag color='cyan' shape='circle' prefixIcon={<Image size={14} />}>
+          <Tag color='cyan' shape='circle' prefixIcon={<ImageIcon size={14} />}>
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx` around
lines 35 - 36, The import of Image from lucide-react shadows the global Image
constructor; change the import to alias the icon (e.g., import { Image as
ImageIcon } from 'lucide-react') and update all usages in this file (e.g., JSX
where <Image ... /> is used) to the new alias (ImageIcon) so any calls to the
global new Image() remain valid and no runtime shadowing occurs.
```

</details>

---

`181-260`: _💤 Low value_

**Logic duplicates the default frontend's `task-media-results.ts` extractor.**

`extractImageUrlFromTaskData` / `resolveTaskPreviewUrl` / `isAsyncImageTaskForPreview` re-implement the same URL detection, b64_json → data-URL conversion, stale-image-proxy filtering, and seedream/tos heuristics that already exist in `web/default/src/features/usage-logs/lib/task-media-results.ts`. The two implementations are now coupled by behavior but not by code, so any future fix to detection rules (e.g., new image hosts, new url patterns) will likely be applied to only one frontend and silently diverge.

Since classic and default are separate codebases, full extraction may not be in scope for this PR, but at minimum keep these heuristics colocated and add a `// keep in sync with web/default/src/features/usage-logs/lib/task-media-results.ts` comment so the linkage isn't lost.

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In `@web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx` around
lines 181 - 260, The three functions extractImageUrlFromTaskData,
resolveTaskPreviewUrl, and isAsyncImageTaskForPreview duplicate heuristics from
web/default/src/features/usage-logs/lib/task-media-results.ts; update this file
to add a clear sync/comment header above these functions like "// keep in sync
with web/default/src/features/usage-logs/lib/task-media-results.ts" (and a brief
TODO suggesting to refactor to a shared helper when feasible) so future changes
to detection rules are noticed, and ensure the comment references the exact
source file path used in the review comment; optionally, if you can, replace the
duplicated logic by importing the shared extractor from that module instead of
keeping two implementations.
```

</details>

</blockquote></details>
<details>
<summary>relay/relay_task.go (2)</summary><blockquote>

`167-171`: _💤 Low value_

**Silent error suppression on Kie fallback model resolution.**

When `relaycommon.GetTaskRequest(c)` fails (e.g., body re-read failure or unexpected shape), the fallback model resolution is silently skipped and `modelName` keeps the placeholder (e.g., `"dall-e"`). Downstream `ModelPriceHelperPerCall` may then operate on the placeholder, leading to confusing pricing/quota errors instead of a clear root-cause signal.

Consider at least logging the error so operators can diagnose missed Kie default-model selection:

<details>
<summary>🪵 Suggested logging</summary>

```diff
 if info.ChannelType == constant.ChannelTypeKieAI && isKieFallbackTaskModel(modelName, platform, info.Action) {
-    if taskReq, err := relaycommon.GetTaskRequest(c); err == nil {
+    taskReq, err := relaycommon.GetTaskRequest(c)
+    if err == nil {
         modelName = taskkie.DefaultModelForRequest(info.RequestURLPath, taskReq.HasImage())
+    } else {
+        common.LogWarn(c, fmt.Sprintf("kie default-model resolution skipped: %v", err))
     }
 }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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 167 - 171, The code currently swallows
errors from relaycommon.GetTaskRequest inside the Kie fallback block (used by
isKieFallbackTaskModel) which can leave modelName as a placeholder; update the
block so that when relaycommon.GetTaskRequest(c) returns an error you log that
error (include context like info.RequestURLPath, info.Action and the attempted
modelName) via the existing logger before leaving modelName unchanged; keep the
existing behavior of calling taskkie.DefaultModelForRequest when GetTaskRequest
succeeds and ensure the log message is clear so downstream uses (e.g.,
ModelPriceHelperPerCall) can be diagnosed.
```

</details>

---

`292-300`: _💤 Low value_

**`otherRatiosProduct` silently treats ratio `0` and negative ratios as `1`.**

The condition `if ra != 1.0 && ra > 0` means a misconfigured or buggy `0` ratio (which would legitimately zero out a quota) is silently coerced to `1`, hiding upstream bugs. If `0` is unreachable by construction this is fine, but consider asserting/logging when a non-finite or non-positive ratio is encountered so configuration mistakes surface.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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 292 - 300, otherRatiosProduct currently
ignores ra==0 and negative values by treating them as 1.0 (due to the `ra > 0`
guard), which hides configuration bugs; update otherRatiosProduct to multiply by
any ratio that is not exactly 1.0 (so genuine zeros zero out the product) but
validate inputs first: detect non-finite (NaN/Inf) or negative ratios and either
log a warning/error or return an error upstream so misconfiguration surfaces;
reference the otherRatiosProduct function and the loop variable ra for where to
add the validation/logging and where to change the condition to only skip ra ==
1.0.
```

</details>

</blockquote></details>
<details>
<summary>relay/common/relay_info.go (1)</summary><blockquote>

`711-818`: _💤 Low value_

**`UnmarshalJSON` does a redundant marshal/unmarshal round-trip.**

The function (1) unmarshals into `map[string]json.RawMessage`, (2) deletes `"image"`, (3) re-marshals the map back to JSON via `common.Marshal(raw)`, (4) unmarshals that re-serialized blob into `aux`. Step 3/4 is unnecessary — you already have the raw fields and can decode them directly into the alias struct from the original `data` (since the alias struct's `image` field will be re-handled by your own logic anyway), or extract `Duration`/`Metadata` `json.RawMessage` directly from the existing `raw` map and skip the re-marshal entirely. This avoids an extra allocation/parse per task submission.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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_info.go` around lines 711 - 818,
TaskSubmitReq.UnmarshalJSON currently does an unnecessary marshal/unmarshal
round-trip: it parses into raw map[string]json.RawMessage, removes "image", then
common.Marshal(raw) and common.Unmarshal(stitched, &aux); remove that round-trip
and decode fields directly from the raw map. Specifically, in UnmarshalJSON use
the existing raw map to populate aux.Duration and aux.Metadata (e.g. set
aux.Duration = raw["duration"] / aux.Metadata = raw["metadata"] if present) and
unmarshal the rest of the alias fields by unmarshalling raw into the Alias
struct (or manually unmarshal known alias fields from raw), then continue the
existing image, Duration (int/string) and metadata merging logic; update
references to raw, aux, Metadata, Duration, Image/Images and Alias in
TaskSubmitReq.UnmarshalJSON to avoid calling common.Marshal/common.Unmarshal on
stitched.
```

</details>

</blockquote></details>
<details>
<summary>relay/channel/task/fal/adaptor.go (1)</summary><blockquote>

`158-167`: _⚡ Quick win_

**Avoid calling `parseDimensions` twice; reuse the first result.**

The success branch invokes `parseDimensions(taskReq.Size)` once for `err == nil` and then again to bind `w, h`, swallowing the error the second time. Bind values from the first call.

<details>
<summary>♻️ Suggested fix</summary>

```diff
-	if taskReq.Size != "" {
-		if _, _, err := parseDimensions(taskReq.Size); err == nil {
-			w, h, _ := parseDimensions(taskReq.Size)
-			req.ImageSize = map[string]int{"width": w, "height": h}
-		} else {
-			req.ImageSize = imageSize
-		}
-	} else {
-		req.ImageSize = imageSize
-	}
+	if taskReq.Size != "" {
+		if w, h, err := parseDimensions(taskReq.Size); err == nil {
+			req.ImageSize = map[string]int{"width": w, "height": h}
+		} else {
+			req.ImageSize = imageSize
+		}
+	} else {
+		req.ImageSize = imageSize
+	}
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/fal/adaptor.go` around lines 158 - 167, The code calls
parseDimensions(taskReq.Size) twice; change the logic to call it once and reuse
the returned values: call parseDimensions(taskReq.Size) into w, h, err, check
err == nil and on success set req.ImageSize = map[string]int{"width": w,
"height": h}, otherwise set req.ImageSize = imageSize; update the branch around
taskReq.Size, parseDimensions, and req.ImageSize to use the single result
instead of invoking parseDimensions a second time.
```

</details>

</blockquote></details>
<details>
<summary>relay/channel/task/sub2api_async/adaptor_test.go (1)</summary><blockquote>

`30-81`: _⚡ Quick win_

**Consider asserting `images` key absence alongside `input_urls` for the image-to-image case.**

The test only verifies the presence/shape of `wantKey` (`input_urls`). It does not assert that the generic `images` key is absent. Combined with the source logic in `adaptor.go` (lines 447-454), both keys can end up populated simultaneously. Asserting absence here would catch any future double-population regression.

<details>
<summary>♻️ Suggested addition</summary>

```diff
 			got, ok := body[tc.wantKey].([]string)
 			if !ok {
 				t.Fatalf("%s has type %T", tc.wantKey, body[tc.wantKey])
 			}
 			if len(got) != 2 || got[0] != "https://example.com/a.png" || got[1] != "https://example.com/b.png" {
 				t.Fatalf("%s = %#v", tc.wantKey, got)
 			}
+			if _, hasImages := body["images"]; hasImages {
+				t.Fatalf("input should not also include generic 'images' key: %#v", body)
+			}
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/sub2api_async/adaptor_test.go` around lines 30 - 81, In
TestConvertGPTImagePayloadsFromUnifiedRequest update the assertions for the
image-to-image case (inside t.Run) to also assert that the generic "images" key
is not present in the returned payload from TaskAdaptor.convertToRequestPayload;
locate the image case by modelName ModelGPTImage2ImageToImage and add a check
that body does not contain "images" (or that body["images"] is nil/absent) to
catch any double-population with "input_urls".
```

</details>

</blockquote></details>
<details>
<summary>relay/channel/task/sub2api_async/adaptor.go (1)</summary><blockquote>

`432-454`: _⚡ Quick win_

**Consolidate duplicate size check and clarify image-key population logic.**

Two issues in `convertToRequestPayload`:

1. **Lines 432-436 duplicate the size guard**: Both `if req.Size != ""` blocks are redundant—consolidate them into a single block that assigns `size` and calls `applySize`.

2. **Lines 447-454 may set redundant image keys**: When `cfg.ImageKey` is configured, the code sets both standard keys (`input["image"]` or `input["images"]`) AND the config-specific key (`input[cfg.ImageKey]`). For example, with two images and `ImageKey="input_urls"`, both `input["images"]` and `input["input_urls"]` are populated. If the upstream API requires both keys for compatibility, this is acceptable; otherwise, prefer a single canonical key per model config. Consider restructuring to use `cfg.ImageKey` exclusively when configured, falling back to standard keys only when not set.

<details>
<summary>♻️ Suggested cleanup</summary>

```diff
-	if req.Size != "" {
-		input["size"] = req.Size
-	}
-	if req.Size != "" {
-		applySize(input, req.Size)
-	}
+	if req.Size != "" {
+		input["size"] = req.Size
+		applySize(input, req.Size)
+	}
 	if req.Resolution != "" {
 		input["resolution"] = req.Resolution
 	}
 	if err := taskcommon.UnmarshalMetadata(req.Metadata, &input); err != nil {
 		return nil, err
 	}
 
 	cfg := getModelConfig(modelName)
 	images := requestImages(req)
-	if len(images) == 1 {
-		input["image"] = images[0]
-	} else if len(images) > 1 {
-		input["images"] = images
-	}
-	if len(images) > 0 && cfg.ImageKey != "" {
-		input[cfg.ImageKey] = images
+	if len(images) > 0 {
+		if cfg.ImageKey != "" {
+			input[cfg.ImageKey] = images
+		} else if len(images) == 1 {
+			input["image"] = images[0]
+		} else {
+			input["images"] = images
+		}
+	}
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
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/sub2api_async/adaptor.go` around lines 432 - 454, In
convertToRequestPayload, remove the duplicated size guard by consolidating the
two `if req.Size != ""` blocks into one that sets input["size"]=req.Size and
calls applySize(input, req.Size) once; for images, use getModelConfig(modelName)
and requestImages(req) and then populate only the canonical key: if cfg.ImageKey
!= "" set input[cfg.ImageKey] = images (use singular/plural form as desired by
cfg if needed), otherwise fall back to setting input["image"] for one image or
input["images"] for multiple; ensure you no longer set both the standard keys
and input[cfg.ImageKey] to avoid redundancy.
```

</details>

</blockquote></details>

</blockquote></details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `dd5e714a-74dc-46dc-b0d2-6756d161f893`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ba474393fbb91ad7bb66e6c9693da8d0550eedfb and 0510b7df0450ccb0f0c01e77ae263118736a07c3.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `web/classic/bun.lock` is excluded by `!**/*.lock`
* `web/classic/public/kie-ai-logo.png` is excluded by `!**/*.png`

</details>

<details>
<summary>📒 Files selected for processing (95)</summary>

* `.gitignore`
* `common/api_type.go`
* `common/api_type_test.go`
* `constant/channel.go`
* `controller/channel-test.go`
* `controller/model.go`
* `controller/model_list_test.go`
* `controller/relay.go`
* `docs/fal-ai-channel.md`
* `docs/pingxingshijie-api-reference.md`
* `docs/pingxingshijie-downstream-curl.md`
* `docs/pingxingshijie-openai-compatibility.md`
* `docs/token-management-api-reference.md`
* `dto/openai_image.go`
* `dto/openai_request.go`
* `dto/task.go`
* `middleware/distributor.go`
* `model/task.go`
* `model/task_result_url.go`
* `relay/channel/adapter.go`
* `relay/channel/claude/relay-claude.go`
* `relay/channel/openai/adaptor.go`
* `relay/channel/openai/relay-openai.go`
* `relay/channel/task/ali/adaptor.go`
* `relay/channel/task/ali/constants.go`
* `relay/channel/task/doubao/adaptor.go`
* `relay/channel/task/fal/adaptor.go`
* `relay/channel/task/fal/constants.go`
* `relay/channel/task/kie/adaptor.go`
* `relay/channel/task/kie/adaptor_test.go`
* `relay/channel/task/kie/constants.go`
* `relay/channel/task/pingxingshijie/adaptor.go`
* `relay/channel/task/pingxingshijie/constants.go`
* `relay/channel/task/pingxingshijie/constants_test.go`
* `relay/channel/task/pingxingshijie/draft_upscale_test.go`
* `relay/channel/task/pingxingshijie/envelope.go`
* `relay/channel/task/pingxingshijie/envelope_test.go`
* `relay/channel/task/pingxingshijie/extract_video_id_test.go`
* `relay/channel/task/pingxingshijie/kind.go`
* `relay/channel/task/pingxingshijie/unwrap_task_data_test.go`
* `relay/channel/task/pingxingshijie/upstream_metadata.go`
* `relay/channel/task/pingxingshijie/upstream_metadata_test.go`
* `relay/channel/task/sub2api_async/adaptor.go`
* `relay/channel/task/sub2api_async/adaptor_test.go`
* `relay/channel/task/sub2api_async/constants.go`
* `relay/channel/volcengine/adaptor.go`
* `relay/channel/volcengine/adaptor_test.go`
* `relay/common/relay_info.go`
* `relay/common/task_submit_req_test.go`
* `relay/helper/stream_scanner.go`
* `relay/relay_adaptor.go`
* `relay/relay_adaptor_test.go`
* `relay/relay_task.go`
* `relay/relay_task_test.go`
* `router/relay-router.go`
* `router/relay_router_test.go`
* `router/video-router.go`
* `service/file_service.go`
* `service/task_polling.go`
* `web/classic/package.json`
* `web/classic/src/components/table/task-logs/TaskLogsColumnDefs.jsx`
* `web/classic/src/components/table/task-logs/TaskLogsTable.jsx`
* `web/classic/src/components/table/task-logs/index.jsx`
* `web/classic/src/components/table/task-logs/modals/ContentModal.jsx`
* `web/classic/src/constants/channel.constants.js`
* `web/classic/src/helpers/render.jsx`
* `web/classic/src/hooks/task-logs/useTaskLogsData.js`
* `web/classic/src/i18n/locales/en.json`
* `web/classic/src/i18n/locales/fr.json`
* `web/classic/src/i18n/locales/ja.json`
* `web/classic/src/i18n/locales/ru.json`
* `web/classic/src/i18n/locales/vi.json`
* `web/classic/src/i18n/locales/zh-CN.json`
* `web/classic/src/i18n/locales/zh-TW.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`
* `web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx`
* `web/default/src/features/usage-logs/components/dialogs/task-media-results-dialog.tsx`
* `web/default/src/features/usage-logs/lib/task-media-results.ts`
* `web/default/src/features/usage-logs/types.ts`
* `web/default/src/i18n/locales/_reports/_sync-report.json`
* `web/default/src/i18n/locales/_reports/ja.untranslated.json`
* `web/default/src/i18n/locales/_reports/ru.untranslated.json`
* `web/default/src/i18n/locales/_reports/zh.untranslated.json`
* `web/default/src/i18n/locales/en.json`
* `web/default/src/i18n/locales/fr.json`
* `web/default/src/i18n/locales/ja.json`
* `web/default/src/i18n/locales/ru.json`
* `web/default/src/i18n/locales/vi.json`
* `web/default/src/i18n/locales/zh.json`
* `web/default/src/lib/lobe-icon.tsx`
* `web/default/tests/channel-utils.test.ts`
* `web/default/tests/lobe-icon.test.tsx`
* `web/default/tests/task-media-results.test.ts`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread controller/channel-test.go
Comment thread docs/fal-ai-channel.md Outdated
Comment thread middleware/distributor.go
Comment thread relay/channel/openai/relay-openai.go Outdated
Comment thread relay/channel/openai/relay-openai.go Outdated
Comment on lines +2326 to +2327
"点击预览图片": "點擊預覽圖片",
"图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。": "圖片無法在此瀏覽器中載入,可能是跨域或防盜鏈限制。",

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

Translation key names violate coding guidelines.

The translation keys use Chinese phrases directly ("点击预览图片", "图片无法在当前浏览器中加载...") instead of hierarchical, semantically clear English identifiers. According to the coding guidelines, translation keys should follow patterns like dashboard.overview.title or image.preview.click.

Consider refactoring to:

  • "image.preview.click""點擊預覽圖片"
  • "image.preview.error.loadFailed""圖片無法在此瀏覽器中載入,可能是跨域或防盜鏈限制。"

This improves maintainability and consistency across all locale files.

As per coding guidelines: Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency.

🤖 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 `@web/classic/src/i18n/locales/zh-TW.json` around lines 2326 - 2327, The
translation keys in zh-TW.json currently use Chinese text as keys ("点击预览图片",
"图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。"); replace those keys with hierarchical,
semantically clear identifiers (e.g., change the key "点击预览图片" to
"image.preview.click" and change "图片无法在当前浏览器中加载,这可能是由于跨域或防盗链。" to
"image.preview.error.loadFailed") while preserving the existing Traditional
Chinese values ("點擊預覽圖片" and "圖片無法在此瀏覽器中載入,可能是跨域或防盜鏈限制。") so other code can
reference image.preview.click and image.preview.error.loadFailed instead of
literal Chinese keys.

Comment on lines +124 to +129
hints: {
baseUrl: 'Default: https://api.sub2api.com',
key: 'Bearer API key',
models: 'gpt-image-2-text-to-image,gpt-image-2-image-to-image',
other: 'Task-only channel for POST /v1/images/generations/async',
},

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

New channel hints are not i18n-ready

The newly added hints text is hardcoded English in a non-React .ts module. Please wrap these strings with t(...) so they participate in localization like other user-facing copy.

As per coding guidelines: "web/default/**/*.ts: In non-React environments (utility functions, constants, class methods), use import { t } from 'i18next' for translations; note this will not auto-update on language changes".

🤖 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 `@web/default/src/features/channels/lib/channel-type-config.ts` around lines
124 - 129, The new hardcoded English strings in the hints object inside
channel-type-config.ts need to be made i18n-ready: add import { t } from
'i18next' at the top of the module and replace each literal in the hints object
(baseUrl, key, models, other) with calls to t('...') (e.g., baseUrl: t('Default:
https://api.sub2api.com')), preserving the original text as the translation
key/value; keep using t from i18next (non-React usage) so these constants
participate in localization.

Comment on lines +86 to +88
<p className='text-muted-foreground font-mono text-xs break-all'>
{result.url}
</p>

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 render full data URLs in the card body.

The PR now supports base64 task outputs, and this paragraph renders result.url verbatim. For data:image/...;base64,... results that can mean megabytes of text per card, which will bloat the DOM and make the dialog unusable.

Suggested fix
+  const displayUrl =
+    result.url.startsWith('data:') ? `${result.url.slice(0, 64)}…` : result.url
+
   return (
@@
         <p className='text-muted-foreground font-mono text-xs break-all'>
-          {result.url}
+          {displayUrl}
         </p>
📝 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
<p className='text-muted-foreground font-mono text-xs break-all'>
{result.url}
</p>
const displayUrl =
result.url.startsWith('data:') ? `${result.url.slice(0, 64)}…` : result.url
<p className='text-muted-foreground font-mono text-xs break-all'>
{displayUrl}
</p>
🤖 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
`@web/default/src/features/usage-logs/components/dialogs/task-media-results-dialog.tsx`
around lines 86 - 88, The dialog is currently rendering full result.url which
can be a huge data URL; update TaskMediaResultsDialog (the component rendering
the paragraph with {result.url}) to detect data URLs (e.g., strings starting
with "data:") and avoid inserting the entire base64 blob into the DOM: instead
render a short placeholder/summary (e.g., mime type + size or "data:<type>
(base64, N KB)") and provide either an explicit "View" or "Download" action that
uses the full data URL only when the user requests it (or render an <img>
preview for image types), so the paragraph no longer dumps megabytes of base64
text into the DOM.

Comment thread web/default/src/i18n/locales/en.json Outdated
"Click the button below to bind your Telegram account": "Click the button below to bind your Telegram account",
"Click to open deployment": "Click to open deployment",
"Click to preview audio": "Click to preview audio",
"Click to preview image": "Click to preview image",

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 hierarchical i18n keys for these new entries

These additions should use semantic, hierarchical keys instead of literal English phrases to keep key naming consistent and maintainable.

Suggested key pattern (example)
- "Click to preview image": "Click to preview image",
- "Generated Results": "Generated Results",
- "Generated video": "Generated video",
- "No generated media results": "No generated media results",
- "View generated media results": "View generated media results",
- "View generated results": "View generated results",
+ "taskMedia.preview.image": "Click to preview image",
+ "taskMedia.results.title": "Generated Results",
+ "taskMedia.results.video": "Generated video",
+ "taskMedia.results.empty": "No generated media results",
+ "taskMedia.results.viewMedia": "View generated media results",
+ "taskMedia.results.view": "View generated results",

As per coding guidelines web/default/src/i18n/**/*.{ts,tsx,json}: "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

Also applies to: 1643-1645, 2259-2259, 3789-3790, 3913-3913

🤖 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 `@web/default/src/i18n/locales/en.json` at line 641, The JSON entry uses a
literal English phrase as the key ("Click to preview image"); replace it with a
hierarchical, semantic key such as image.preview.tooltip or image.preview.click
(pick the most semantically appropriate pattern used across the repo), update
the JSON key accordingly, and then update all code references that read "Click
to preview image" to use the new hierarchical key; also apply the same refactor
to the other flagged entries (around the indicated ranges) to follow the same
naming convention for consistency.

Comment thread web/default/src/i18n/locales/vi.json Outdated
"Click the button below to bind your Telegram account": "Nhấp vào nút bên dưới để liên kết tài khoản Telegram của bạn",
"Click to open deployment": "Nhấp để mở triển khai",
"Click to preview audio": "Nhấp để xem trước âm thanh",
"Click to preview image": "Nhấp để xem trước hình ảnh",

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 | 🏗️ Heavy lift

Adopt hierarchical i18n key names for newly added locale entries

The new keys are still phrase-based; please migrate these additions to hierarchical semantic keys (and keep compatibility aliases only if needed) to match the locale-key standard.

As per coding guidelines, "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

Also applies to: 1643-1645, 2259-2259, 3789-3790, 3913-3913

🤖 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 `@web/default/src/i18n/locales/vi.json` at line 641, Replace the phrase-based
locale key "Click to preview image" with a hierarchical, semantic key (for
example "image.preview.click" or "image.preview.tooltip") and move the
Vietnamese string "Nhấp để xem trước hình ảnh" under that new key in the same
JSON; if backward compatibility is required, add the original phrase key as an
alias whose value references the new hierarchical key value. Also apply the same
refactor to the other phrase-style keys noted (the entries around the other
ranges) so all new translations follow the hierarchical pattern (e.g.,
dashboard.overview.title, image.preview.*).

Fankouzu added 2 commits May 11, 2026 21:13
Review feedback surfaced several concrete edge cases around async task fetch authorization, Sub2API upstream calls, streamed usage parsing, Fal result parsing, and frontend rendering of inline image data. This commit keeps the fixes narrow and avoids changing the broader async-channel design.

Constraint: PR review comments came from CodeRabbit and needed validation against this codebase before applying.

Rejected: Convert locale keys to hierarchical names | default frontend project convention uses source-string i18n keys.

Confidence: high

Scope-risk: moderate

Directive: Keep Sub2API-async as an async task wrapper over the upstream synchronous OpenAI-compatible image generation endpoint.

Tested: go test ./controller ./middleware ./relay/channel/openai ./relay/channel/task/ali ./relay/channel/task/doubao ./relay/channel/task/fal ./relay/channel/task/pingxingshijie ./relay/channel/task/sub2api_async ./service

Tested: bun test tests/channel-utils.test.ts tests/task-media-results.test.ts
The PR branch conflicted with QuantumNous/main after upstream frontend and DTO changes. This merge preserves upstream main as the base while reapplying the Sub2API async image task behavior, task media preview integration, and review-risk fixes.

Constraint: PR QuantumNous#4778 base is QuantumNous/new-api:main and GitHub reported mergeStateStatus DIRTY/CONFLICTING.

Rejected: Rebase the branch | merge commit preserves the already-published PR branch history and avoids rewriting remote review context.

Confidence: high

Scope-risk: broad

Directive: Future fixes should land after this merge base; do not remove Sub2API-async media preview wiring when editing usage-log columns.

Tested: go test ./controller ./middleware ./relay/channel/openai ./relay/channel/task/ali ./relay/channel/task/doubao ./relay/channel/task/fal ./relay/channel/task/pingxingshijie ./relay/channel/task/sub2api_async ./service

Tested: bun test tests/channel-utils.test.ts tests/task-media-results.test.ts
@Fankouzu

Copy link
Copy Markdown
Author

撤回该 PR。

@Fankouzu Fankouzu closed this May 11, 2026
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