Skip to content

feat: vertex veo (#1450) - #1659

Merged
seefs001 merged 2 commits into
QuantumNous:alphafrom
Sh1n3zZ:feat-vertex-veo
Sep 13, 2025
Merged

feat: vertex veo (#1450)#1659
seefs001 merged 2 commits into
QuantumNous:alphafrom
Sh1n3zZ:feat-vertex-veo

Conversation

@Sh1n3zZ

@Sh1n3zZ Sh1n3zZ commented Aug 26, 2025

Copy link
Copy Markdown
Contributor
  1. 支持 vertex 渠道文生视频,返回格式 bytesBase64Encoded
    2. 使用正则简单匹配 mysql 数据库格式并向用户脱敏(感觉不是很好的实践,但是目前没有想到更好的办法)
    删掉了,这个 pr 现在只包含对于 vertex 渠道 veo 的适配

Summary by CodeRabbit

  • New Features

    • Added Vertex AI video generation support with full submit and fetch flows; live task fetching now integrates upstream status and results.
    • New token acquisition helper for Vertex service accounts.
  • Bug Fixes

    • Safer region fallback for Vertex AI models.
    • Ignore data: URLs when determining task failure reasons.
    • Sanitized video responses to truncate/remove large base64 payloads.
  • Chores

    • Adjusted request handling for video endpoints (POST vs GET) and Google Cloud project headers.

@coderabbitai

coderabbitai Bot commented Aug 26, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds Vertex AI video task support (new TaskAdaptor and channel integration), adjusts middleware routing for /v1/video/generations (POST vs GET), sanitizes stored video task responses, expands Vertex token acquisition and headers, and includes minor formatting/no-op edits.

Changes

Cohort / File(s) Summary
Vertex AI Task Adaptor (new)
relay/channel/task/vertex/adaptor.go
Adds a TaskAdaptor implementing Vertex long-running prediction workflow: request validation, URL/header/body construction, DoRequest/DoResponse, task ID encoding/decoding, FetchTask and ParseTaskResult, plus GetModelList/GetChannelName.
Vertex Channel Core Updates
relay/channel/vertex/adaptor.go, relay/channel/vertex/relay-vertex.go, relay/channel/vertex/service_account.go
Adds x-goog-user-project header; makes GetModelRegion fallback-safe ("global"); introduces AcquireAccessToken(creds, proxy) and proxy-aware JWT exchange helper.
Relay Integration and Fetch Logic
relay/relay_adaptor.go, relay/relay_task.go
Registers Vertex adaptor and task adaptor cases; RelayTaskSubmit calls InitChannelMeta twice (added second call); RelayTaskFetch adds default empty JSON response and implements Vertex-aware fetch path that queries Vertex, updates origin task, maps statuses/formats, and returns normalized payload with fallback to local snapshot.
Middleware Routing for /v1/video/generations
middleware/distributor.go
Removes unconditional body unmarshal; POST unmarshals body and sets VideoSubmit, GET sets VideoFetchByID and skips channel selection; sets relay_mode only if absent.
Video Task Data Sanitization
controller/task_video.go
Replaces storing raw response with redactVideoResponseBody(responseBody) to remove bytesBase64Encoded and truncate base64 fields to 256 chars; treats data: URLs as non-failure (do not set FailReason).
Minor / No-op Formatting
common/database.go, controller/setup.go, main.go
Trailing-newline/format-only edits; no behavioral/API changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant API as API Server
  participant MW as Middleware (/v1/video/generations)
  participant Relay as Relay Router
  participant VTask as Vertex TaskAdaptor
  participant Vertex as Vertex AI

  rect rgb(240,248,255)
    Note over Client,API: Submit video generation (POST)
    Client->>API: POST /v1/video/generations {prompt,...}
    API->>MW: Route request
    MW->>Relay: relay_mode=VideoSubmit
    Relay->>VTask: Validate + Build URL/Header/Body
    VTask->>Vertex: predictLongRunning (auth, project header)
    Vertex-->>VTask: operation{name}
    VTask-->>Relay: task_id (b64), raw payload
    Relay-->>API: Enqueue/record task
    API-->>Client: {task_id,...}
  end
Loading
sequenceDiagram
  autonumber
  participant Client
  participant API as API Server
  participant MW as Middleware
  participant Relay as Relay Router
  participant VTask as Vertex TaskAdaptor
  participant DB as Task Store
  participant Vertex as Vertex AI

  rect rgb(245,255,250)
    Note over Client,API: Fetch by task_id (GET)
    Client->>API: GET /v1/video/generations?task_id=...
    API->>MW: Route request
    MW->>Relay: relay_mode=VideoFetchByID
    Relay->>DB: Load origin task
    alt Channel is Vertex AI
      Relay->>VTask: FetchTask(baseURL,key,{task_id,action})
      VTask->>Vertex: operations:get (auth)
      Vertex-->>VTask: status/result
      VTask-->>Relay: ParseTaskResult(status,url/mime,error)
      Relay->>DB: Update status/progress/failReason
      Relay-->>API: Normalized response {status,format,url,...}
    else Other channel or failure
      Relay-->>API: Fallback response (task snapshot)
    end
    API-->>Client: dto.TaskResponse
  end
Loading
sequenceDiagram
  autonumber
  participant Worker as Task Updater
  participant CTV as controller/task_video
  Note over Worker,CTV: On task update
  Worker->>CTV: updateVideoSingleTask(responseBody)
  CTV->>CTV: redactVideoResponseBody() remove bytesBase64Encoded, truncate base64
  CTV-->>Worker: store sanitized task.Data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • xyfacai

Poem

I thump my paws—new Vertex roads,
A task hops off where long-run flows.
Bits trimmed small, base64 tucked tight,
Headers set, regions checked just right.
I fetch by ID and nibble the prize—🥕

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81e29aa and 6a34d36.

📒 Files selected for processing (2)
  • main.go (1 hunks)
  • relay/relay_task.go (4 hunks)

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (3)
middleware/distributor.go (1)

169-179: Prevent token model-limit check from blocking GET video fetches

The current implementation applies the token model-limit check (lines 55–73 in middleware/distributor.go) unconditionally—before consulting shouldSelectChannel. Since shouldSelectChannel is set to false for GET fetches, a missing or mismatched model can still trigger a 403 on valid fetch requests.

To fix this, wrap the existing model-limit block inside a shouldSelectChannel guard:

• File: middleware/distributor.go
• Around line 55:

-    modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled)
-    if modelLimitEnable {
-        // existing model-limit logic...
-    }
+    if shouldSelectChannel {
+        modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled)
+        if modelLimitEnable {
+            // existing model-limit logic...
+        }
+    }

This ensures GET-based fetches (where shouldSelectChannel == false) skip token model-limit enforcement.

relay/channel/vertex/adaptor.go (2)

88-131: Region resolved before model normalization can select the wrong endpoint.

