Skip to content

fix(pingxingshijie): enforce asset upload model - #4578

Closed
Fankouzu wants to merge 66 commits into
QuantumNous:mainfrom
Fankouzu:fix/pingxing-asset-upload-model
Closed

fix(pingxingshijie): enforce asset upload model#4578
Fankouzu wants to merge 66 commits into
QuantumNous:mainfrom
Fankouzu:fix/pingxing-asset-upload-model

Conversation

@Fankouzu

@Fankouzu Fankouzu commented May 1, 2026

Copy link
Copy Markdown

Summary

  • Require PingXing asset uploads to explicitly use pingxingshijie-asset for routing and billing.
  • Add distributor and adaptor regression coverage rejecting blank or non-asset models.
  • Label asset upload tasks distinctly in usage logs instead of falling back to image-to-video.

Verification

  • go test -race ./middleware ./relay/channel/task/pingxingshijie
  • node --experimental-strip-types src/features/usage-logs/lib/task-action-label.test.ts
  • npm run typecheck in web/default
  • npm run build in web/default

Notes

  • Full npm run lint still fails on unrelated pre-existing files outside this patch.
  • Runtime token/channel/ability allowlists must include pingxingshijie-asset for successful uploads.

Summary by CodeRabbit

  • New Features

    • Added PingXingShiJie and KieAI providers with OpenAI-style interoperability, async image/video flows, and asset upload support
    • Image preview modal and consolidated generated-media/results dialog in task logs
  • Documentation

    • New PingXingShiJie API reference, compatibility guide, downstream curl page, and Token Management API docs
  • Improvements

    • Better media result extraction, MIME detection, routing, billing estimates, and expanded i18n/UI strings
  • Tests

    • Extensive unit and integration tests across providers and media flows
  • Chores

    • .gitignore updates and build dependency adjustments

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 and others added 15 commits April 30, 2026 17:16
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>
fix(ui): add default channel logos for PingXingShiJie and KieAI
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>
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>
feat(logs): show task media results in default UI
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>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

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

Adds two new channels (PingXingShiJie: 58, KieAI: 59) with adaptors, model registrations, async image/video/asset endpoints and converters, upstream-envelope normalization, upstream_kind propagation, result URL extraction, middleware/router/frontend wiring, tests, and documentation.

Changes