You compute region using info.UpstreamModelName before stripping -thinking/-nothinking and before model aliasing in the Claude path. If ApiVersion carries a per-model region map, keys won’t match the unnormalized model and you may fall back to "global" unexpectedly. This can break routing and produce 404s.

Apply this refactor to resolve region after normalization per mode:

-   region := GetModelRegion(info.ApiVersion, info.UpstreamModelName)
-   a.AccountCredentials = *adc
+   a.AccountCredentials = *adc
+   var region string
    suffix := ""
    if a.RequestMode == RequestModeGemini {
 
       if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
         // 新增逻辑:处理 -thinking-<budget> 格式
         if strings.Contains(info.UpstreamModelName, "-thinking-") {
           parts := strings.Split(info.UpstreamModelName, "-thinking-")
           info.UpstreamModelName = parts[0]
         } else if strings.HasSuffix(info.UpstreamModelName, "-thinking") { // 旧的适配
           info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-thinking")
         } else if strings.HasSuffix(info.UpstreamModelName, "-nothinking") {
           info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-nothinking")
         }
       }
+      // resolve region AFTER normalization
+      region = GetModelRegion(info.ApiVersion, info.UpstreamModelName)
 
       if info.IsStream {
         suffix = "streamGenerateContent?alt=sse"
       } else {
         suffix = "generateContent"
       }

And in the Claude path:

-   } else if a.RequestMode == RequestModeClaude {
+   } else if a.RequestMode == RequestModeClaude {
       if info.IsStream {
         suffix = "streamRawPredict?alt=sse"
       } else {
         suffix = "rawPredict"
       }
       model := info.UpstreamModelName
       if v, ok := claudeModelMap[info.UpstreamModelName]; ok {
         model = v
       }
+      // resolve with the actual request model
+      region = GetModelRegion(info.ApiVersion, model)

Finally, handle Llama below (see next comment) to ensure correct region usage.


---

`160-166`: **Llama URL builder fails for region "global".**

`https://global-aiplatform.googleapis.com` is invalid; other branches special-case `"global"` (no region prefix). Add the same handling here to avoid DNS failures when the map resolves to global.

Apply:

```diff
-  } else if a.RequestMode == RequestModeLlama {
-    return fmt.Sprintf(
-      "https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
-      region,
-      adc.ProjectID,
-      region,
-    ), nil
+  } else if a.RequestMode == RequestModeLlama {
+    region = GetModelRegion(info.ApiVersion, info.UpstreamModelName)
+    if region == "global" {
+      return fmt.Sprintf(
+        "https://aiplatform.googleapis.com/v1beta1/projects/%s/locations/global/endpoints/openapi/chat/completions",
+        adc.ProjectID,
+      ), nil
+    }
+    return fmt.Sprintf(
+      "https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
+      region,
+      adc.ProjectID,
+      region,
+    ), nil
   }
🧹 Nitpick comments (17)
relay/channel/vertex/relay-vertex.go (1)

15-19: Type-assert defensively to avoid panics on non-string config values.

Both localModelName and default lookups use direct . (string) assertions. If config JSON isn’t strictly string-typed, this panics. Add safe assertions.

-        if v, ok := m["default"]; ok {
-            return v.(string)
-        }
+        if v, ok := m["default"]; ok {
+            if s, ok := v.(string); ok && s != "" {
+                return s
+            }
+        }
         return "global"

Optional: apply the same guard for m[localModelName].

controller/task_video.go (3)

116-123: Avoid stuffing data URLs into FailReason on success.

Using FailReason for URLs is odd, but ignoring data: here prevents oversized records and log noise. Consider a dedicated field for “result URL” in the model to avoid overloading FailReason.


152-176: Redactor is effective; consider broader shapes and a size guard.

  • Current logic strips bytesBase64Encoded at response/ and within response.videos[]. Good.
  • If future providers nest under other keys (e.g., predictions[], outputs[]), consider a shallow recursive walk for keys named bytesBase64Encoded.
  • Optional: if marshalled size still > N (e.g., 256 KB), drop the remaining base64 fields as a hard cap.

178-184: Base64 truncation is safe; consider making max length configurable.

Base64 is ASCII-only, so byte slicing is fine. Expose maxKeep via config or const at top for easier tuning during ops.

relay/channel/vertex/service_account.go (1)

150-182: Duplicate token-exchange logic; add timeout and unify paths.

  • exchangeJwtForAccessTokenWithProxy duplicates exchangeJwtForAccessToken. Prefer a single helper that accepts an http.Client to avoid drift.
  • Add a request timeout; PostForm on a client without timeouts risks hangs if the proxy/endpoint stalls.

Example refactor within this range:

- resp, err := client.PostForm(authURL, data)
+ req, err := http.NewRequest(http.MethodPost, authURL, strings.NewReader(data.Encode()))
+ if err != nil { return "", err }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ // optional: context deadline
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ req = req.WithContext(ctx)
+ resp, err := client.Do(req)

Optional: cache tokens per (clientEmail, key fingerprint, proxy) similar to the existing asynccache flow to reduce auth churn.

controller/log.go (1)

88-92: Nit: use http.StatusOK and/or ApiError for consistency.

Elsewhere in this file and project we use http.StatusOK and common.ApiError(...). Consider aligning for uniformity and future changes centralized in ApiError.

Apply:

-    c.JSON(200, gin.H{
+    c.JSON(http.StatusOK, gin.H{
       "success": false,
       "message": common.MaskDatabaseSensitiveInfo(err.Error()),
     })

Or:

-    c.JSON(http.StatusOK, gin.H{
-      "success": false,
-      "message": common.MaskDatabaseSensitiveInfo(err.Error()),
-    })
+    common.ApiError(c, err)
common/gin.go (1)

102-107: Also log the raw error server-side before masking to preserve debuggability.

Masking helps UX/security, but we should still log the raw message internally. Suggest logging guarded by a debug flag to avoid flooding logs with user errors.

Apply:

 func ApiError(c *gin.Context, err error) {
-  c.JSON(http.StatusOK, gin.H{
+  if DebugEnabled && err != nil {
+    // retain raw for operators; do not return this to clients
+    SysLog("ApiError: " + err.Error())
+  }
+  c.JSON(http.StatusOK, gin.H{
     "success": false,
     "message": MaskDatabaseSensitiveInfo(err.Error()),
   })
 }

And similarly for ApiErrorMsg:

 func ApiErrorMsg(c *gin.Context, msg string) {
-  c.JSON(http.StatusOK, gin.H{
+  if DebugEnabled && msg != "" {
+    SysLog("ApiErrorMsg: " + msg)
+  }
+  c.JSON(http.StatusOK, gin.H{
     "success": false,
     "message": MaskDatabaseSensitiveInfo(msg),
   })
 }
relay/channel/vertex/adaptor.go (1)

170-178: Guard x-goog-user-project; fail early when ProjectID is empty.

Setting an empty project can lead to permission/billing errors that are hard to diagnose. Make it explicit and only set the header when non-empty.

Apply:

   req.Set("Authorization", "Bearer "+accessToken)
-  req.Set("x-goog-user-project", a.AccountCredentials.ProjectID)
+  if a.AccountCredentials.ProjectID == "" {
+    return fmt.Errorf("vertex credentials missing ProjectID")
+  }
+  req.Set("x-goog-user-project", a.AccountCredentials.ProjectID)
   return nil
main.go (2)

130-134: Masking also redacts the GitHub hostname, breaking the help link.

MaskDatabaseSensitiveInfo replaces hostnames like github.com with [masked-host], so clients will see an unusable link. Build the masked error and append the help URL unmasked.

Apply:

-    masked := common.MaskDatabaseSensitiveInfo(fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err))
+    maskedErr := common.MaskDatabaseSensitiveInfo(fmt.Sprintf("Panic detected, error: %v", err))
+    help := " Please submit an issue here: https://github.com/Calcium-Ion/new-api"
+    masked := maskedErr + help

129-131: Optional: include stack trace in server logs for postmortems.

You log the panic value, but not the stack. Consider adding debug.Stack() to logs (server-side only) to speed up triage. No change to client payloads.

Example:

import "runtime/debug"

// ...
common.SysLog(fmt.Sprintf("panic detected: %v\n%s", err, string(debug.Stack())))
controller/setup.go (1)

107-112: DRY: leverage ApiErrorMsg for repeated JSON error payloads.

You can centralize JSON formatting and masking via common.ApiErrorMsg(...) to keep controllers lean and consistent.

For example:

- c.JSON(500, gin.H{
-   "success": false,
-   "message": "系统错误: " + common.MaskDatabaseSensitiveInfo(err.Error()),
- })
+ common.ApiErrorMsg(c, "系统错误: "+common.MaskDatabaseSensitiveInfo(err.Error()))

Repeat similarly for the other error blocks.

Also applies to: 123-129, 139-144, 147-153, 164-169

relay/relay_adaptor.go (1)

131-133: Enable Vertex task flow in GetTaskAdaptor

Adding case constant.ChannelTypeVertexAi to return &taskvertex.TaskAdaptor{} correctly routes Vertex tasks through the new adaptor. Consider adding a small unit test for GetTaskAdaptor covering this case to avoid regressions when new channel types are added.

relay/relay_task.go (2)

293-296: Unused baseURL variable

baseURL is computed but not used for Vertex fetch. Either use it (if intended for future non-Google endpoints) or remove to reduce confusion.

-        baseURL := constant.ChannelBaseURLs[channelModel.Type]
-        if channelModel.GetBaseURL() != "" {
-            baseURL = channelModel.GetBaseURL()
-        }

301-305: Proxy is not propagated to FetchTask token acquisition

FetchTask currently acquires an access token with an empty proxy argument. If channels rely on per-channel proxy settings, fetches may fail in proxied environments. Consider extending the TaskAdaptor interface to accept a proxy or deriving it from the channel model inside FetchTask.

Would you like me to draft an interface-compatible approach (e.g., overload via context or extend body with an optional proxy) to carry the proxy through?

relay/channel/task/vertex/adaptor.go (3)

158-175: Response handling writes directly to the client

DoResponse both writes c.JSON and returns taskID, taskData. This matches how RelayTaskSubmit relies on adaptor-owned IO. If you plan to standardize responses via the caller, return the payload and let the caller write. For now, this is acceptable.


180-226: FetchTask: ignores baseUrl and does not use proxy

  • baseUrl parameter is unused; the method reconstructs the Google endpoint. That’s fine for Vertex, but the unused param can confuse callers.
  • Token acquisition uses "" for proxy, which may break in proxied deployments.

Consider:

  • Documenting that baseUrl is ignored for Vertex.
  • Accepting a proxy (non-breaking approach: infer from environment or extend body to carry it).

311-344: Operation-name parsing is reasonable

Regexes for region, model, project are straightforward with fallbacks. Consider unit tests with both global and regional operation names to lock behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a3c2b28 and 78e0705.

📒 Files selected for processing (13)
  • common/database.go (2 hunks)
  • common/gin.go (1 hunks)
  • controller/log.go (1 hunks)
  • controller/setup.go (6 hunks)
  • controller/task_video.go (3 hunks)
  • main.go (1 hunks)
  • middleware/distributor.go (1 hunks)
  • relay/channel/task/vertex/adaptor.go (1 hunks)
  • relay/channel/vertex/adaptor.go (2 hunks)
  • relay/channel/vertex/relay-vertex.go (1 hunks)
  • relay/channel/vertex/service_account.go (2 hunks)
  • relay/relay_adaptor.go (3 hunks)
  • relay/relay_task.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
controller/log.go (1)
common/database.go (1)
  • MaskDatabaseSensitiveInfo (21-33)
main.go (1)
common/database.go (1)
  • MaskDatabaseSensitiveInfo (21-33)
relay/channel/vertex/adaptor.go (1)
relay/channel/vertex/relay-vertex.go (1)
  • GetModelRegion (5-22)
relay/channel/task/vertex/adaptor.go (7)
relay/common/relay_info.go (3)
  • TaskRelayInfo (467-473)
  • TaskSubmitReq (487-495)
  • TaskInfo (497-504)
common/gin.go (1)
  • UnmarshalBodyReusable (30-51)
service/error.go (2)
  • TaskErrorWrapperLocal (131-135)
  • TaskErrorWrapper (137-153)
relay/channel/vertex/service_account.go (2)
  • Credentials (22-28)
  • AcquireAccessToken (142-148)
relay/channel/vertex/relay-vertex.go (1)
  • GetModelRegion (5-22)
relay/channel/api_request.go (1)
  • DoTaskApiRequest (280-302)
service/http_client.go (1)
  • GetHttpClient (27-29)
common/gin.go (1)
common/database.go (1)
  • MaskDatabaseSensitiveInfo (21-33)
middleware/distributor.go (1)
common/gin.go (1)
  • UnmarshalBodyReusable (30-51)
controller/setup.go (1)
common/database.go (1)
  • MaskDatabaseSensitiveInfo (21-33)
relay/relay_task.go (6)
model/channel.go (1)
  • GetChannelById (328-343)
constant/channel.go (2)
  • ChannelTypeVertexAi (41-41)
  • ChannelBaseURLs (57-111)
relay/relay_adaptor.go (1)
  • GetTaskAdaptor (118-138)
constant/task.go (1)
  • TaskPlatform (3-3)
model/task.go (5)
  • TaskStatus (11-11)
  • TaskStatusSuccess (19-19)
  • TaskStatusFailure (18-18)
  • TaskStatusQueued (16-16)
  • TaskStatusSubmitted (15-15)
common/json.go (2)
  • Unmarshal (8-10)
  • Marshal (20-22)
relay/relay_adaptor.go (3)
constant/channel.go (1)
  • ChannelTypeVertexAi (41-41)
relay/channel/task/vertex/adaptor.go (1)
  • TaskAdaptor (55-55)
relay/channel/adapter.go (1)
  • TaskAdaptor (32-51)
controller/task_video.go (1)
common/json.go (2)
  • Unmarshal (8-10)
  • Marshal (20-22)
relay/channel/vertex/service_account.go (1)
service/http_client.go (2)
  • NewProxyHttpClient (32-81)
  • GetHttpClient (27-29)
🔇 Additional comments (17)
middleware/distributor.go (2)

169-176: Good: avoid body unmarshal on GET for /v1/video/generations.

Moving UnmarshalBodyReusable under POST is correct and prevents spurious errors for fetch-by-ID GETs.


171-171: Verify UnmarshalBodyReusable actually populates modelRequest.

UnmarshalBodyReusable uses Unmarshal(requestBody, &v) (pointer-to-interface) per common/gin.go; this typically won’t populate the caller’s struct and may leave modelRequest zeroed, causing “未指定模型名称”. Please confirm with a unit test or fix UnmarshalBodyReusable to pass v instead of &v.

Minimal fix (in common/gin.go, outside this diff):

// instead of: err = Unmarshal(requestBody, &v)
err = Unmarshal(requestBody, v)
controller/task_video.go (1)

97-99: Good: redact large/base64 video payloads before persisting.

Storing the sanitized body in task.Data reduces storage bloat and accidental exposure of raw bytes.

relay/channel/vertex/service_account.go (1)

142-149: Public AcquireAccessToken API is a useful addition.

Simple, focused entrypoint for callers that only have credentials and a proxy.

controller/log.go (1)

88-92: Good hardening: mask DB details before returning error messages.

Wrapping the error string with common.MaskDatabaseSensitiveInfo(err.Error()) prevents leaking connection strings/hosts to clients. Matches the masking applied elsewhere in the PR.

common/gin.go (1)

95-100: Appropriate masking at the API boundary.

Replacing raw err.Error() with MaskDatabaseSensitiveInfo(err.Error()) mitigates accidental exposure of DB endpoints in client-visible payloads.

controller/setup.go (5)

107-112: Good: mask low-level errors when exposing user-facing messages.

This change avoids leaking DB hosts/connection strings on password hashing failures.


123-129: Good: mask DB error details when admin creation fails.

Consistent with the new masking policy across the API surface.


139-144: Good: mask DB error details when persisting SelfUseModeEnabled.


147-153: Good: mask DB error details when persisting DemoSiteEnabled.


164-169: Good: mask DB error details when writing setup record.

relay/relay_adaptor.go (1)

30-31: Vertex task adaptor wiring looks correct

Importing the Vertex task adaptor under the taskvertex alias is appropriate and keeps adaptor resolution local to GetTaskAdaptor. No issues spotted.

relay/relay_task.go (2)

37-37: Good: initialize channel meta before request build

Calling relayInfo.InitChannelMeta(c) early ensures downstream header building (e.g., proxy, region, project scoping) has the metadata it needs. This prevents subtle header/token acquisition bugs for Vertex.


203-205: Safer default response

Defaulting to {"code":"success","data":null} when body is empty avoids 200 responses with empty payloads. Confirm clients tolerate data: null vs an empty object/array for this route.

relay/channel/task/vertex/adaptor.go (3)

59-71: Validate request: good, but be mindful of body unmarshal helper behavior

Setting the action and validating prompt is correct. Note: if common.UnmarshalBodyReusable ever changes semantics, ensure it still populates typed structs (we’ve seen patterns where passing &v inside can lead to decoding into interface{} instead of the typed struct).

Would you like a quick test scaffold to assert that TaskSubmitReq is populated via UnmarshalBodyReusable?


106-122: Header build is solid

Decoding SA credentials, acquiring an access token, and setting x-goog-user-project aligns with Vertex requirements. No issues spotted.


297-307: Task ID encoding scheme is fine

Using base64.RawURLEncoding for the operation name avoids slashes and padding. LGTM.

Comment thread common/database.go Outdated
Comment on lines +21 to +33
func MaskDatabaseSensitiveInfo(message string) string {
if message == "" {
return message
}

reTcp := regexp.MustCompile(`@tcp\(([^)]+)\)`)
message = reTcp.ReplaceAllString(message, "@tcp([masked])")

reHost := regexp.MustCompile(`(?i)\b((?:[0-9]{1,3}\.){3}[0-9]{1,3}|[a-z0-9.-]+\.[a-z]{2,})(?::[0-9]{1,5})?\b`)
message = reHost.ReplaceAllString(message, "[masked-host]")

return message
}

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

⚠️ Potential issue

Credentials are not masked; DSNs can still leak usernames/passwords.

Current regex only hides hosts and @tcp(...), but leaves credentials like user:password@... and query params password=... visible (e.g., MySQL/Postgres URIs). This is a PII/security leak in user-facing errors. Also missing coverage for @unix(...) and IPv6 literals.

Apply this focused patch to extend masking within the current function:

 func MaskDatabaseSensitiveInfo(message string) string {
   if message == "" {
     return message
   }

   reTcp := regexp.MustCompile(`@tcp\(([^)]+)\)`)
   message = reTcp.ReplaceAllString(message, "@tcp([masked])")

+  // Also mask unix socket DSNs like @unix(/path/to/socket)
+  reUnix := regexp.MustCompile(`@unix\(([^)]+)\)`)
+  message = reUnix.ReplaceAllString(message, "@unix([masked])")
+
+  // Mask embedded credentials: scheme://user:password@host or user:password@host
+  reCred := regexp.MustCompile(`(?i)([a-z]+://)?([A-Za-z0-9._%+\-]+):([^@/\s]+)@`)
+  message = reCred.ReplaceAllString(message, "$1$2:****@")
+
+  // Mask password-like query parameters
+  rePwdParam := regexp.MustCompile(`(?i)(password|pwd|pass)=([^&\s]+)`)
+  message = rePwdParam.ReplaceAllString(message, "$1=****")
+
   reHost := regexp.MustCompile(`(?i)\b((?:[0-9]{1,3}\.){3}[0-9]{1,3}|[a-z0-9.-]+\.[a-z]{2,})(?::[0-9]{1,5})?\b`)
   message = reHost.ReplaceAllString(message, "[masked-host]")
+
+  // Mask IPv6 literals like [2001:db8::1]:5432
+  reIPv6 := regexp.MustCompile(`\[[0-9a-fA-F:]+\](?::[0-9]{1,5})?`)
+  message = reIPv6.ReplaceAllString(message, "[masked-host]")

   return message
 }