Cohort / File(s) Summary
Repo config
\.gitignore
Add skills-lock.json, ignore local docs file; adjust .test entry.
Channel constants & mapping
constant/channel.go, common/api_type.go
Introduce channel types 58/59, register base URLs/names, map to API types.
Adaptor registration & model lists
relay/relay_adaptor.go, controller/model.go
Register PingXingShiJie & KieAI adaptors and include their model lists in global registries.
PingXingShiJie adaptor & utilities
relay/channel/task/pingxingshijie/...
Full TaskAdaptor for pingxingshijie: request/validation/body building, envelope normalization, fetch/poll, upstream_kind detection, OpenAI-style converters (video/image/asset), pricing helpers and many tests.
KIE adaptor & constants
relay/channel/task/kie/...
New TaskAdaptor, model catalog, request/polling/parsing, OpenAI-compatible conversions, billing estimation, and tests.
VolcEngine & adaptor plumbing
relay/channel/volcengine/adaptor.go, .../adaptor_test.go
Route PingXingShiJie to v2 endpoints; preprocess/unpack envelopes and map biz codes to HTTP status.
Relay task & polling plumbing
relay/relay_task.go, controller/relay.go, service/task_polling.go
Propagate PrivateData.UpstreamKind, KieAI model override, include upstream_kind in fetch payloads, quota/ratio refactor, and response conversion routing.
Common relay DTOs & request parsing
relay/common/relay_info.go, dto/task.go, dto/openai_request.go
Support image as string/array, add resolution field, capture unknown top-level fields into metadata, add TaskDto.UpstreamKind, and infer file MIME types from filenames.
Result URL extraction
model/task.go, model/task_result_url.go
Add UpstreamKind to private data; implement JSON extraction heuristics and upstream-kind-aware GetResultURL; add GetByUpstreamTaskId.
Middleware, routing & controller tweaks
middleware/distributor.go, router/video-router.go, controller/channel-test.go
Map async image/asset routes to relay modes, validate asset-upload model, add image/asset routes, and seedream endpoint detection for PingXingShiJie.
Relay interfaces & adapters
relay/channel/adapter.go, relay/channel/claude/...
Add OpenAI async image/asset converter interfaces; refine Claude media handling.
Ali adaptor updates
relay/channel/task/ali/...
PixVerse support: media arrays, shot_type, extra resolutions, request shaping, and billing multipliers; extend model list.
Helpers & envelope utilities
relay/channel/task/pingxingshijie/envelope.go, kind.go, upstream_metadata.go
PingXingShiJie envelope parsing/normalization, UpstreamKind-from-path detection, and upstream JSON helpers.
Frontend: task logs, previews & hooks
web/classic/src/...task-logs/*, web/classic/src/hooks/...
Add image preview modal, preview URL resolution, hook state and wiring, UI strings, and component updates.
Frontend: generated media results
web/default/src/features/usage-logs/*
Add media-extraction util, TaskMediaResultsDialog component, task-action label helper, types updates, i18n strings, and tests.
UI channel icons & lists
web/classic/src/constants/*, web/default/.../channel-utils.ts, web/default/src/lib/lobe-icon.tsx
Add channels 58/59 to UI options, map favicon/logo URLs, and support external icon URLs.
Tests & docs
many *_test.go, frontend tests, docs/*
Extensive unit/integration tests across adaptors, envelopes, model registration, quota math, UI tests; add PingXingShiJie API docs and token-management docs.
Misc & infra
service/file_service.go, web/classic/package.json, web/classic/vite.config.js
MIME detection for base64 files, frontend dependency upgrades, and disable compressed size report in Vite.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Gateway
    participant Relay
    participant Adaptor
    participant Upstream
    participant DB
    Client->>Gateway: POST /v1/images/generations/async (OpenAI-shape)
    Gateway->>Relay: RelayTask submit (model, metadata, relay_mode)
    Relay->>Adaptor: BuildRequestURL/Body/Header (includes upstream_kind)
    Adaptor->>Upstream: HTTP Create Task (v1/v2 endpoint)
    Upstream-->>Adaptor: 200 with envelope (code/data/taskId)
    Adaptor->>Relay: DoResponse (unwrap envelope, return task id & data)
    Relay->>DB: Persist Task (PrivateData.UpstreamKind, ResultURL/taskId)
    Client->>Gateway: GET /v1/images/generations/:task_id (poll)
    Gateway->>Relay: RelayTaskFetch (includes upstream_kind)
    Relay->>Adaptor: FetchTask (routes by upstream_kind)
    Adaptor->>Upstream: Poll status
    Upstream-->>Adaptor: status/data
    Adaptor->>Relay: ConvertToOpenAIAsyncImage/Asset/Video (normalize)
    Relay->>Gateway: Return OpenAI-shaped task status to Client
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • seefs001
  • Calcium-Ion

Poem

🐰 I hopped through code with ears alight,
Two channels planted, models take flight,
Envelopes unwrap and tasks hum through night,
Previews open wide — images in sight,
A cheerful hop — the relay feels right. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(pingxingshijie): enforce asset upload model' directly and clearly describes the main change—enforcing the asset upload model requirement for PingXingShiJie uploads.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

Fankouzu and others added 2 commits May 1, 2026 15:12
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>

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

Caution

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

⚠️ Outside diff range comments (1)
controller/model.go (1)

84-113: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Register taskpxsj.ModelList in openAIModels as well.

This block adds taskkie.ModelList to openAIModels, but PingXingShiJie is only added to channelId2Models. That leaves openAIModelsMap unaware of pingxingshijie-asset, so GET /v1/models/pingxingshijie-asset still falls through to model_not_found, and list metadata degrades to OwnedBy: "custom" instead of the channel name when the model is enabled.

💡 Suggested fix
+	for _, modelName := range taskpxsj.ModelList {
+		openAIModels = append(openAIModels, dto.OpenAIModels{
+			Id:      modelName,
+			Object:  "model",
+			Created: 1626777600,
+			OwnedBy: taskpxsj.ChannelName,
+		})
+	}
 	for _, modelName := range taskkie.ModelList {
 		openAIModels = append(openAIModels, dto.OpenAIModels{
 			Id:      modelName,
 			Object:  "model",
 			Created: 1626777600,
 			OwnedBy: taskkie.ChannelName,
 		})
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/model.go` around lines 84 - 113, The code populates openAIModels
with entries from taskkie.ModelList but never adds taskpxsj.ModelList, so models
like "pingxingshijie-asset" are missing from openAIModels and thus
openAIModelsMap; update the block that builds openAIModels to also append
dto.OpenAIModels entries for each modelName in taskpxsj.ModelList (similar to
the existing loop for taskkie.ModelList), using Object:"model",
Created:1626777600 and OwnedBy: taskpxsj.ChannelName, so openAIModels,
openAIModelsMap and channelId2Models remain consistent for
constant.ChannelTypePingXingShiJie handling.
🧹 Nitpick comments (6)
docs/token-management-api-reference.md (1)

76-76: ⚡ Quick win

Add language specifiers to HTTP endpoint code blocks.

The HTTP endpoint declarations are missing language specifiers, which triggers markdownlint warnings and prevents proper syntax highlighting.

📝 Proposed fix

Add http after the opening backticks for all endpoint declarations. For example:

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

Apply this pattern to all 11 endpoint blocks at lines 76, 136, 197, 256, 294, 390, 520, 560, 609, 671, and 743.

</details>


Also applies to: 136-136, 197-197, 256-256, 294-294, 390-390, 520-520, 560-560, 609-609, 671-671, 743-743

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

Verify each finding against the current code and only fix it if needed.

In @docs/token-management-api-reference.md at line 76, Several HTTP endpoint
code blocks (e.g., the block starting with "GET /api/token/") are missing
language specifiers which causes markdownlint warnings; open each endpoint's
fenced code block and add the language tag "http" immediately after the three
backticks (e.g., change tohttp) for all listed endpoint blocks (lines
referenced in the comment include the blocks for GET /api/token/ and the other
10 endpoint declarations). Ensure you update every endpoint code fence so all 11
HTTP endpoint blocks use ```http for proper syntax highlighting and linting.


</details>

</blockquote></details>
<details>
<summary>controller/model_list_test.go (1)</summary><blockquote>

`214-220`: _⚡ Quick win_

**Make this test fail fast when the source model list is empty.**

Right now the loop can pass with zero iterations. Add an explicit non-empty assertion so registry wiring regressions don’t go unnoticed.
 

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

```diff
 func TestKieModelsAppearInGlobalModelRegistry(t *testing.T) {
+	require.NotEmpty(t, taskkie.ModelList, "taskkie.ModelList should not be empty")
 	for _, modelName := range taskkie.ModelList {
 		aiModel, ok := openAIModelsMap[modelName]
 		require.True(t, ok, "missing Kie model %s", modelName)
 		require.Equal(t, taskkie.ChannelName, aiModel.OwnedBy)
 	}
 }
```
</details>

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

```
Verify each finding against the current code and only fix it if needed.

In `@controller/model_list_test.go` around lines 214 - 220, The test
TestKieModelsAppearInGlobalModelRegistry currently iterates over
taskkie.ModelList but will silently succeed if that slice is empty; add a
precondition assertion (e.g., require.NotEmpty with t and taskkie.ModelList)
before the for-loop to fail fast when the source model list is empty, then keep
the existing loop that checks openAIModelsMap entries and aiModel.OwnedBy.
```

</details>

</blockquote></details>
<details>
<summary>web/default/src/features/channels/lib/channel-utils.ts (1)</summary><blockquote>

`86-87`: _⚡ Quick win_

**Clarify the helper contract now that it returns URLs too.**

Line 86 and Line 87 return raw URLs, while this helper is documented as returning icon *names*. Please align the docs/name to avoid misuse in future call sites.
 

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

```diff
-/**
- * Get channel type icon name for getLobeIcon
- * Maps channel types to Lobe icon names using type number (language-independent)
- */
+/**
+ * Get channel type icon identifier for UI.
+ * Returns either a Lobe icon name or a remote icon URL.
+ */
 export function getChannelTypeIcon(type: number): string {
-  const TYPE_TO_ICON: Record<number, string> = {
+  const TYPE_TO_ICON_OR_URL: Record<number, string> = {
@@
-  return TYPE_TO_ICON[type] || 'OpenAI'
+  return TYPE_TO_ICON_OR_URL[type] || 'OpenAI'
 }
```
</details>

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

```
Verify each finding against the current code and only fix it if needed.

In `@web/default/src/features/channels/lib/channel-utils.ts` around lines 86 - 87,
The helper in web/default/src/features/channels/lib/channel-utils.ts currently
returns raw icon URLs while its docs and name indicate it returns icon "names";
update the helper contract so it is unambiguous: either (A) change the function
name and JSDoc to reflect it returns icon URLs (e.g., rename to getIconUrl /
update return type to string URL and update callers/tests), or (B) preserve the
“name” contract by converting those URL entries (like
'https://www.pingxingshijie.cn/favicon.ico' and 'https://kie.ai/logo.png') to
icon identifiers and return only names (and map names to URLs in a separate
resolver). Make the chosen change consistently in the function signature,
exported type, JSDoc, and any callers in the repo.
```

</details>

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

`3-8`: **The current code complies with JSON wrapper guidelines; removing `encoding/json` is optional.**

The file correctly uses `common.Unmarshal()` instead of direct `encoding/json` calls. The `encoding/json` import is present only because the function accepts `json.RawMessage` as a parameter type. According to the coding guidelines, `json.RawMessage` and `json.Number` may still be referenced as types, making this import and type usage compliant with the JSON wrapper rule.

Replacing `json.RawMessage` with `[]byte` is a valid stylistic refactor (the types are equivalent at runtime), but it is not required by the documented guidelines. The suggestion assumes a stricter interpretation that goes beyond the stated exception.

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

```
Verify each finding against the current code and only fix it if needed.

In `@model/task_result_url.go` around lines 3 - 8, The import of encoding/json and
use of the json.RawMessage type in model/task_result_url.go is acceptable
because the function parameter uses json.RawMessage and the code uses
common.Unmarshal rather than direct json calls; no change required—leave the
import and the parameter type as-is, or if you prefer a stylistic refactor you
may change the parameter to []byte and remove the encoding/json import, but do
not modify the common.Unmarshal usage (refer to json.RawMessage and
common.Unmarshal in the function signature to locate the code).
```

</details>

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

`23-27`: _💤 Low value_

**Consider adding error handling for clipboard API.**

The clipboard API can fail (e.g., when permissions are denied or in insecure contexts). While not critical, catching the rejection would prevent silent failures.


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

```diff
 function copyUrl(url: string, successMessage: string): void {
-  void navigator.clipboard.writeText(url).then(() => {
-    toast.success(successMessage)
-  })
+  void navigator.clipboard.writeText(url)
+    .then(() => toast.success(successMessage))
+    .catch(() => toast.error('Failed to copy'))
 }
```
</details>

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

```
Verify each finding against the current code and only fix it if needed.

In
`@web/default/src/features/usage-logs/components/dialogs/task-media-results-dialog.tsx`
around lines 23 - 27, The copyUrl function currently writes to
navigator.clipboard without handling failures; update copyUrl(url: string,
successMessage: string) to catch promise rejections from
navigator.clipboard.writeText and surface an error (e.g., toast.error with a
helpful message) so clipboard permission/secure-context failures are not silent;
keep the success toast on then() and add a .catch() (or try/catch if you convert
to async/await) to log or toast the error and optionally console.error the
caught error for debugging.
```

</details>

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

`312-328`: _💤 Low value_

**Consider handling the case where `cfg.ImageKey` is empty for unknown models.**

When `modelName` doesn't match `ModelSeedance2` or `ModelHappyHorseImageToVideo`, and `cfg.ImageKey` is empty, images are silently dropped. This may be intentional, but if a new model is added without an image key config, image inputs would be lost without any warning.



<details>
<summary>🔧 Optional: Add debug logging for dropped images</summary>

```diff
 		default:
 			if cfg.ImageKey != "" {
 				input[cfg.ImageKey] = images
+			} else if len(images) > 0 {
+				// Consider logging that images were provided but model has no image key configured
 			}
 		}
```
</details>

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

```
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/task/kie/adaptor.go` around lines 312 - 328, When images are
present but fall through the switch (i.e., modelName is not ModelSeedance2 or
ModelHappyHorseImageToVideo) and getModelConfig(modelName).ImageKey is empty,
explicitly handle that case instead of silently dropping images: update the
block in the function that calls getModelConfig, requestImages and builds input
to detect images > 0 && cfg.ImageKey == "" and then either log a warning
(including modelName and number of images) or return an error so callers know
images were ignored; reference cfg.ImageKey, ModelSeedance2,
ModelHappyHorseImageToVideo, requestImages, getModelConfig and createTaskRequest
to locate where to add the check.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @controller/channel-test.go:

  • Around line 125-128: When forcing the PingXingShiJie image test path you set
    requestPath but forgot to set endpointType, causing buildTestRequest(...) to
    produce a non-image request; update the branch that checks channel.Type ==
    constant.ChannelTypePingXingShiJie &&
    strings.Contains(strings.ToLower(testModel), "seedream") to also set
    endpointType to the image type used by your request (e.g., the image/images
    enum/value your code expects) so buildTestRequest(...) builds an image request
    and image conversion won't fail.

In @dto/openai_request.go:

  • Around line 395-397: Change the conditional that sets a fallback MIME so it
    does not depend on file.FileName: whenever mimeType == "" and there is a file
    payload, set mimeType = "application/octet-stream" regardless of file.FileName.
    Locate the logic that currently checks mimeType == "" && file.FileName != ""
    (using the mimeType and file.FileName variables) and remove the file.FileName
    check so the fallback is unconditional for empty mimeType on file payloads.

In @middleware/distributor_test.go:

  • Around line 32-39: Update TestGetModelRequestAssetUploadRejectsBlankModel to
    also exercise the explicit empty-string model case by calling newJSONContext
    with a body that includes "model":"" (e.g.,
    {"model":"","image_url":"https://example.com/a.jpg","asset_type":"Image"}),
    call getModelRequest for that context, assert err is non-nil, and assert the
    returned error message contains the expected placeholder constant
    taskpxsj.AssetPlaceholderModel; keep the existing missing-model case as well so
    both scenarios are covered.

In @model/task_result_url.go:

  • Around line 37-55: walkFirstImageLikeURL currently iterates a map
    (map[string]any) which yields nondeterministic order; change the map descent in
    walkFirstImageLikeURL so keys are collected, sorted (e.g., lexicographically),
    and then iterated in that sorted order before recursing into values so the
    "first" image-like url is stable across runs; keep the existing url check
    (x["url"]) and recursion logic but replace the direct range over the map with a
    deterministic sorted-key loop.

In @model/task.go:

  • Around line 137-146: GetResultURL currently returns t.FailReason verbatim when
    PrivateData.ResultURL is empty, which can leak data: base64 payloads; change the
    logic in GetResultURL so that after selecting u := t.PrivateData.ResultURL or u
    = t.FailReason, you validate u with isVideoProxyContentURL(u, t.TaskID) and if
    that function indicates a disallowed data: URL, return an empty string instead
    of u; keep references to t.PrivateData.ResultURL, t.FailReason,
    isVideoProxyContentURL, and TaskID so reviewers can find and update the
    GetResultURL implementation accordingly.

In @relay/channel/task/doubao/adaptor.go:

  • Around line 303-308: When contentHasDraftTask(r.Content) is true, don’t just
    remove "text" items—enforce exclusivity by keeping only items where Type ==
    "draft_task" and removing everything else (filter r.Content to retain only
    draft_task entries); update the block in convertToRequestPayload that currently
    uses lo.Reject on "text" and make the same change in the analogous later block
    (the second contentHasDraftTask branch) so both places only preserve draft_task
    items.

In @relay/channel/task/pingxingshijie/asset_model_validation_test.go:

  • Around line 44-52: Update the test
    TestValidateRequestAndSetActionRejectsBlankAssetUploadModel to also exercise an
    explicit empty model value: create a second context (using
    newAssetUploadContext) whose payload includes "model":"" (e.g.
    {"image_url":"https://...","asset_type":"Image","model":""}) and call
    (&TaskAdaptor{}).ValidateRequestAndSetAction with that context and the same
    info, asserting that the returned error is non-nil; this ensures both missing
    and explicitly-blank model cases are covered.

In @relay/relay_task_test.go:

  • Around line 10-18: The test
    TestApplyOtherRatiosToQuotaMultipliesBeforeTruncating is flaky because it uses a
    map (ratios) whose iteration order can change and the chosen fractions land near
    an integer boundary; update the test(s) that call applyOtherRatiosToQuota (also
    the similar cases around lines 21-34) to use values that are well clear of
    truncation boundaries (so any multiplication order yields the same integer) or
    replace the map with an ordered helper that tests both multiplication orders
    explicitly; reference the test name
    TestApplyOtherRatiosToQuotaMultipliesBeforeTruncating and the function
    applyOtherRatiosToQuota when making the change.

In @service/file_service.go:

  • Around line 352-359: The current MIME-sniffing in the block using
    http.DetectContentType ignores the case where DetectContentType returns
    "application/octet-stream" (a generic unknown) so valid files stay
    misclassified; update the condition in the block that inspects sniffed (from
    http.DetectContentType(decodedData)) to treat "" and "application/octet-stream"
    as unknown and only set mimeType when sniffed is non-empty and not
    "application/octet-stream" (still stripping any ";charset=..." via strings.Index
    as done now). Locate the mimeType variable assignment and the
    http.DetectContentType call in this function and change the condition
    accordingly so application/octet-stream does not get accepted as a real MIME
    type.

In @service/task_polling.go:

  • Around line 362-369: FetchTask calls currently only pass baseURL, key, body,
    proxy so polling drops per-task resolved header overrides; update the FetchTask
    interface and all adaptor.FetchTask implementations to accept and forward the
    resolved header overrides and then populate the fetchBody with
    task.PrivateData.ResolvedHeaderOverride (falling back to existing derivation
    only if empty), similar to how task.PrivateData.Key is preferred; ensure the new
    parameter is threaded through callers and that fetchBody includes
    "resolved_header_override" (or equivalent key used by downstream) so follow-up
    polls reuse submit-time auth context and do not re-derive headers.

In @web/classic/package.json:

  • Around line 10-12: The package.json lists incompatible chart packages:
    @visactor/vchart-semi-theme@1.12.3 declares a peerDependency for
    @visactor/vchart 1.x while the project depends on @visactor/vchart@2.0.21 and
    @visactor/react-vchart@2.0.21; resolve by either upgrading
    @visactor/vchart-semi-theme to a release that supports vchart 2.x (replace the
    dependency entry for "@visactor/vchart-semi-theme" with the compatible version)
    or by downgrading "@visactor/vchart" and "@visactor/react-vchart" to matching
    1.x versions (update both entries) and ensure package-lock/yarn.lock is
    regenerated and CI installs succeed.

In @web/classic/src/components/table/task-logs/modals/ContentModal.jsx:

  • Around line 39-48: The effect that resets preview state only depends on
    isModalOpen, isVideo, and isImage, so it won't rerun when the parent swaps the
    URL (modalContent) while the modal remains open; update the dependency array of
    the useEffect that sets setVideoError(false), setImageError(false), and
    setIsLoading(true) to also include modalContent (or the prop that carries the
    new URL) so the effect runs whenever modalContent changes and the preview state
    is properly reset when a new asset is loaded.

In @web/default/src/features/usage-logs/lib/task-action-label.ts:

  • Around line 9-14: The function in task-action-label.ts currently returns
    hardcoded user-facing strings for log.upstream_kind ('asset' -> 'Asset Upload',
    'image' -> 'Image Generation'); replace those literal returns with stable i18n
    keys (e.g. 'usage.task.assetUpload', 'usage.task.imageGeneration') so the
    function returns keys only, and update consuming components to call
    useTranslation() and t(returnedKey) at render time; update any tests/fixtures to
    expect keys instead of English text and add the new keys to the locale files.

In @web/default/src/features/usage-logs/lib/task-media-results.ts:

  • Around line 117-121: extractTaskMediaResults currently seeds a task-proxy
    result (using isTaskVideoProxyUrl) before calling walkTaskData, which lets a
    stale proxy win even when a real image exists inside data; change the logic to
    treat proxy entries as fallbacks: either (A) do not push the seeded proxy into
    results up-front — instead keep it in a local fallbackCandidate and only append
    it after walkTaskData if no concrete image result was produced, or (B) after
    walkTaskData completes, scan results and remove any entries whose URL is a
    task-proxy (isTaskVideoProxyUrl(url, source)) whenever a concrete image result
    exists; apply the same change to the analogous block referenced at lines 183-185
    so proxy fallbacks cannot override real images.
  • Around line 68-74: Update the image-proxy heuristic to treat upstream_kind ===
    'asset' the same as 'image': in isStaleImageProxyUrl (and the other predicate
    that similarly checks source.upstream_kind for image/video heuristics later in
    the file), include source.upstream_kind === 'asset' alongside 'image' so asset
    uploads use the image-path logic and won't be misclassified as video or skipped.

Outside diff comments:
In @controller/model.go:

  • Around line 84-113: The code populates openAIModels with entries from
    taskkie.ModelList but never adds taskpxsj.ModelList, so models like
    "pingxingshijie-asset" are missing from openAIModels and thus openAIModelsMap;
    update the block that builds openAIModels to also append dto.OpenAIModels
    entries for each modelName in taskpxsj.ModelList (similar to the existing loop
    for taskkie.ModelList), using Object:"model", Created:1626777600 and OwnedBy:
    taskpxsj.ChannelName, so openAIModels, openAIModelsMap and channelId2Models
    remain consistent for constant.ChannelTypePingXingShiJie handling.

Nitpick comments:
In @controller/model_list_test.go:

  • Around line 214-220: The test TestKieModelsAppearInGlobalModelRegistry
    currently iterates over taskkie.ModelList but will silently succeed if that
    slice is empty; add a precondition assertion (e.g., require.NotEmpty with t and
    taskkie.ModelList) before the for-loop to fail fast when the source model list
    is empty, then keep the existing loop that checks openAIModelsMap entries and
    aiModel.OwnedBy.

In @docs/token-management-api-reference.md:

  • Line 76: Several HTTP endpoint code blocks (e.g., the block starting with "GET
    /api/token/") are missing language specifiers which causes markdownlint
    warnings; open each endpoint's fenced code block and add the language tag "http"
    immediately after the three backticks (e.g., change tohttp) for all
    listed endpoint blocks (lines referenced in the comment include the blocks for
    GET /api/token/ and the other 10 endpoint declarations). Ensure you update every
    endpoint code fence so all 11 HTTP endpoint blocks use ```http for proper syntax
    highlighting and linting.

In @model/task_result_url.go:

  • Around line 3-8: The import of encoding/json and use of the json.RawMessage
    type in model/task_result_url.go is acceptable because the function parameter
    uses json.RawMessage and the code uses common.Unmarshal rather than direct json
    calls; no change required—leave the import and the parameter type as-is, or if
    you prefer a stylistic refactor you may change the parameter to []byte and
    remove the encoding/json import, but do not modify the common.Unmarshal usage
    (refer to json.RawMessage and common.Unmarshal in the function signature to
    locate the code).

In @relay/channel/task/kie/adaptor.go:

  • Around line 312-328: When images are present but fall through the switch
    (i.e., modelName is not ModelSeedance2 or ModelHappyHorseImageToVideo) and
    getModelConfig(modelName).ImageKey is empty, explicitly handle that case instead
    of silently dropping images: update the block in the function that calls
    getModelConfig, requestImages and builds input to detect images > 0 &&
    cfg.ImageKey == "" and then either log a warning (including modelName and number
    of images) or return an error so callers know images were ignored; reference
    cfg.ImageKey, ModelSeedance2, ModelHappyHorseImageToVideo, requestImages,
    getModelConfig and createTaskRequest to locate where to add the check.

In @web/default/src/features/channels/lib/channel-utils.ts:

  • Around line 86-87: The helper in
    web/default/src/features/channels/lib/channel-utils.ts currently returns raw
    icon URLs while its docs and name indicate it returns icon "names"; update the
    helper contract so it is unambiguous: either (A) change the function name and
    JSDoc to reflect it returns icon URLs (e.g., rename to getIconUrl / update
    return type to string URL and update callers/tests), or (B) preserve the “name”
    contract by converting those URL entries (like
    'https://www.pingxingshijie.cn/favicon.ico' and 'https://kie.ai/logo.png') to
    icon identifiers and return only names (and map names to URLs in a separate
    resolver). Make the chosen change consistently in the function signature,
    exported type, JSDoc, and any callers in the repo.

In
@web/default/src/features/usage-logs/components/dialogs/task-media-results-dialog.tsx:

  • Around line 23-27: The copyUrl function currently writes to
    navigator.clipboard without handling failures; update copyUrl(url: string,
    successMessage: string) to catch promise rejections from
    navigator.clipboard.writeText and surface an error (e.g., toast.error with a
    helpful message) so clipboard permission/secure-context failures are not silent;
    keep the success toast on then() and add a .catch() (or try/catch if you convert
    to async/await) to log or toast the error and optionally console.error the
    caught error for debugging.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

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

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

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `ddaf82af-c5c3-43e2-b851-f34e1607c888`

</details>

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

Reviewing files that changed from the base of the PR and between dac55f0fdeb16bbbc2bdc472bda14e60431f3845 and e5d030799a7ed82444eb9032d8ca63bdccafeab2.

</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 (83)</summary>

* `.gitignore`
* `common/api_type.go`
* `constant/channel.go`
* `controller/channel-test.go`
* `controller/model.go`
* `controller/model_list_test.go`
* `controller/relay.go`
* `docs/pingxingshijie-api-reference.md`
* `docs/pingxingshijie-downstream-curl.md`
* `docs/pingxingshijie-openai-compatibility.md`
* `docs/token-management-api-reference.md`
* `dto/openai_request.go`
* `dto/task.go`
* `middleware/distributor.go`
* `middleware/distributor_test.go`
* `model/task.go`
* `model/task_result_url.go`
* `relay/channel/adapter.go`
* `relay/channel/claude/relay-claude.go`
* `relay/channel/task/ali/adaptor.go`
* `relay/channel/task/ali/constants.go`
* `relay/channel/task/doubao/adaptor.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/asset_model_validation_test.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/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/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-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/index.ts`
* `web/default/src/features/usage-logs/lib/task-action-label.test.ts`
* `web/default/src/features/usage-logs/lib/task-action-label.ts`
* `web/default/src/features/usage-logs/lib/task-media-results.ts`
* `web/default/src/features/usage-logs/types.ts`
* `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 on lines +125 to +128
// PingXingShiJie: sync image models use OpenAI image relay; text uses chat (Volc-style upstream via adaptor mapping).
if channel.Type == constant.ChannelTypePingXingShiJie && strings.Contains(strings.ToLower(testModel), "seedream") {
requestPath = "/v1/images/generations"
}

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

Keep endpoint type in sync when forcing PingXing image test path.

Line 125-128 switches requestPath to /v1/images/generations, but endpointType stays empty, so buildTestRequest(...) can still build a non-image request and later fail image conversion (invalid image request type).

🔧 Proposed fix
-		// VolcEngine 图像生成模型
-		if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
-			requestPath = "/v1/images/generations"
-		}
-
-		// PingXingShiJie: sync image models use OpenAI image relay; text uses chat (Volc-style upstream via adaptor mapping).
-		if channel.Type == constant.ChannelTypePingXingShiJie && strings.Contains(strings.ToLower(testModel), "seedream") {
-			requestPath = "/v1/images/generations"
-		}
+		// Seedream image models should use OpenAI image endpoint and image request payload.
+		lowerModel := strings.ToLower(testModel)
+		if strings.Contains(lowerModel, "seedream") &&
+			(channel.Type == constant.ChannelTypeVolcEngine || channel.Type == constant.ChannelTypePingXingShiJie) {
+			requestPath = "/v1/images/generations"
+			endpointType = string(constant.EndpointTypeImageGeneration)
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/channel-test.go` around lines 125 - 128, When forcing the
PingXingShiJie image test path you set requestPath but forgot to set
endpointType, causing buildTestRequest(...) to produce a non-image request;
update the branch that checks channel.Type == constant.ChannelTypePingXingShiJie
&& strings.Contains(strings.ToLower(testModel), "seedream") to also set
endpointType to the image type used by your request (e.g., the image/images
enum/value your code expects) so buildTestRequest(...) builds an image request
and image conversion won't fail.

Comment thread dto/openai_request.go
Comment on lines +395 to +397
if mimeType == "" && file.FileName != "" {
mimeType = "application/octet-stream"
}

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

Make the fallback MIME unconditional for file payloads.

When file.FileName is empty, MIME remains blank even though this branch already confirmed file data exists. Defaulting to application/octet-stream in all empty-MIME cases keeps behavior consistent.

Suggested patch
-		if mimeType == "" && file.FileName != "" {
+		if mimeType == "" {
 			mimeType = "application/octet-stream"
 		}
📝 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 mimeType == "" && file.FileName != "" {
mimeType = "application/octet-stream"
}
if mimeType == "" {
mimeType = "application/octet-stream"
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dto/openai_request.go` around lines 395 - 397, Change the conditional that
sets a fallback MIME so it does not depend on file.FileName: whenever mimeType
== "" and there is a file payload, set mimeType = "application/octet-stream"
regardless of file.FileName. Locate the logic that currently checks mimeType ==
"" && file.FileName != "" (using the mimeType and file.FileName variables) and
remove the file.FileName check so the fallback is unconditional for empty
mimeType on file payloads.

Comment on lines +32 to +39
func TestGetModelRequestAssetUploadRejectsBlankModel(t *testing.T) {
c := newJSONContext(http.MethodPost, "/v1/assets/upload", `{"image_url":"https://example.com/a.jpg","asset_type":"Image"}`)

_, _, err := getModelRequest(c)

if err == nil {
t.Fatalf("expected blank model to be rejected for /v1/assets/upload; required model is %q", taskpxsj.AssetPlaceholderModel)
}

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

Cover the explicit empty-string model case too.

This test currently exercises a missing model, not {"model":""}. That leaves a regression gap where the handler could still accept an explicit blank string and this suite would stay green. It also doesn't verify that the error mentions taskpxsj.AssetPlaceholderModel, which is part of the contract you described in the PR.

Suggested test tightening
 func TestGetModelRequestAssetUploadRejectsBlankModel(t *testing.T) {
-	c := newJSONContext(http.MethodPost, "/v1/assets/upload", `{"image_url":"https://example.com/a.jpg","asset_type":"Image"}`)
+	c := newJSONContext(http.MethodPost, "/v1/assets/upload", `{"model":"","image_url":"https://example.com/a.jpg","asset_type":"Image"}`)

 	_, _, err := getModelRequest(c)

 	if err == nil {
 		t.Fatalf("expected blank model to be rejected for /v1/assets/upload; required model is %q", taskpxsj.AssetPlaceholderModel)
 	}
+	if !strings.Contains(err.Error(), taskpxsj.AssetPlaceholderModel) {
+		t.Fatalf("expected error to mention required model %q, got %v", taskpxsj.AssetPlaceholderModel, err)
+	}
 }
📝 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
func TestGetModelRequestAssetUploadRejectsBlankModel(t *testing.T) {
c := newJSONContext(http.MethodPost, "/v1/assets/upload", `{"image_url":"https://example.com/a.jpg","asset_type":"Image"}`)
_, _, err := getModelRequest(c)
if err == nil {
t.Fatalf("expected blank model to be rejected for /v1/assets/upload; required model is %q", taskpxsj.AssetPlaceholderModel)
}
func TestGetModelRequestAssetUploadRejectsBlankModel(t *testing.T) {
c := newJSONContext(http.MethodPost, "/v1/assets/upload", `{"model":"","image_url":"https://example.com/a.jpg","asset_type":"Image"}`)
_, _, err := getModelRequest(c)
if err == nil {
t.Fatalf("expected blank model to be rejected for /v1/assets/upload; required model is %q", taskpxsj.AssetPlaceholderModel)
}
if !strings.Contains(err.Error(), taskpxsj.AssetPlaceholderModel) {
t.Fatalf("expected error to mention required model %q, got %v", taskpxsj.AssetPlaceholderModel, err)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/distributor_test.go` around lines 32 - 39, Update
TestGetModelRequestAssetUploadRejectsBlankModel to also exercise the explicit
empty-string model case by calling newJSONContext with a body that includes
`"model":""` (e.g.,
`{"model":"","image_url":"https://example.com/a.jpg","asset_type":"Image"}`),
call getModelRequest for that context, assert err is non-nil, and assert the
returned error message contains the expected placeholder constant
taskpxsj.AssetPlaceholderModel; keep the existing missing-model case as well so
both scenarios are covered.

Comment thread model/task_result_url.go
Comment on lines +37 to +55
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
}
for _, vv := range x {
if s := walkFirstImageLikeURL(vv); s != "" {
return s
}
}
case []any:
for _, item := range x {
if s := walkFirstImageLikeURL(item); s != "" {
return s
}
}
}
return ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

walkFirstImageLikeURL does not have a stable “first” URL.

Line 43 ranges over a map[string]any, so payloads with multiple image-like url fields can return different results across calls. That makes result previews nondeterministic. Please make the descent order explicit here instead of relying on map iteration.

One deterministic fix
+import "sort"
+
 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
 		}
-		for _, vv := range x {
-			if s := walkFirstImageLikeURL(vv); s != "" {
+		keys := make([]string, 0, len(x))
+		for k := range x {
+			keys = append(keys, k)
+		}
+		sort.Strings(keys)
+		for _, k := range keys {
+			if s := walkFirstImageLikeURL(x[k]); s != "" {
 				return s
 			}
 		}
 	case []any:
📝 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
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
}
for _, vv := range x {
if s := walkFirstImageLikeURL(vv); s != "" {
return s
}
}
case []any:
for _, item := range x {
if s := walkFirstImageLikeURL(item); s != "" {
return s
}
}
}
return ""
import "sort"
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
}
keys := make([]string, 0, len(x))
for k := range x {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if s := walkFirstImageLikeURL(x[k]); s != "" {
return s
}
}
case []any:
for _, item := range x {
if s := walkFirstImageLikeURL(item); s != "" {
return s
}
}
}
return ""
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/task_result_url.go` around lines 37 - 55, walkFirstImageLikeURL
currently iterates a map (map[string]any) which yields nondeterministic order;
change the map descent in walkFirstImageLikeURL so keys are collected, sorted
(e.g., lexicographically), and then iterated in that sorted order before
recursing into values so the "first" image-like url is stable across runs; keep
the existing url check (x["url"]) and recursion logic but replace the direct
range over the map with a deterministic sorted-key loop.

Comment thread model/task.go
Comment on lines +137 to +146
u := t.PrivateData.ResultURL
if u == "" {
u = t.FailReason
}
return t.FailReason
if u == "" {
return ""
}
if !isVideoProxyContentURL(u, t.TaskID) {
return u
}

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 expose data: fallbacks as result URLs.

GetResultURL() now returns the FailReason fallback verbatim whenever PrivateData.ResultURL is empty. If an older row still has a data: URL there, this pushes the full base64 payload back into API responses/logs instead of suppressing it.

Guard the fallback before returning it
 func (t *Task) GetResultURL() string {
 	u := t.PrivateData.ResultURL
 	if u == "" {
 		u = t.FailReason
 	}
+	u = strings.TrimSpace(u)
 	if u == "" {
 		return ""
 	}
+	if strings.HasPrefix(strings.ToLower(u), "data:") {
+		return ""
+	}
 	if !isVideoProxyContentURL(u, t.TaskID) {
 		return u
 	}

Based on learnings: In controller/task_video.go, data: URLs are intentionally prevented from being stored in task.FailReason, and the same suppression pattern should be applied consistently across the codebase.

📝 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
u := t.PrivateData.ResultURL
if u == "" {
u = t.FailReason
}
return t.FailReason
if u == "" {
return ""
}
if !isVideoProxyContentURL(u, t.TaskID) {
return u
}
u := t.PrivateData.ResultURL
if u == "" {
u = t.FailReason
}
u = strings.TrimSpace(u)
if u == "" {
return ""
}
if strings.HasPrefix(strings.ToLower(u), "data:") {
return ""
}
if !isVideoProxyContentURL(u, t.TaskID) {
return u
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/task.go` around lines 137 - 146, GetResultURL currently returns
t.FailReason verbatim when PrivateData.ResultURL is empty, which can leak data:
base64 payloads; change the logic in GetResultURL so that after selecting u :=
t.PrivateData.ResultURL or u = t.FailReason, you validate u with
isVideoProxyContentURL(u, t.TaskID) and if that function indicates a disallowed
data: URL, return an empty string instead of u; keep references to
t.PrivateData.ResultURL, t.FailReason, isVideoProxyContentURL, and TaskID so
reviewers can find and update the GetResultURL implementation accordingly.