Follow-up (optional, recommended for perf/clarity): hoist these regexes to package-level vars so they’re compiled once, and add unit tests covering MySQL, Postgres, URI-with-creds, unix socket, IPv4/IPv6, and query-param forms.

📝 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 MaskDatabaseSensitiveInfo(message string) string {
if message == "" {
return message
}
reTcp := regexp.MustCompile(`@tcp\(([^)]+)\)`)
message = reTcp.ReplaceAllString(message, "@tcp([masked])")
reHost := regexp.MustCompile(`(?i)\b((?:[0-9]{1,3}\.){3}[0-9]{1,3}|[a-z0-9.-]+\.[a-z]{2,})(?::[0-9]{1,5})?\b`)
message = reHost.ReplaceAllString(message, "[masked-host]")
return message
}
func MaskDatabaseSensitiveInfo(message string) string {
if message == "" {
return message
}
reTcp := regexp.MustCompile(`@tcp\(([^)]+)\)`)
message = reTcp.ReplaceAllString(message, "@tcp([masked])")
// Also mask unix socket DSNs like @unix(/path/to/socket)
reUnix := regexp.MustCompile(`@unix\(([^)]+)\)`)
message = reUnix.ReplaceAllString(message, "@unix([masked])")
// Mask embedded credentials: scheme://user:password@host or user:password@host
reCred := regexp.MustCompile(`(?i)([a-z]+://)?([A-Za-z0-9._%+\-]+):([^@/\s]+)@`)
message = reCred.ReplaceAllString(message, "$1$2:****@")
// Mask password-like query parameters
rePwdParam := regexp.MustCompile(`(?i)(password|pwd|pass)=([^&\s]+)`)
message = rePwdParam.ReplaceAllString(message, "$1=****")
reHost := regexp.MustCompile(`(?i)\b((?:[0-9]{1,3}\.){3}[0-9]{1,3}|[a-z0-9.-]+\.[a-z]{2,})(?::[0-9]{1,5})?\b`)
message = reHost.ReplaceAllString(message, "[masked-host]")
// Mask IPv6 literals like [2001:db8::1]:5432
reIPv6 := regexp.MustCompile(`\[[0-9a-fA-F:]+\](?::[0-9]{1,5})?`)
message = reIPv6.ReplaceAllString(message, "[masked-host]")
return message
}
🤖 Prompt for AI Agents
In common/database.go around lines 21 to 33, the MaskDatabaseSensitiveInfo
function currently only masks hosts and @tcp(...) and therefore leaks
credentials (user:password@...), query parameters (e.g., password=...), IPv6
literals, and @unix(...) sockets; update the function to also replace
credentials in URIs (strip user[:password]@), redact sensitive query params
(password, passwd, secret, token, auth) values, handle @unix(...) similar to
@tcp(...), and ensure IPv6 literals are matched by the host regex; additionally,
hoist the regexes to package-level vars so they are compiled once and add unit
tests covering MySQL/Postgres URIs with credentials, URI query-param forms,
@unix sockets, IPv4 and IPv6 addresses.