Comment thread web/classic/package.json
Comment on lines +10 to +12
"@visactor/react-vchart": "^2.0.21",
"@visactor/vchart": "^2.0.21",
"@visactor/vchart-semi-theme": "^1.12.3",

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

npm view `@visactor/vchart`@2.0.21 version peerDependencies
npm view `@visactor/react-vchart`@2.0.21 version peerDependencies
npm view `@visactor/vchart-semi-theme`@1.12.3 version peerDependencies

Repository: QuantumNous/new-api

Length of output: 228


@visactor/vchart-semi-theme is incompatible with vchart 2.x and must be resolved before merge.

The @visactor/vchart-semi-theme@1.12.3 package declares a peerDependency on @visactor/vchart: >=1.10.4, which covers only 1.x versions. Using it with @visactor/vchart@2.0.21 violates this constraint and will cause theme registration to fail at runtime.

Options:

  • Upgrade @visactor/vchart-semi-theme to a version compatible with vchart 2.x, or
  • Downgrade @visactor/vchart and @visactor/react-vchart to 1.x if a compatible theme package exists
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/classic/package.json` around lines 10 - 12, The package.json lists
incompatible chart packages: `@visactor/vchart-semi-theme`@1.12.3 declares a
peerDependency for `@visactor/vchart` 1.x while the project depends on
`@visactor/vchart`@2.0.21 and `@visactor/react-vchart`@2.0.21; resolve by either
upgrading `@visactor/vchart-semi-theme` to a release that supports vchart 2.x
(replace the dependency entry for "@visactor/vchart-semi-theme" with the
compatible version) or by downgrading "@visactor/vchart" and
"@visactor/react-vchart" to matching 1.x versions (update both entries) and
ensure package-lock/yarn.lock is regenerated and CI installs succeed.

Comment on lines 39 to +48
useEffect(() => {
if (isModalOpen && isVideo) {
setVideoError(false);
setIsLoading(true);
}
}, [isModalOpen, isVideo]);
if (isModalOpen && isImage) {
setImageError(false);
setIsLoading(true);
}
}, [isModalOpen, isVideo, isImage]);

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

Reset preview state when modalContent changes.

The effect never reruns for a new URL while the modal stays open. If one preview hits imageError/videoError, then the parent swaps modalContent, the fallback branch sticks and the next asset never gets a chance to load.

Small fix
-  }, [isModalOpen, isVideo, isImage]);
+  }, [isModalOpen, isVideo, isImage, modalContent]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/classic/src/components/table/task-logs/modals/ContentModal.jsx` around
lines 39 - 48, The effect that resets preview state only depends on isModalOpen,
isVideo, and isImage, so it won't rerun when the parent swaps the URL
(modalContent) while the modal remains open; update the dependency array of the
useEffect that sets setVideoError(false), setImageError(false), and
setIsLoading(true) to also include modalContent (or the prop that carries the
new URL) so the effect runs whenever modalContent changes and the preview state
is properly reset when a new asset is loaded.

Comment on lines +9 to +14
if (log.upstream_kind === 'asset') {
return 'Asset Upload'
}
if (log.upstream_kind === 'image') {
return 'Image Generation'
}

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

Replace hardcoded task labels with i18n keys.

These returned strings are user-facing and bypass localization. Return translation keys here (and translate at render) to keep usage logs multilingual.

🌐 Suggested adjustment
 export function getTaskActionLabel(
   log: TaskActionLabelInput,
   fallbackLabel: string
 ): string {
   if (log.upstream_kind === 'asset') {
-    return 'Asset Upload'
+    return 'usageLogs.taskAction.assetUpload'
   }
   if (log.upstream_kind === 'image') {
-    return 'Image Generation'
+    return 'usageLogs.taskAction.imageGeneration'
   }
   return fallbackLabel
 }

As per coding guidelines: web/default/src/**: “Frontend i18n … use useTranslation() hook and call t('English key') in components”, and web/default/**/*.{ts,tsx}: “All user-facing text must support i18n using … t()”.

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

In `@web/default/src/features/usage-logs/lib/task-action-label.ts` around lines 9
- 14, The function in task-action-label.ts currently returns hardcoded
user-facing strings for log.upstream_kind ('asset' -> 'Asset Upload', 'image' ->
'Image Generation'); replace those literal returns with stable i18n keys (e.g.
'usage.task.assetUpload', 'usage.task.imageGeneration') so the function returns
keys only, and update consuming components to call useTranslation() and
t(returnedKey) at render time; update any tests/fixtures to expect keys instead
of English text and add the new keys to the locale files.