Comment on lines +124 to +131
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, _ *relaycommon.TaskRelayInfo) (io.Reader, error) {
v, ok := c.Get("task_request")
if !ok {
return nil, fmt.Errorf("request not found in context")
}
req := v.(relaycommon.TaskSubmitReq)

body := requestPayload{

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

Avoid risky type assertion panic in BuildRequestBody

Type asserting without a check can panic if the context value drifts. Guard the assertion.

-    v, ok := c.Get("task_request")
-    if !ok {
-        return nil, fmt.Errorf("request not found in context")
-    }
-    req := v.(relaycommon.TaskSubmitReq)
+    v, ok := c.Get("task_request")
+    if !ok {
+        return nil, fmt.Errorf("request not found in context")
+    }
+    req, ok := v.(relaycommon.TaskSubmitReq)
+    if !ok {
+        return nil, fmt.Errorf("invalid request payload type")
+    }
📝 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 (a *TaskAdaptor) BuildRequestBody(c *gin.Context, _ *relaycommon.TaskRelayInfo) (io.Reader, error) {
v, ok := c.Get("task_request")
if !ok {
return nil, fmt.Errorf("request not found in context")
}
req := v.(relaycommon.TaskSubmitReq)
body := requestPayload{
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, _ *relaycommon.TaskRelayInfo) (io.Reader, error) {
v, ok := c.Get("task_request")
if !ok {
return nil, fmt.Errorf("request not found in context")
}
req, ok := v.(relaycommon.TaskSubmitReq)
if !ok {
return nil, fmt.Errorf("invalid request payload type")
}
body := requestPayload{
// ...
}
// ...
}
🤖 Prompt for AI Agents
In relay/channel/task/vertex/adaptor.go around lines 124 to 131, the code uses
an unchecked type assertion v.(relaycommon.TaskSubmitReq) which can panic if the
context value is not the expected type; change this to a safe check (either use
vTyped, ok := v.(relaycommon.TaskSubmitReq) or a type switch), and if the
assertion fails return a descriptive error (e.g., wrong type in context:
expected relaycommon.TaskSubmitReq) instead of allowing a panic so
BuildRequestBody returns an error when the stored value is not the expected
type.

Comment thread relay/channel/task/vertex/adaptor.go
Comment thread relay/relay_task.go
Comment on lines +285 to +305
func() {
channelModel, err2 := model.GetChannelById(originTask.ChannelId, true)
if err2 != nil {
return
}
if channelModel.Type != constant.ChannelTypeVertexAi {
return
}
baseURL := constant.ChannelBaseURLs[channelModel.Type]
if channelModel.GetBaseURL() != "" {
baseURL = channelModel.GetBaseURL()
}
adaptor := GetTaskAdaptor(constant.TaskPlatform(strconv.Itoa(channelModel.Type)))
if adaptor == nil {
return
}
resp, err2 := adaptor.FetchTask(baseURL, channelModel.Key, map[string]any{
"task_id": originTask.TaskID,
"action": originTask.Action,
})
if err2 != nil || resp == nil {

@coderabbitai coderabbitai Bot Aug 26, 2025

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

⚠️ Potential issue

Do not persist base64 video data into FailReason; keep it ephemeral

Currently, when Vertex returns a base64 video, it is assigned to originTask.FailReason (Line 321). This will store potentially megabytes of data in a field intended for short error text, risking DB bloat, slow queries, and truncation/data loss depending on column type. Return the URL in the response only; persist status/progress and (on failure) the textual reason.

Apply this diff:

@@
-        if ti.Status != "" {
-            originTask.Status = model.TaskStatus(ti.Status)
-        }
-        if ti.Progress != "" {
-            originTask.Progress = ti.Progress
-        }
-        if ti.Url != "" {
-            originTask.FailReason = ti.Url
-        }
-        _ = originTask.Update()
+        if ti.Status != "" {
+            originTask.Status = model.TaskStatus(ti.Status)
+        }
+        if ti.Progress != "" {
+            originTask.Progress = ti.Progress
+        }
+        // Only persist textual failure reasons; do NOT store large base64 payloads in FailReason.
+        if ti.Status == model.TaskStatusFailure && ti.Reason != "" {
+            originTask.FailReason = ti.Reason
+        }
+        if err := originTask.Update(); err != nil {
+            common.SysLog("update task failed: " + err.Error())
+        }
@@
-        format := "mp4"
+        format := "mp4"
         if respObj, ok := raw["response"].(map[string]any); ok {
             if vids, ok := respObj["videos"].([]any); ok && len(vids) > 0 {
                 if v0, ok := vids[0].(map[string]any); ok {
                     if mt, ok := v0["mimeType"].(string); ok && mt != "" {
-                        if strings.Contains(mt, "mp4") {
-                            format = "mp4"
-                        } else {
-                            format = mt
-                        }
+                        format = mimeToFormat(mt)
                     }
                 }
             }
         }
@@
-        out := map[string]any{
+        // Note: resultURL is not persisted; only returned to caller.
+        resultURL := ti.Url
+        out := map[string]any{
             "error":    nil,
             "format":   format,
             "metadata": nil,
             "status":   status,
             "task_id":  originTask.TaskID,
-            "url":      originTask.FailReason,
+            "url":      resultURL,
         }

Add this helper in the same file (outside the function) to normalize MIME types to short formats:

func mimeToFormat(mime string) string {
    mime = strings.TrimSpace(mime)
    if mime == "" {
        return "mp4"
    }
    if i := strings.Index(mime, "/"); i >= 0 && i+1 < len(mime) {
        return mime[i+1:]
    }
    if strings.Contains(mime, "mp4") {
        return "mp4"
    }
    return mime
}

Also applies to: 306-324, 325-340, 341-361

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no i delete it at controller/task_video.go

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.

🧩 Analysis chain

@Sh1n3zZ I see you've implemented response sanitization in controller/task_video.go with the redactVideoResponseBody function, which is excellent for preventing large base64 data from being stored in task.Data.

However, my concern is about a different issue in relay/relay_task.go at Line 321 where ti.Url (which could contain base64 video data) is being assigned to originTask.FailReason and then persisted to the database via originTask.Update(). This is separate from the response sanitization you implemented.

The FailReason field should only store textual error messages, not large binary data. Even though you're sanitizing the response data, the base64 URL could still be stored in the database's FailReason column, which could cause database performance issues.


🏁 Script executed:

#!/bin/bash
# Check the controller/task_video.go file to understand the redactVideoResponseBody implementation
cat controller/task_video.go

Length of output: 5624


@Sh1n3zZ You're absolutely right! I can see that you've already implemented the protection in controller/task_video.go at lines 104-108:

if !(len(taskResult.Url) > 5 && taskResult.Url[:5] == "data:") {
    task.FailReason = taskResult.Url
}

This correctly prevents data: URLs (containing base64 data) from being stored in the FailReason field.

However, for consistency, the same protection should be applied in relay/relay_task.go at Line 321. Currently, it directly assigns ti.Url to originTask.FailReason without the data: URL check:

if ti.Url != "" {
    originTask.FailReason = ti.Url
}

Consider updating it to match your pattern:

if ti.Url != "" && !(len(ti.Url) > 5 && ti.Url[:5] == "data:") {
    originTask.FailReason = ti.Url
}

This will ensure both code paths handle base64 video data consistently. Thanks for pointing this out!


✏️ Learnings added
Learnt from: Sh1n3zZ
PR: QuantumNous/new-api#1659
File: relay/relay_task.go:285-305
Timestamp: 2025-08-26T09:59:00.307Z
Learning: In controller/task_video.go, the redactVideoResponseBody function sanitizes video task responses by removing bytesBase64Encoded fields and truncating base64 strings to 256 characters to prevent large binary data from being stored in task.Data.

Learnt from: Sh1n3zZ
PR: QuantumNous/new-api#1659
File: relay/relay_task.go:285-305
Timestamp: 2025-08-26T09:59:00.307Z
Learning: In controller/task_video.go, data: URLs (containing base64 encoded video data) are prevented from being stored in task.FailReason by checking if the URL starts with "data:" before assignment. This same pattern should be applied consistently across the codebase.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@HynoR

HynoR commented Aug 27, 2025

Copy link
Copy Markdown
Contributor

把两个改动拆开 pr 比较好吧,毕竟第二个还没确定

@Sh1n3zZ

Sh1n3zZ commented Aug 27, 2025

Copy link
Copy Markdown
Contributor Author

把两个改动拆开 pr 比较好吧,毕竟第二个还没确定

好的建议 🥰

@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

♻️ Duplicate comments (1)
relay/channel/task/vertex/adaptor.go (1)

124-131: Avoid panic on unchecked type assertion in BuildRequestBody

Use a safe assertion and return a clear error if the context type drifts.

-	req := v.(relaycommon.TaskSubmitReq)
+	req, ok := v.(relaycommon.TaskSubmitReq)
+	if !ok {
+		return nil, fmt.Errorf("invalid request payload type")
+	}
🧹 Nitpick comments (8)
controller/setup.go (1)

176-181: No functional change; consider stdlib for bool formatting.

End-of-file formatting only. Optionally replace boolToString with strconv.FormatBool for consistency.

-func boolToString(b bool) string {
-	if b {
-		return "true"
-	}
-	return "false"
-}
+func boolToString(b bool) string { return strconv.FormatBool(b) }

Additional change outside hunk:

// add import
import "strconv"
main.go (1)

211-211: Restore newline at EOF.

Many linters/tools expect a trailing newline; cheap to keep CI quiet.

-}
\ No newline at end of file
+}
controller/task_video.go (3)

116-123: Use strings.HasPrefix for the data: URL check.

Safer and clearer than manual slicing.

-        if !(len(taskResult.Url) > 5 && taskResult.Url[:5] == "data:") {
-            task.FailReason = taskResult.Url
-        }
+        if !strings.HasPrefix(taskResult.Url, "data:") {
+            task.FailReason = taskResult.Url
+        }

Additional change outside hunk:

// add import
import "strings"

152-176: Broaden redaction: also truncate nested video strings within videos[].

Currently you delete bytesBase64Encoded under response.videos[i] but don’t truncate a potential videos[i].video string. Minor extension keeps consistency.

 		if vs, ok := resp["videos"].([]any); ok {
 			for i := range vs {
 				if vm, ok := vs[i].(map[string]any); ok {
 					delete(vm, "bytesBase64Encoded")
+					if vv, ok := vm["video"].(string); ok {
+						vm["video"] = truncateBase64(vv)
+					}
 				}
 			}
 		}

178-184: Avoid magic number; hoist max length to a const.

Improves readability and reuse.

-func truncateBase64(s string) string {
-	const maxKeep = 256
+const maxBase64Keep = 256
+
+func truncateBase64(s string) string {
-	if len(s) <= maxKeep {
+	if len(s) <= maxBase64Keep {
 		return s
 	}
-	return s[:maxKeep] + "..."
+	return s[:maxBase64Keep] + "..."
 }
relay/channel/vertex/service_account.go (2)

141-149: Avoid duplication and consider caching in AcquireAccessToken

AcquireAccessToken reimplements the exchange path and bypasses the existing cache in getAccessToken. Prefer a single exchange path (shared helper) and reuse the cache keyed by service account + proxy to reduce auth round-trips.

Would you like me to draft a small refactor that extracts a shared token-exchange helper and wires AcquireAccessToken through the cache?


106-140: Align the legacy exchange path with improved checks and endpoint

Mirror the same status checks and endpoint in exchangeJwtForAccessToken to keep both paths consistent.

Apply this diff:

 func exchangeJwtForAccessToken(signedJWT string, info *relaycommon.RelayInfo) (string, error) {
 
-	authURL := "https://www.googleapis.com/oauth2/v4/token"
+	authURL := "https://oauth2.googleapis.com/token"
@@
-	var result map[string]interface{}
-	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+	var result map[string]interface{}
+	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
 		return "", err
 	}
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return "", fmt.Errorf("token endpoint %d: %v", resp.StatusCode, result)
+	}
 
-	if accessToken, ok := result["access_token"].(string); ok {
+	if accessToken, ok := result["access_token"].(string); ok && accessToken != "" {
 		return accessToken, nil
 	}
-	return "", fmt.Errorf("failed to get access token: %v", result)
+	return "", fmt.Errorf("no access_token in response: %v", result)
 }
relay/channel/task/vertex/adaptor.go (1)

234-239: Desensitize upstream error messages before persisting

op.Error.Message may include sensitive strings (e.g., DB URIs). Run it through your new MySQL/secret masking helper before assigning to ti.Reason.

Would you like me to wire in the shared desensitizer here?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 78e0705 and 81e29aa.

📒 Files selected for processing (11)
  • common/database.go (1 hunks)
  • controller/setup.go (1 hunks)
  • controller/task_video.go (3 hunks)
  • main.go (1 hunks)
  • middleware/distributor.go (1 hunks)
  • relay/channel/task/vertex/adaptor.go (1 hunks)
  • relay/channel/vertex/adaptor.go (1 hunks)
  • relay/channel/vertex/relay-vertex.go (1 hunks)
  • relay/channel/vertex/service_account.go (2 hunks)
  • relay/relay_adaptor.go (3 hunks)
  • relay/relay_task.go (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • relay/channel/vertex/adaptor.go
  • common/database.go
  • relay/relay_task.go
  • relay/channel/vertex/relay-vertex.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-26T09:59:00.337Z
Learnt from: Sh1n3zZ
PR: QuantumNous/new-api#1659
File: relay/relay_task.go:285-305
Timestamp: 2025-08-26T09:59:00.337Z
Learning: In controller/task_video.go, data: URLs (containing base64 encoded video data) are prevented from being stored in task.FailReason by checking if the URL starts with "data:" before assignment. This same pattern should be applied consistently across the codebase.

Applied to files:

  • relay/channel/task/vertex/adaptor.go
  • controller/task_video.go
📚 Learning: 2025-08-26T09:59:00.337Z
Learnt from: Sh1n3zZ
PR: QuantumNous/new-api#1659
File: relay/relay_task.go:285-305
Timestamp: 2025-08-26T09:59:00.337Z
Learning: In controller/task_video.go, the redactVideoResponseBody function sanitizes video task responses by removing bytesBase64Encoded fields and truncating base64 strings to 256 characters to prevent large binary data from being stored in task.Data.

Applied to files:

  • relay/channel/task/vertex/adaptor.go
  • controller/task_video.go
🧬 Code graph analysis (5)
middleware/distributor.go (1)
common/gin.go (1)
  • UnmarshalBodyReusable (30-51)
relay/relay_adaptor.go (3)
constant/channel.go (1)
  • ChannelTypeVertexAi (41-41)
relay/channel/task/vertex/adaptor.go (1)
  • TaskAdaptor (55-55)
relay/channel/adapter.go (1)
  • TaskAdaptor (32-51)
relay/channel/task/vertex/adaptor.go (7)
relay/common/relay_info.go (3)
  • TaskRelayInfo (467-473)
  • TaskSubmitReq (487-495)
  • TaskInfo (497-504)
common/gin.go (1)
  • UnmarshalBodyReusable (30-51)
service/error.go (2)
  • TaskErrorWrapperLocal (131-135)
  • TaskErrorWrapper (137-153)
relay/channel/vertex/service_account.go (2)
  • Credentials (22-28)
  • AcquireAccessToken (142-148)
relay/channel/vertex/relay-vertex.go (1)
  • GetModelRegion (5-22)
relay/channel/api_request.go (1)
  • DoTaskApiRequest (280-302)
service/http_client.go (1)
  • GetHttpClient (27-29)
controller/task_video.go (1)
common/json.go (2)
  • Unmarshal (8-10)
  • Marshal (20-22)
relay/channel/vertex/service_account.go (1)
service/http_client.go (2)
  • NewProxyHttpClient (32-81)
  • GetHttpClient (27-29)
🔇 Additional comments (7)
middleware/distributor.go (1)

171-172: Body unmarshal only on POST is correct.

Avoids consuming body on GET and aligns with idempotent fetch semantics.

controller/task_video.go (1)

97-98: Good: sanitize task.Data before storing.

Storing redacted JSON reduces DB bloat and risk of leaking raw base64 payloads.

relay/relay_adaptor.go (2)

30-31: Vertex AI task adaptor wiring added — LGTM.

Import alias and wiring look correct.


131-133: Vertex channel support in GetTaskAdaptor — LGTM.

Enables long-running Vertex workflows via task adaptor.

relay/channel/vertex/service_account.go (1)

88-95: Update JWT “aud” to the recommended Google OAuth2 token endpoint

Google’s current OAuth 2.0 token endpoint for JWT bearer grants is https://oauth2.googleapis.com/token. Aligning the JWT “aud” claim with this endpoint prevents validation mismatches.

• File: relay/channel/vertex/service_account.go, lines 88–95

     claims := jwt.MapClaims{
         "iss":   email,
         "scope": "https://www.googleapis.com/auth/cloud-platform",
-        "aud":   "https://www.googleapis.com/oauth2/v4/token",
+        "aud":   "https://oauth2.googleapis.com/token",
         "exp":   now.Add(time.Minute * 35).Unix(),
         "iat":   now.Unix(),
     }
relay/channel/task/vertex/adaptor.go (2)

59-61: Review comment incorrect: vertex/adaptor.go is for text tasks, not video

The code under review lives in relay/channel/task/vertex/adaptor.go, which implements the Vertex channel adaptor for text generation. Setting info.Action = constant.TaskActionTextGenerate here is intentional and correct.

relay/channel/task/vertex/adaptor.go is the Vertex (text) adaptor, so TaskActionTextGenerate is expected.
• Video tasks are handled separately in relay/channel/task/vidu/adaptor.go, which uses constant.TaskActionGenerate where appropriate (e.g., generate vs. textGenerate logic in Vidu) .
constant/task.go only defines TaskActionGenerate = "generate" and TaskActionTextGenerate = "textGenerate"; there isn’t a dedicated “videoGenerate” constant .

No change needed in this file; please disregard the original comment.

Likely an incorrect or invalid review comment.


240-291: Clarify Done-without-payload behavior in task/vertex adaptor

It looks like only the Vertex task adaptor checks op.Done and sets ti.Url; no other relay/channel/**/adaptor.go implements this pattern. That means if op.Done is true but neither Videos, BytesBase64Encoded, nor Video fields are present (e.g. a storage-URI-only response), ti.Url remains unset and the client sees "SUCCESS" with no URL.

Please confirm whether this is intended. If not, consider:

  • Surface the storage URI (e.g. op.Response.StorageUri) when available, or
  • Return an explicit error/status indicating “no payload” instead of silent success.

Key location:

  • relay/channel/task/vertex/adaptor.go around lines 239–241 and 290–291

Comment on lines +55 to +58
type TaskAdaptor struct{}

func (a *TaskAdaptor) Init(info *relaycommon.TaskRelayInfo) {}

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

Honor channel proxy for both submit and fetch flows

Submit path uses proxy via AcquireAccessToken(info.ChannelSetting.Proxy) but FetchTask ignores proxy and uses the default client. Persist proxy on the adaptor and use it in both places to avoid breakage behind proxies.

Apply this diff:

-type TaskAdaptor struct{}
+type TaskAdaptor struct{ proxy string }

-func (a *TaskAdaptor) Init(info *relaycommon.TaskRelayInfo) {}
+func (a *TaskAdaptor) Init(info *relaycommon.TaskRelayInfo) { a.proxy = info.ChannelSetting.Proxy }
@@
-	token, err := vertexcore.AcquireAccessToken(*adc, info.ChannelSetting.Proxy)
+	token, err := vertexcore.AcquireAccessToken(*adc, a.proxy)
@@
-	token, err := vertexcore.AcquireAccessToken(*adc, "")
+	token, err := vertexcore.AcquireAccessToken(*adc, a.proxy)
@@
-	req.Header.Set("x-goog-user-project", adc.ProjectID)
-	return service.GetHttpClient().Do(req)
+	req.Header.Set("x-goog-user-project", adc.ProjectID)
+	var client *http.Client
+	if a.proxy != "" {
+		client, err = service.NewProxyHttpClient(a.proxy)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		client = service.GetHttpClient()
+	}
+	return client.Do(req)

Also applies to: 106-122, 180-226

Comment on lines +150 to +182
func exchangeJwtForAccessTokenWithProxy(signedJWT string, proxy string) (string, error) {
authURL := "https://www.googleapis.com/oauth2/v4/token"
data := url.Values{}
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
data.Set("assertion", signedJWT)

var client *http.Client
var err error
if proxy != "" {
client, err = service.NewProxyHttpClient(proxy)
if err != nil {
return "", fmt.Errorf("new proxy http client failed: %w", err)
}
} else {
client = service.GetHttpClient()
}

resp, err := client.PostForm(authURL, data)
if err != nil {
return "", err
}
defer resp.Body.Close()

var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}

if accessToken, ok := result["access_token"].(string); ok {
return accessToken, nil
}
return "", fmt.Errorf("failed to get access token: %v", result)
}

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

Harden token exchange: use oauth2.googleapis.com and check HTTP status

Switch to the modern token endpoint and handle non-2xx responses explicitly to avoid opaque failures.

Apply this diff:

 func exchangeJwtForAccessTokenWithProxy(signedJWT string, proxy string) (string, error) {
-	authURL := "https://www.googleapis.com/oauth2/v4/token"
+	authURL := "https://oauth2.googleapis.com/token"
@@
-	var result map[string]interface{}
-	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+	var result map[string]interface{}
+	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
 		return "", err
 	}
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return "", fmt.Errorf("token endpoint %d: %v", resp.StatusCode, result)
+	}
 
-	if accessToken, ok := result["access_token"].(string); ok {
+	if accessToken, ok := result["access_token"].(string); ok && accessToken != "" {
 		return accessToken, nil
 	}
-	return "", fmt.Errorf("failed to get access token: %v", result)
+	return "", fmt.Errorf("no access_token in response: %v", result)
 }
📝 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 exchangeJwtForAccessTokenWithProxy(signedJWT string, proxy string) (string, error) {
authURL := "https://www.googleapis.com/oauth2/v4/token"
data := url.Values{}
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
data.Set("assertion", signedJWT)
var client *http.Client
var err error
if proxy != "" {
client, err = service.NewProxyHttpClient(proxy)
if err != nil {
return "", fmt.Errorf("new proxy http client failed: %w", err)
}
} else {
client = service.GetHttpClient()
}
resp, err := client.PostForm(authURL, data)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if accessToken, ok := result["access_token"].(string); ok {
return accessToken, nil
}
return "", fmt.Errorf("failed to get access token: %v", result)
}
func exchangeJwtForAccessTokenWithProxy(signedJWT string, proxy string) (string, error) {
authURL := "https://oauth2.googleapis.com/token"
data := url.Values{}
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
data.Set("assertion", signedJWT)
var client *http.Client
var err error
if proxy != "" {
client, err = service.NewProxyHttpClient(proxy)
if err != nil {
return "", fmt.Errorf("new proxy http client failed: %w", err)
}
} else {
client = service.GetHttpClient()
}
resp, err := client.PostForm(authURL, data)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("token endpoint %d: %v", resp.StatusCode, result)
}
if accessToken, ok := result["access_token"].(string); ok && accessToken != "" {
return accessToken, nil
}
return "", fmt.Errorf("no access_token in response: %v", result)
}
🤖 Prompt for AI Agents
In relay/channel/vertex/service_account.go around lines 150 to 182, the token
exchange uses the outdated auth URL and doesn't check HTTP response status;
update authURL to "https://oauth2.googleapis.com/token" and explicitly handle
non-2xx responses by reading the response body, returning a descriptive error
that includes resp.StatusCode and the response body (or parsed JSON) instead of
proceeding to decode or assuming success; keep decoding the JSON for the
access_token only on a successful status and ensure resp.Body is closed as
currently done.

@seefs001
seefs001 changed the base branch from alpha to main September 13, 2025 05:07
@seefs001
seefs001 changed the base branch from main to alpha September 13, 2025 05:07
@seefs001
seefs001 merged commit a3b8a19 into QuantumNous:alpha Sep 13, 2025
1 of 2 checks passed
This was referenced Sep 13, 2025
@Sh1n3zZ
Sh1n3zZ deleted the feat-vertex-veo branch September 14, 2025 13:25
@coderabbitai coderabbitai Bot mentioned this pull request Mar 28, 2026
x22x22 pushed a commit to x22x22/new-api that referenced this pull request Apr 24, 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.

3 participants