Comment on lines +68 to +74
function isStaleImageProxyUrl(url: string, source: TaskMediaSource): boolean {
return (
source.upstream_kind === 'image' &&
url.includes('/v1/videos/') &&
url.includes('/content')
)
}

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

Treat upstream_kind: 'asset' as image-like here.

The backend now distinguishes video | image | asset, but this extractor only special-cases 'image' and 'video'. For successful asset uploads, extension-less or proxy-shaped URLs can still fall through to the video heuristics, which means the new usage-log preview can show the wrong icon/dialog or skip rendering entirely.

Localized fix
 function isStaleImageProxyUrl(url: string, source: TaskMediaSource): boolean {
   return (
-    source.upstream_kind === 'image' &&
+    (source.upstream_kind === 'image' || source.upstream_kind === 'asset') &&
     url.includes('/v1/videos/') &&
     url.includes('/content')
   )
 }
@@
   if (allowTaskFallback) {
-    if (source.upstream_kind === 'image') return 'image'
+    if (source.upstream_kind === 'image' || source.upstream_kind === 'asset') {
+      return 'image'
+    }
     if (source.upstream_kind === 'video') return 'video'
     if (VIDEO_ACTIONS.has(source.action)) return 'video'
   }

Also applies to: 97-101

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

In `@web/default/src/features/usage-logs/lib/task-media-results.ts` around lines
68 - 74, Update the image-proxy heuristic to treat upstream_kind === 'asset' the
same as 'image': in isStaleImageProxyUrl (and the other predicate that similarly
checks source.upstream_kind for image/video heuristics later in the file),
include source.upstream_kind === 'asset' alongside 'image' so asset uploads use
the image-path logic and won't be misclassified as video or skipped.

Comment on lines +117 to +121
if (
isTaskVideoProxyUrl(url, source) &&
results.some((result) => result.type === 'image')
) {
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

A stale task-proxy URL can still win over the real image.

extractTaskMediaResults() seeds result_url/fail_reason before it walks data, but the /v1/videos/{task_id}/content filter only suppresses that proxy once an image is already present. For legacy rows where the seeded URL is the stale proxy and the actual image only exists inside data, you keep the wrong first entry and the dialog opens the broken target.

A safer approach is to defer task-proxy fallbacks until after walkTaskData(), or to drop any seeded task-proxy entry once a concrete image result is discovered.

Also applies to: 183-185

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

In `@web/default/src/features/usage-logs/lib/task-media-results.ts` around lines
117 - 121, extractTaskMediaResults currently seeds a task-proxy result (using
isTaskVideoProxyUrl) before calling walkTaskData, which lets a stale proxy win
even when a real image exists inside data; change the logic to treat proxy
entries as fallbacks: either (A) do not push the seeded proxy into results
up-front — instead keep it in a local fallbackCandidate and only append it after
walkTaskData if no concrete image result was produced, or (B) after walkTaskData
completes, scan results and remove any entries whose URL is a task-proxy
(isTaskVideoProxyUrl(url, source)) whenever a concrete image result exists;
apply the same change to the analogous block referenced at lines 183-185 so
proxy fallbacks cannot override real images.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
relay/channel/task/pingxingshijie/asset_request_body_test.go (1)

24-31: ⚡ Quick win

Prefer structural JSON assertions over raw substring matching in this test.

strings.Contains(string(raw), "model") can become brittle with unrelated content. Decode once and assert key presence/absence directly.

💡 Suggested refactor
 import (
 	"io"
-	"strings"
 	"testing"
 
+	"github.com/QuantumNous/new-api/common"
 	relaycommon "github.com/QuantumNous/new-api/relay/common"
 )
@@
-	if strings.Contains(string(raw), "model") {
-		t.Fatalf("asset upstream body must not include gateway-only model: %s", string(raw))
-	}
-	if !strings.Contains(string(raw), `"image_url":"https://example.com/a.jpg"`) {
-		t.Fatalf("asset upstream body lost image_url: %s", string(raw))
-	}
-	if !strings.Contains(string(raw), `"asset_type":"Image"`) {
-		t.Fatalf("asset upstream body lost asset_type: %s", string(raw))
-	}
+	var got map[string]any
+	if err := common.Unmarshal(raw, &got); err != nil {
+		t.Fatalf("unmarshal upstream body failed: %v, raw=%s", err, string(raw))
+	}
+	if _, ok := got["model"]; ok {
+		t.Fatalf("asset upstream body must not include gateway-only model: %s", string(raw))
+	}
+	if got["image_url"] != "https://example.com/a.jpg" {
+		t.Fatalf("asset upstream body lost/changed image_url: %s", string(raw))
+	}
+	if got["asset_type"] != "Image" {
+		t.Fatalf("asset upstream body lost/changed asset_type: %s", string(raw))
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/task/pingxingshijie/asset_request_body_test.go` around lines 24
- 31, Replace brittle substring checks that use strings.Contains on raw with a
single JSON decode of raw into a map[string]interface{} (e.g., var body
map[string]interface{}; json.Unmarshal(raw, &body)), then assert the top-level
"model" key is absent (check _, ok := body["model"]; if ok { t.Fatalf(...) })
and assert body["image_url"] equals "https://example.com/a.jpg" and
body["asset_type"] equals "Image" using type assertions for string values;
update the error messages to include the marshalled body when a check fails to
aid debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/pingxingshijie-api-reference.md`:
- Around line 22-23: The current sentence implies every /v1 route uses Token 认证
+ Distribute; narrow it to only task submission/query routes by updating the doc
text so it states that routes under router/video-router.go related to task
submit/query (e.g., routes handling video task creation and status/query) use
TokenAuth + Distribute, and explicitly call out the exception
/v1/videos/:task_id/content which uses TokenOrUserAuth; reference
router/video-router.go and the /v1/videos/:task_id/content route and mention
TokenOrUserAuth vs TokenAuth + Distribute to avoid confusion.

In `@relay/channel/task/pingxingshijie/adaptor.go`:
- Around line 802-807: The success-case sets tr.Status/tr.Progress but only
reads URL from the top-level map m, which loses URLs when upstream embeds them
under the Result object; update the success branch (the switch/case block that
sets tr.Status = model.TaskStatusSuccess and tr.Progress = "100%") to attempt
extraction from the Result/result map first (the same object used to derive
status) and fall back to extractStringFromMap(m, "url", "asset_url", "AssetUrl")
if not found, then assign that value to tr.Url so successful tasks pick up URLs
returned inside Result.

---

Nitpick comments:
In `@relay/channel/task/pingxingshijie/asset_request_body_test.go`:
- Around line 24-31: Replace brittle substring checks that use strings.Contains
on raw with a single JSON decode of raw into a map[string]interface{} (e.g., var
body map[string]interface{}; json.Unmarshal(raw, &body)), then assert the
top-level "model" key is absent (check _, ok := body["model"]; if ok {
t.Fatalf(...) }) and assert body["image_url"] equals "https://example.com/a.jpg"
and body["asset_type"] equals "Image" using type assertions for string values;
update the error messages to include the marshalled body when a check fails to
aid debugging.
🪄 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: 18ccb10a-03cc-49a1-8a23-2e53a811f83a

📥 Commits

Reviewing files that changed from the base of the PR and between e5d0307 and a0abb43.

📒 Files selected for processing (3)
  • docs/pingxingshijie-api-reference.md
  • relay/channel/task/pingxingshijie/adaptor.go
  • relay/channel/task/pingxingshijie/asset_request_body_test.go

Comment on lines +22 to +23
**说明**:下列路由挂在 `router/video-router.go` 的 `/v1` 分组上,使用 **Token 认证** + **Distribute(按模型选渠道)**。调用方必须在渠道中配置 **渠道类型 58(PingXingShiJie)**,且令牌对该渠道上的 **model** 有权限。

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

Clarify the scope of auth/distribution statement.

Line 22 currently reads like all /v1 routes in this doc use TokenAuth + Distribute, but /v1/videos/:task_id/content is an exception (TokenOrUserAuth). Narrow this sentence to task submit/query routes to avoid confusion.

✏️ Suggested wording
-**说明**:下列路由挂在 `router/video-router.go` 的 `/v1` 分组上,使用 **Token 认证** + **Distribute(按模型选渠道)**。调用方必须在渠道中配置 **渠道类型 58(PingXingShiJie)**,且令牌对该渠道上的 **model** 有权限。
+**说明**:本文档中的**任务提交/查询路由**挂在 `router/video-router.go` 的 `/v1` 分组上,使用 **Token 认证** + **Distribute(按模型选渠道)**。调用方必须在渠道中配置 **渠道类型 58(PingXingShiJie)**,且令牌对该渠道上的 **model** 有权限。  
+`/v1/videos/:task_id/content` 为例外,使用 `TokenOrUserAuth`(见第 7 节)。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/pingxingshijie-api-reference.md` around lines 22 - 23, The current
sentence implies every /v1 route uses Token 认证 + Distribute; narrow it to only
task submission/query routes by updating the doc text so it states that routes
under router/video-router.go related to task submit/query (e.g., routes handling
video task creation and status/query) use TokenAuth + Distribute, and explicitly
call out the exception /v1/videos/:task_id/content which uses TokenOrUserAuth;
reference router/video-router.go and the /v1/videos/:task_id/content route and
mention TokenOrUserAuth vs TokenAuth + Distribute to avoid confusion.

Comment on lines +802 to +807
case "active", "succeeded", "success", "completed":
tr.Status = model.TaskStatusSuccess
tr.Progress = "100%"
if u := extractStringFromMap(m, "url", "asset_url", "AssetUrl"); u != "" {
tr.Url = u
}

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

Successful asset status may lose URL because extraction reads the wrong object.

On Line 805, status is derived from res (Result/result), but URL is read only from top-level m. If upstream returns URL inside Result, TaskInfo.Url stays empty even on success.

💡 Suggested fix
 	case "active", "succeeded", "success", "completed":
 		tr.Status = model.TaskStatusSuccess
 		tr.Progress = "100%"
-		if u := extractStringFromMap(m, "url", "asset_url", "AssetUrl"); u != "" {
+		if u := extractStringFromMap(res, "url", "Url", "asset_url", "AssetUrl", "assetUrl"); u != "" {
+			tr.Url = u
+		} else if u := extractStringFromMap(m, "url", "Url", "asset_url", "AssetUrl", "assetUrl"); u != "" {
 			tr.Url = u
 		}
📝 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
case "active", "succeeded", "success", "completed":
tr.Status = model.TaskStatusSuccess
tr.Progress = "100%"
if u := extractStringFromMap(m, "url", "asset_url", "AssetUrl"); u != "" {
tr.Url = u
}
case "active", "succeeded", "success", "completed":
tr.Status = model.TaskStatusSuccess
tr.Progress = "100%"
if u := extractStringFromMap(res, "url", "Url", "asset_url", "AssetUrl", "assetUrl"); u != "" {
tr.Url = u
} else if u := extractStringFromMap(m, "url", "Url", "asset_url", "AssetUrl", "assetUrl"); u != "" {
tr.Url = u
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/task/pingxingshijie/adaptor.go` around lines 802 - 807, The
success-case sets tr.Status/tr.Progress but only reads URL from the top-level
map m, which loses URLs when upstream embeds them under the Result object;
update the success branch (the switch/case block that sets tr.Status =
model.TaskStatusSuccess and tr.Progress = "100%") to attempt extraction from the
Result/result map first (the same object used to derive status) and fall back to
extractStringFromMap(m, "url", "asset_url", "AssetUrl") if not found, then
assign that value to tr.Url so successful tasks pick up URLs returned inside
Result.

Fankouzu and others added 6 commits May 1, 2026 15:26
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>
…et_id

- Add GetByUpstreamTaskId to query tasks by private_data.upstream_task_id
- Add getTaskForVideoFetch to prefer upstream_task_id for /v1/assets/ routes
- Rename route param :task_id to :asset_id for asset fetch
- Fallback to public task_id for backward compatibility
- Add regression tests for upstream-task-id lookup

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Change GET /v1/assets/:task_id to GET /v1/assets/:asset_id
- Document fallback to public task_id for legacy clients
- Update route summary table and compatibility notes

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
relay/relay_task.go (1)

502-519: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Realtime fetch should reuse the task-scoped API key.

This follow-up call uses channelModel.Key, even though Gemini/Vertex tasks persist the effective key in task.PrivateData.Key at submit time. If the channel rotates keys or the task was created with a different key, realtime fetch can start polling with the wrong credentials.

Suggested fix
-	resp, err := adaptor.FetchTask(baseURL, channelModel.Key, ft, proxy)
+	apiKey := channelModel.Key
+	if task.PrivateData.Key != "" {
+		apiKey = task.PrivateData.Key
+	}
+	resp, err := adaptor.FetchTask(baseURL, apiKey, ft, proxy)

Based on learnings: follow-up requests should reuse the effective authentication context captured at task submit time, preferring task.PrivateData.Key over channel.Key.

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

In `@relay/relay_task.go` around lines 502 - 519, The realtime fetch uses
channelModel.Key but should prefer the task-scoped effective API key stored on
the task; change the call that builds credentials for adaptor.FetchTask to use
task.PrivateData.Key if non-empty, falling back to
channelModel.GetKey()/channelModel.Key otherwise (keep baseURL resolution and
proxy retrieval as-is); update the local variable currently passed
(channelModel.Key) to the effectiveKey (check task.PrivateData.Key first) before
calling GetTaskAdaptor(...).FetchTask so follow-up polling uses the same auth
context captured at submit time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/relay_task_test.go`:
- Around line 47-54: The test sets model.DB and common.UsingSQLite but doesn't
clear other DB engine flags, making GetByUpstreamTaskId() behavior
order-dependent; update the setup to save and restore common.UsingPostgreSQL and
common.UsingMySQL (alongside common.UsingSQLite and model.DB) and in the test
set only common.UsingSQLite = true and the others = false, then restore all
saved flags in the t.Cleanup closure so GetByUpstreamTaskId() will reliably take
the SQLite branch.

In `@relay/relay_task.go`:
- Around line 292-299: otherRatiosProduct is nondeterministic because it
multiplies values by iterating a map; to avoid floating-point rounding
differences that change int-truncated quotas, collect the values from ratios (or
keys), sort the numeric values in a deterministic order (e.g., ascending), then
multiply them in that sorted order and return the product; update the function
otherRatiosProduct to build a slice of ra values, sort.Float64s on that slice,
and multiply only those >0 and !=1.0 so the multiplication order is stable
across runs.

---

Outside diff comments:
In `@relay/relay_task.go`:
- Around line 502-519: The realtime fetch uses channelModel.Key but should
prefer the task-scoped effective API key stored on the task; change the call
that builds credentials for adaptor.FetchTask to use task.PrivateData.Key if
non-empty, falling back to channelModel.GetKey()/channelModel.Key otherwise
(keep baseURL resolution and proxy retrieval as-is); update the local variable
currently passed (channelModel.Key) to the effectiveKey (check
task.PrivateData.Key first) before calling GetTaskAdaptor(...).FetchTask so
follow-up polling uses the same auth context captured at submit time.
🪄 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: 7f4761e9-850d-4431-a7db-eb2f9156e90d

📥 Commits

Reviewing files that changed from the base of the PR and between 37c7e49 and 60f42c2.

📒 Files selected for processing (8)
  • docs/pingxingshijie-api-reference.md
  • docs/pingxingshijie-openai-compatibility.md
  • model/task.go
  • model/task_cas_test.go
  • relay/channel/adapter.go
  • relay/relay_task.go
  • relay/relay_task_test.go
  • router/video-router.go
✅ Files skipped from review due to trivial changes (1)
  • relay/channel/adapter.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • router/video-router.go

Comment thread relay/relay_task_test.go
Comment on lines +47 to +54
oldDB := model.DB
oldUsingSQLite := common.UsingSQLite
model.DB = db
common.UsingSQLite = true
t.Cleanup(func() {
model.DB = oldDB
common.UsingSQLite = oldUsingSQLite
})

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

Reset all DB-engine flags in this SQLite test.

GetByUpstreamTaskId() checks common.UsingPostgreSQL and common.UsingMySQL before the SQLite branch. Only setting common.UsingSQLite = true makes this test order-dependent if another test left one of the other flags enabled.

Suggested fix
 	oldDB := model.DB
 	oldUsingSQLite := common.UsingSQLite
+	oldUsingMySQL := common.UsingMySQL
+	oldUsingPostgreSQL := common.UsingPostgreSQL
 	model.DB = db
 	common.UsingSQLite = true
+	common.UsingMySQL = false
+	common.UsingPostgreSQL = false
 	t.Cleanup(func() {
 		model.DB = oldDB
 		common.UsingSQLite = oldUsingSQLite
+		common.UsingMySQL = oldUsingMySQL
+		common.UsingPostgreSQL = oldUsingPostgreSQL
 	})
📝 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
oldDB := model.DB
oldUsingSQLite := common.UsingSQLite
model.DB = db
common.UsingSQLite = true
t.Cleanup(func() {
model.DB = oldDB
common.UsingSQLite = oldUsingSQLite
})
oldDB := model.DB
oldUsingSQLite := common.UsingSQLite
oldUsingMySQL := common.UsingMySQL
oldUsingPostgreSQL := common.UsingPostgreSQL
model.DB = db
common.UsingSQLite = true
common.UsingMySQL = false
common.UsingPostgreSQL = false
t.Cleanup(func() {
model.DB = oldDB
common.UsingSQLite = oldUsingSQLite
common.UsingMySQL = oldUsingMySQL
common.UsingPostgreSQL = oldUsingPostgreSQL
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/relay_task_test.go` around lines 47 - 54, The test sets model.DB and
common.UsingSQLite but doesn't clear other DB engine flags, making
GetByUpstreamTaskId() behavior order-dependent; update the setup to save and
restore common.UsingPostgreSQL and common.UsingMySQL (alongside
common.UsingSQLite and model.DB) and in the test set only common.UsingSQLite =
true and the others = false, then restore all saved flags in the t.Cleanup
closure so GetByUpstreamTaskId() will reliably take the SQLite branch.

Comment thread relay/relay_task.go
Comment on lines +292 to +299
func otherRatiosProduct(ratios map[string]float64) float64 {
multiplier := 1.0
for _, ra := range ratios {
if ra != 1.0 {
result *= ra
if ra != 1.0 && ra > 0 {
multiplier *= ra
}
}
return int(result)
return multiplier

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

Make ratio multiplication deterministic before truncating.

ratios is a map, so otherRatiosProduct() can multiply the same factors in different orders. With floating-point math and the later int() cast, that can shift the computed quota by 1 on boundary cases.

Suggested fix
+import "sort"
...
 func otherRatiosProduct(ratios map[string]float64) float64 {
+	keys := make([]string, 0, len(ratios))
+	for k := range ratios {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
 	multiplier := 1.0
-	for _, ra := range ratios {
+	for _, k := range keys {
+		ra := ratios[k]
 		if ra != 1.0 && ra > 0 {
 			multiplier *= ra
 		}
 	}
 	return multiplier
 }
📝 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
func otherRatiosProduct(ratios map[string]float64) float64 {
multiplier := 1.0
for _, ra := range ratios {
if ra != 1.0 {
result *= ra
if ra != 1.0 && ra > 0 {
multiplier *= ra
}
}
return int(result)
return multiplier
func otherRatiosProduct(ratios map[string]float64) float64 {
keys := make([]string, 0, len(ratios))
for k := range ratios {
keys = append(keys, k)
}
sort.Strings(keys)
multiplier := 1.0
for _, k := range keys {
ra := ratios[k]
if ra != 1.0 && ra > 0 {
multiplier *= ra
}
}
return multiplier
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/relay_task.go` around lines 292 - 299, otherRatiosProduct is
nondeterministic because it multiplies values by iterating a map; to avoid
floating-point rounding differences that change int-truncated quotas, collect
the values from ratios (or keys), sort the numeric values in a deterministic
order (e.g., ascending), then multiply them in that sorted order and return the
product; update the function otherRatiosProduct to build a slice of ra values,
sort.Float64s on that slice, and multiply only those >0 and !=1.0 so the
multiplication order is stable across runs.

@Fankouzu Fankouzu closed this May 1, 2026
@Fankouzu
Fankouzu deleted the fix/pingxing-asset-upload-model branch May 1, 2026 09:39
@Fankouzu
Fankouzu restored the fix/pingxing-asset-upload-model branch May 24, 2026 05:04
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