Skip to content

Add ModelAPI Seedance 2.5 with Flatkey URLs and GCS delivery - #683

Merged
think-back merged 33 commits into
mainfrom
feature/modelapi-seedance-25
Aug 11, 2026
Merged

Add ModelAPI Seedance 2.5 with Flatkey URLs and GCS delivery#683
think-back merged 33 commits into
mainfrom
feature/modelapi-seedance-25

Conversation

@think-back

@think-back think-back commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add channel type ModelAPISeedance (111) for https://api.modelapi.co with fixed upstream model doubao-seedance-2-5-260628.
  • Translate Flatkey /v1/videos requests to ModelAPI /v1/tasks submit/polling without exposing supplier task IDs, URLs, billing metadata, keys, or brand names.
  • Reuse Flatkey's existing asset library through URL-native inputs. ModelAPI Seedance 2.5 has no documented provider asset-library upload/binding API, so strict coverage records a source-URL target, skips provider materialization, and signs the stored GCS source only when the selected channel submits the request.
  • Archive successful ModelAPI output into private Google Cloud Storage before reporting success. Clients still receive Flatkey URLs; GET /v1/videos/{task_id}/content returns a short-lived 302 to the private GCS signed download.
  • Reuse existing task billing hooks for the documented Seedance 2.5 price tiers, including submit-time correction from a valid private usage.estimated_usd snapshot.
  • Convert deterministic ModelAPI capacity rejection to HTTP 429 and allow existing queued channel fallback only for ModelAPISeedance + 429; other task-provider behavior is unchanged.

Upstream contract and pricing

Verified against ModelAPI public documentation on 2026-08-11:

Video input Resolution Documented price
No 480p USD 0.140 * duration
No 720p USD 0.314 * duration
Yes 480p USD 0.084 * total_video_duration
Yes 720p USD 0.188 * total_video_duration

The implementation uses model_price = 0.14 as the existing fixed-price calculation base:

billable_units = estimated_usd / model_price
quota = model_price * billable_units * group_ratio * quota_per_unit

This does not mean every request costs USD 0.14. Missing values follow the documented defaults (duration=5, resolution=720p). For video input, Flatkey cannot safely derive total_video_duration, so it reserves the documented 30-second maximum at the matching input-video rate. A positive finite usage.estimated_usd from a successful submit response replaces that reservation; invalid or missing estimates retain the bounded reservation.

Asset-library behavior

  • The Flatkey asset library remains the source of truth; no undocumented ModelAPI asset upload endpoint is called.
  • In strict coverage mode, the ModelAPI target becomes active through source-url:modelapi readiness without uploading provider-native material.
  • asset://... references are replaced only after final channel selection and only when the active target/readiness/channel/model fences still match.
  • Provider-facing GCS input URLs use a dedicated 12-hour TTL so asynchronous ModelAPI consumption is not limited by the one-hour user-facing asset URL cap.
  • Accepted task paths extend source-object retention through the existing asset-retention transaction.

Delivery, concurrency, and security

  • Public task/content URLs remain Flatkey-owned; upstream success is withheld until the MP4 is durably archived.
  • GCS objects remain private and are downloaded through a short-lived signed URL behind Flatkey's 302 endpoint.
  • Archive workers atomically claim a task-scoped lease before external download/upload, renew it with a TTL-derived heartbeat, cancel archive work if the lease is lost, and finalize with the latest owner/expiry fence.
  • Proxy download clients are cloned and receive both the configured timeout and redirect-time SSRF validation.
  • ModelAPI proxy settings are rejected on admin, submit, polling, and archive paths; TechMobi keeps its existing proxy behavior.
  • error.code=rate_limit_exceeded or the observed Selected model is at capacity... semantic failure maps to a white-label 429. Raw provider errors remain private.

Production rollout

Required deployment order:

  1. Deploy Console/master first so Task AutoMigrate creates the archive lease columns.
  2. Deploy Router/API and task workers.

Required configuration:

  • ASSET_MODEL_COVERAGE_STRICT_ENABLED=true
  • Channel type: ModelAPISeedance / 111
  • Base URL: https://api.modelapi.co
  • Model: doubao-seedance-2-5-260628
  • Proxy: empty
  • Fixed model price: 0.14
  • Existing private asset GCS configuration, including ASSET_SERVICE_ACCOUNT_EMAIL where IAM signing is used
  • Existing private video-result GCS configuration:
    • VIDEO_RESULT_STORAGE_BUCKET
    • VIDEO_RESULT_SERVICE_ACCOUNT_EMAIL
    • optional VIDEO_RESULT_SIGNED_URL_TTL_SECONDS (default 900, maximum 3600)
    • optional VIDEO_RESULT_RETENTION_SECONDS (default 86400)
    • optional VIDEO_RESULT_FETCH_TIMEOUT_SECONDS (default/maximum 1800)
    • optional VIDEO_RESULT_MAX_BYTES (default/maximum 524288000)

Deployment impact:

  • Router/API: required
  • Console/master: required
  • Standalone website: not required
  • Terraform: not required
  • Cloudflare: not required

Verification

  • Spec review: passed for ModelAPI-only 429 fallback, archive lease fencing/heartbeat, and proxy redirect validation.
  • Independent final production review: approved with 0 Critical and 0 Important findings after provider input TTL was corrected from the generic one-hour cap to 12 hours.
  • Temporary fake/mock regression tests were run locally with a RED/GREEN cycle and removed before commit.
  • Fresh targeted ModelAPI billing, request validation, response mapping, archive failure/CAS-loser, and white-label capacity tests passed.
  • Fresh compile checks passed for modelapiseedance, taskcommon, relay, middleware, service, and model.
  • Scoped go vet passed for all changed backend packages.
  • go build ./... passed.
  • git diff --check passed; no test/mock files or C-drive paths are included in the commit.

Validation gaps

  • No live ModelAPI request was made, intentionally, to avoid consuming the upstream balance.
  • No live GCS/IAM request and no staging/production deployment were performed.
  • Staging should verify real submit, polling, source-URL fetch, GCS archive creation, Flatkey task/content URLs, signed-download redirect, IAM SignBlob, retention behavior, fallback behavior, and actual settlement before production promotion.
  • The exact capacity message is treated as an observed compatibility signal, not as a documented public contract; the documented-style rate_limit_exceeded code remains the primary classification.

Constraint: Public task responses must retain Flatkey /content URLs and ModelAPI success must be archived before terminal state.

Rejected: Returning upstream or permanent GCS URLs | Both break the white-label and short-lived access boundary.

Confidence: high

Scope-risk: moderate

Directive: Do not add an upstream fallback for ModelAPI content downloads.

Tested: design and plan placeholder, consistency, and whitespace review
Constraint: public requests remain on shared Seedance content contract
Rejected: implementing full ModelAPI upstream behavior | Task 1 only requires registration and a skeletal adaptor; Task 2 owns behavior
Confidence: high
Scope-risk: narrow
Directive: keep ModelAPI public routing on OpenAI video endpoint and shared seedance binding; do not leak ModelAPI brand text through whitelabel task paths
Tested: RED exact command failed in temporary HEAD worktree on missing ChannelTypeModelAPISeedance and modelapiseedance package; GREEN exact command passed for constant/common/taskcommon subset but ./relay and modelapiseedance package are blocked by unrelated service/task_polling.go undefined service compile error
Not-tested: full requested GREEN until unrelated service package compile error is fixed
Constraint: fixed labels never contain upstream/storage identifiers

Confidence: high

Scope-risk: narrow

Tested: $env:GOCACHE=$PWD\.tmp-gocache; go test -p 1 ./service ./pkg/perf_metrics -run 'VideoResultChannel|ModelAPI.*Metric|ArchiveVideoResultForChannel' -count=1

Tested: $env:GOCACHE=$PWD\.tmp-gocache; go test -p 1 ./service ./pkg/perf_metrics -run 'VideoResult|ArchiveVideoResult' -count=1
Constraint: terminal settlement remains guarded by existing multi-node CAS
Rejected: persisting upstream URL | archived whitelabel results must serve through the proxy and persisted polling payloads must not retain provider URLs
Confidence: high
Scope-risk: moderate
Directive: new archive channels require fixed label/redaction
Tested: $env:GOCACHE=$PWD\.tmp-gocache; go test -p 1 ./service -run 'ModelAPI.*Archive|ModelAPI.*Redact|UpdateVideoSingleTask' -count=1
Tested: $env:GOCACHE=$PWD\.tmp-gocache; go test -v -timeout 60s -p 1 ./service -run 'UpdateVideoSingleTask|RedactTechMobiVideoResponseBody|ModelAPI.*Archive|ModelAPI.*Redact' -count=1
… surfaces

Constraint: internal configurable channel only

Confidence: high

Scope-risk: narrow

Tested: Push-Location web/default; bun test src/features/channels/constants.test.ts; Pop-Location

Tested: Push-Location web/default; bun run build; Pop-Location

Tested: Push-Location web/classic; bun run build; Pop-Location

Tested: Push-Location web/default; bun run typecheck; Pop-Location
… bytes

Constraint: ModelAPI never falls back upstream

Confidence: high

Scope-risk: moderate

Tested: $env:GOCACHE=$PWD\.tmp-gocache; go test -p 1 ./controller -run 'ArchivedModelAPI|ModelAPI.*WithoutArchive|LegacyTechMobi' -count=1

Tested: $env:GOCACHE=$PWD\.tmp-gocache; go test -p 1 ./controller -run 'VideoProxy|ArchivedTechMobi|ArchivedModelAPI|LegacyTechMobi' -count=1
…tics

Constraint: explicit false/zero survive
Rejected: provider-specific client input | the channel reuses the shared Seedance content contract and maps internally
Confidence: high
Scope-risk: moderate
Directive: keep ModelAPI upstream task IDs and asset URLs internal; expose only public task IDs and proxy result URLs
Tested: $env:GOCACHE="$PWD\.tmp-gocache"; go test -p 1 ./relay/channel/task/modelapiseedance ./dto -run 'ModelAPI|AudioOnly|Build|Validate|Parse|Fetch|Response|Convert' -count=1; $env:GOCACHE="$PWD\.tmp-gocache"; go test -p 1 ./relay/channel/task/modelapiseedance ./dto -count=1
Not-tested: live ModelAPI submission/polling against external service
Constraint: ModelAPI create JSON requires grouped text/image/video/audio input arrays and customer-facing failures must not expose upstream URLs, IDs, hosts, or brands.

Rejected: Reusing upstream error.message or error.code after brand-only scrubbing | arbitrary CDN URLs and upstream task IDs can still leak.

Confidence: high

Scope-risk: narrow

Directive: Keep ModelAPI Seedance failure messages fixed unless a comprehensive safe redactor exists for arbitrary upstream identifiers.

Tested: go test ./relay/channel/task/modelapiseedance -count=1; go test ./dto -count=1; gofmt; git diff --check

Not-tested: full repository test suite
Constraint: Scope limited to web/classic channel 111 key prompt and icon mapping; no web/default or backend changes.

Rejected: Importing JSX helpers directly in tests | would require heavier UI/runtime setup for a two-branch regression.

Confidence: high

Scope-risk: narrow

Directive: Keep channel 111 provider-facing copy generic and reuse Doubao iconography in classic console.

Tested: bun test src/components/table/channels/modals/modelapi-seedance-classic.test.js; bun run build

Not-tested: Browser-rendered classic channel drawer interaction.
Constraint: Official ModelAPI create requests require grouped input object branches, and text-only generation should send only input.text.

Rejected: Always serializing empty image/video/audio arrays | optional oneOf branches should be omitted when not used.

Confidence: high

Scope-risk: narrow

Directive: Keep ModelAPI input media groups omitted when empty; do not make optional branches required without upstream contract evidence.

Tested: go test ./relay/channel/task/modelapiseedance -count=1; gofmt; git diff --check

Not-tested: full repository test suite; dto package unchanged in this narrow follow-up
Constrain archived video polling errors and logs to public task IDs plus generic phase/status context while preserving ModelAPI metrics labels and archive ordering.

Constraint: White-label polling output must not expose internal ModelAPI branding, upstream IDs, private URLs, or failure reasons.

Rejected: Logging sanitized failure reasons | Archived-channel logs may only expose public task IDs and status/phase style fields.

Confidence: high

Scope-risk: narrow

Directive: Keep channel=modelapi metrics labels stable even when user-visible/log output is scrubbed.

Tested: go test ./service -run Test(UpdateVideoSingleTask|RedactTechMobiVideoResponseBody); go test ./service -run TestUpdateVideoSingleTask(ModelAPIFailureRedactsDBAndLogs|ArchiveFailurePayloadRedactsDBAndLogs|ModelAPIArchiveFailureNoUpstreamLeaks|ModelAPIArchiveErrorDoesNotFinalizeOrSettle|ModelAPIArchivesAndSetsProxyURL|ModelAPIEmptySuccessURLDoesNotFinalizeOrSettle|ModelAPIRedactsStoredDataAndLogs|ModelAPIUnknownErrorFormatDoesNotLogRawResponse)$; git diff --check -- service/task_polling.go service/task_polling_video_result_test.go

Not-tested: Full repository test suite.
Constraint: Untrusted video asset URLs must not be delegated to proxy clients that resolve and connect outside dial-time SSRF controls.
Rejected: Proxy-side URL text validation only | it cannot prove the proxy will not resolve or connect to unsafe destinations.
Confidence: high
Scope-risk: narrow
Directive: Keep archive object keys task-stable; preserve historical dated object signing for existing records.
Tested: go test ./service -run 'VideoResult|Archived' -count=1; git diff --check -- service/video_result_storage.go service/video_result_storage_test.go
Not-tested: Full repository test suite.
Constraint: Only the classic ModelAPISeedance test file may change; leave service/backend and test logic untouched.

Rejected: Addressing regex-test review minor | explicitly out of scope for this lint-blocker fix.

Confidence: high

Scope-risk: narrow

Directive: Preserve the standard AGPL header on classic JS/JSX tests.

Tested: bun x eslint src/components/table/channels/modals/modelapi-seedance-classic.test.js; bun test src/components/table/channels/modals/modelapi-seedance-classic.test.js; bun run build

Not-tested: Browser UI, unchanged by this header-only fix.
Return fixed-phase errors for archived polling fetch/read/parse failures and remove internal channel IDs from video polling batch logs.

Constraint: White-label and archived-channel output may expose only public task IDs plus generic phase/status/bytes context; ModelAPI metrics labels remain unchanged.

Rejected: Wrapping upstream fetch/read/parse errors | outer polling logs would serialize private URLs, branded hosts, and upstream IDs.

Rejected: Logging video channel IDs in batch polling | ModelAPI channel IDs are internal routing details.

Confidence: high

Scope-risk: narrow

Directive: Do not log unvalidated upstream task status or failure reason text on archived video polling paths.

Tested: go test ./service -run TestUpdateVideo(SingleTaskModelAPI(FetchErrorDoesNotLeakUpstreamDetails|ReadErrorDoesNotLeakUpstreamDetails|ParseErrorDoesNotLeakUpstreamDetails|UnknownStatusDoesNotLeakUpstreamDetails)|TasksModelAPIDoesNotLogChannelID)$ -count=1; go test ./service -run 'Test(UpdateVideoTasks|UpdateVideoSingleTask|RedactTechMobiVideoResponseBody)' -count=1; git diff --check -- service/task_polling.go service/task_polling_video_result_test.go

Not-tested: Full repository test suite.
Constraint: Archive fetch proxy input is already fail-closed before client construction.
Rejected: Retaining dormant GetHttpClientWithProxy branch | future call-site drift could reintroduce proxy-side SSRF bypass.
Confidence: high
Scope-risk: narrow
Directive: Do not add proxy support to video result archival unless it preserves dial-time SSRF enforcement.
Tested: go test ./service -run 'VideoResult|Archived' -count=1; git diff --check -- service/video_result_storage.go; rg confirmed no GetHttpClientWithProxy reference in service/video_result_storage.go and helper signature has no proxy parameter.
Not-tested: Full repository test suite.
Constraint: Archived-channel logs must expose only public task identifiers and fixed white-label phases/status values.

Rejected: Reusing persisted progress text in source/archive errors | upstream-controlled values can carry provider brands, URLs, and secret task identifiers.

Confidence: high

Scope-risk: narrow

Directive: Never interpolate upstream or persisted progress text into archived-channel errors.

Tested: targeted and broad updateVideoSingleTask/updateVideoTasks service tests; gofmt; git diff --check.

Not-tested: Full repository suite is delegated to final verification.
Constraint: White-label regression tests must catch provider brands regardless of capitalization.

Rejected: Keeping the lowercase-only assertion | it could miss a brand-only leak using the canonical ModelAPI casing.

Confidence: high

Scope-risk: narrow

Directive: Normalize untrusted diagnostic text before asserting provider-brand absence.

Tested: TestUpdateVideoSingleTaskModelAPIArchiveFailureNoUpstreamLeaks; gofmt; git diff --check.

Not-tested: Full repository suite pending final verification.
… data

Constraint: ModelAPI tasks must retain the upstream identifier only in PrivateData for polling

Rejected: Persisting full submit or poll payloads | They expose provider identifiers beyond the private polling field

Confidence: high

Scope-risk: narrow

Directive: Keep provider task identifiers out of Task.Data and all client or log surfaces

Tested: ModelAPI adapter tests; targeted polling and content controller tests; scoped go vet; go build ./...

Not-tested: Live ModelAPI to GCS staging round trip
Constraint: ModelAPI Seedance must remain whitelabel and store completed video downloads through Google while exposing Flatkey content URLs.
Rejected: Allowing proxy for type 111 | stale DB/import proxy values could route sensitive upstream media paths outside the intended channel contract.
Rejected: Dropping oversized non-whitelabel submit errors | it hides useful bounded diagnostics for existing channels such as Doubao.
Confidence: high
Scope-risk: moderate
Directive: Keep ModelAPI Seedance proxy fail-closed at validation, UI payload, submit, and polling boundaries.
Tested: go test ./relay -run TestTaskSubmitStatusError -count=1; go test ./relay/channel/task/... ./dto/... -count=1 -timeout 10m; go test ./relay ./service ./controller -run Test(TaskSubmitStatusError|UpdateVideoSingleTaskModelAPI|UpdateVideoTasksModelAPI|ValidateChannelRejects|DoResponse|FetchTask|ModelAPI) -count=1 -timeout 10m; go vet ./relay ./relay/channel/task/modelapiseedance ./service ./controller; go build ./...; bun test src/features/channels/lib/channel-form.test.ts; bun test src/components/table/channels/modals/modelapi-seedance-classic.test.js; web/default bun run typecheck; web/default bun run build; web/classic bun run build; git diff --check; git diff --cached --check
Not-tested: Live ModelAPI submit/poll/archive flow against real credentials and production GCS IAM was not run in this workspace.
Constraint: type 111 must fail closed on real proxy settings while treating whitespace as unset

Rejected: mutating shared RelayInfo in place | would create request-scope side effects

Confidence: high

Scope-risk: narrow

Directive: preserve proxy-free submit and poll paths for ModelAPI Seedance channels

Tested: go test ./relay/channel/task/modelapiseedance -count=1; go test ./service -run 'ModelAPI|VideoResult|Archive|Sign' -count=1; go test ./controller -run 'TestValidateChannelRejectsModelAPISeedanceProxy|TestValidateChannelRejectsInvalidMaxConcurrency' -count=1; bun test src/features/channels/lib/channel-form.test.ts; bun test src/components/table/channels/modals/modelapi-seedance-classic.test.js; git diff --check
Constraint: Channel type 111 must remain internally backed by ModelAPI /v1/tasks while exposing only Flatkey /v1/videos submit and fetch routes.
Rejected: Public route-specific hacks or adding /v1/tasks | would broaden the public API and bypass the shared relay contract.
Confidence: high
Scope-risk: narrow
Directive: Keep type 111 public access limited to POST /v1/videos and GET /v1/videos/:task_id; legacy task routes must stay available for other platforms.
Tested: go test ./relay/channel/task/modelapiseedance -run TestValidateRequestAfterModelMappingRestrictsPublicSubmitEntrypoints -count=1; go test ./relay -run TestModelAPISeedancePrepareTaskAttemptRejectsLegacySubmitPathBeforePricing|TestModelAPISeedanceFetchRejectsLegacyRoutesAfterTaskLookup|TestNonModelAPISeedanceLegacyFetchRoutesRemainUsable -count=1; go test ./relay/channel/task/modelapiseedance -count=1; go test ./relay -count=1; git diff --check
Not-tested: Full repository test suite
Constraint: Reuse the existing task billing hooks and the upstream task-price snapshot.
Rejected: Synthetic public model tiers | They would leak billing choices into routing.
Confidence: high
Scope-risk: narrow
Directive: Keep the public model and response contracts unchanged while correcting non-null submit estimates.
Tested: git diff --check; GitNexus targeted impact LOW; origin/main change scan MEDIUM
Not-tested: No runtime code changed in this commit.
… path

Constraint: Implementation must reuse the existing asynchronous task billing and settlement hooks.
Rejected: Ad-hoc inline implementation | It would lose the required RED-GREEN and per-task review evidence.
Confidence: high
Scope-risk: narrow
Directive: Execute each task with spec review before code-quality review.
Tested: Placeholder scan; git diff --cached --check
Not-tested: Plan commands are exercised by subsequent implementation tasks
Constraint: Video input duration is not locally trustworthy, so reserve the documented 30-second bound until submit usage arrives.
Rejected: Media probing | It adds SSRF, latency, and still cannot reproduce upstream billing.
Confidence: high
Scope-risk: moderate
Directive: Keep estimated_usd private and retain fallback reservation when it is invalid or absent.
Tested: go test ./relay/channel/task/modelapiseedance -count=1
Not-tested: Live ModelAPI billing response
Constraint: One public model must represent all documented request tiers.

Rejected: Synthetic model aliases | They leak upstream billing tiers into routing.

Confidence: high

Scope-risk: narrow

Directive: Keep 0.14 as the calculation base and express full request cost through billable_units.

Tested: go test ./setting/ratio_setting -count=1; go test ./relay -run 'ModelAPISeedance|Billing' -count=1

Not-tested: Production account balances
Constraint: The feature branch was 98 commits behind the requested PR base.
Rejected: Open the PR from the stale base | It would defer integration conflicts and invalidate verification.
Confidence: high
Scope-risk: broad
Directive: Re-run billing, storage, controller, frontend, and full build checks after this merge.
Tested: git merge-tree --write-tree HEAD origin/main
Not-tested: Post-merge verification runs next
Constraint: characterization coverage only; production code stayed unchanged because the focused integration passed.

Rejected: production billing changes | existing ExecutePreparedTaskSubmit and controller settlement already propagate the adjusted quota and ratios.

Confidence: high

Scope-risk: narrow

Directive: Preserve the real ModelAPI submit boundary in this regression when changing Seedance task billing.

Tested: go test ./controller -run TestModelAPISeedanceSubmitEstimatedUSDSettlesAndPersistsAdjustedBilling -count=1 -v

Not-tested: full controller package and repo-wide suite.
ModelAPI Seedance task polling authenticates with the submit-time Bearer key, so queued and direct submits must persist the selected channel key just like TechMobi.

Constraint: ModelAPI Seedance has no materializer; the queued regression seeds an active binding to exercise the worker acceptance path without adding service proxy or frontend changes.

Rejected: Reusing current channel key during polling | rotated or multi-key channels can select a different key after submit.

Confidence: high

Scope-risk: narrow

Directive: Keep polling-key persistence confined to private task data; do not surface provider keys in logs or API responses.

Tested: go test ./model -run TestInitTaskPersists(TechMobi|ModelAPISeedance)SelectedKeyForPolling|TestTechMobiSubmittingFencePreservesSelectedKeyAfterExpiry|TestTaskPollingKeyPersistenceTrimsAndIgnoresBlankValues -count=1

Tested: go test ./controller -run Test(TechMobi|ModelAPISeedance)AssetTaskWorkerPersistsSelectedKeyAfterAcceptance|TestTechMobiAssetTaskWorkerPersistsSelectedKeyForUnknownSubmission -count=1

Tested: git diff --check

Not-tested: full repository test suite
TechMobi archive fetches historically honor channel proxy settings, but ModelAPI archive fetches must remain fail-closed and direct-only.

Constraint: ModelAPI Seedance proxy use is already blocked in controller, polling, and adaptor paths and must remain blocked before upstream/proxy contact.

Rejected: Keep the storage-layer proxy rejection unconditional | it breaks TechMobi channels that rely on configured archive proxies.

Confidence: high

Scope-risk: narrow

Directive: Do not route ModelAPI archive downloads through GetHttpClientWithProxy; keep non-empty proxy rejection before client construction.

Tested: go test ./service -run 'TestArchiveVideoResult|TestVideoResultDirectFetchClientRejectsDialTimePrivateIP|TestUpdateVideoSingleTask(ArchivePersistsMetadataBeforeSuccessSettlement|ArchiveErrorDoesNotFinalizeOrSettle|ModelAPIRejectsProxyBeforeFetchOrArchive|ModelAPIArchiveErrorDoesNotFinalizeOrSettle|ModelAPIArchiveFailureNoUpstreamLeaks)' -count=1; go test ./controller -run 'TestValidateChannelRejectsModelAPISeedanceProxy|TestModelAPIVideoProxyWithoutArchiveDoesNotFetchUpstream|TestArchivedTechMobiVideoRedirect' -count=1; go test ./relay/channel/task/modelapiseedance -run 'TestDoRequestRejectsProxyWithoutUpstreamRequest|TestDoRequestTreatsWhitespaceProxyAsEmpty|TestFetchTaskRejectsProxyWithoutUpstreamRequest|TestFetchTaskWithContextTreatsWhitespaceProxyAsEmpty' -count=1

Not-tested: repo-wide go test ./... skipped per instruction due shared DB conflicts and low C: space.
Constraint: scoped to the four current failing Prettier channel files

Rejected: broader frontend formatting | would touch unrelated files outside ownership

Confidence: high

Scope-risk: narrow

Directive: keep future channel UI edits formatted with package-local Prettier

Tested: web/default bun x prettier --check src/features/channels/constants.ts; web/classic bun x prettier --check src/constants/channel.constants.js src/helpers/render.jsx src/components/table/channels/modals/EditChannelModal.jsx; web/default bun test src/features/channels/constants.test.ts; web/classic bun test src/components/table/channels/modals/modelapi-seedance-classic.test.js; git diff --check; format HEAD copies compare equal after package-local Prettier

Not-tested: full frontend builds
@KingCesc

Copy link
Copy Markdown

🤖 OpenCodeReview · 评审 commit ffa8fac8 · 共 2 条

service/video_result_storage.go

  • L167: [阻塞] 代理下载分支直接使用 GetHttpClientWithProxy 返回的客户端,未设置 videoResultCheckRedirect。这样虽然首个 upstreamURL 会经过 videoResultValidateURL,但 3xx 跳转后的 URL 会由默认策略自动跟随,可能绕过 SSRF/域名/IP/端口限制并访问内网地址;视频归档接口处理外部 URL 时这是安全风险。建议像直连分支一样对代理客户端克隆后设置 CheckRedirect(必要时也显式设置超时),不要修改共享客户端实例。
baseClient, err := GetHttpClientWithProxy(proxy)
		if err == nil {
			cloned := *baseClient
			cloned.Timeout = cfg.FetchTimeout
			cloned.CheckRedirect = videoResultCheckRedirect
			client = &cloned
		}

service/task_polling.go

  • L507-508: [严重] ModelAPI Seedance 成功轮询时会在任务状态 CAS 更新前先执行归档下载/上传;如果两个轮询 worker 同时处理同一任务,它们都会进入这里触发 GCS 归档,后续虽然只有一个 UpdateWithStatus 会赢得结算,但外部归档副作用已经发生,可能造成重复下载/上传、覆盖同一对象或额外资源消耗。建议将归档做成幂等(例如按 taskID 加锁/先查询已存在对象)或把“抢占终态更新”放到归档前,确保只有赢得状态迁移的 worker 执行归档。
case constant.ChannelTypeModelAPISeedance:
				// TODO: ensure archive is guarded by a per-task idempotency lock or only executed by the CAS winner.
				videoResult, archiveErr = archiveModelAPIVideoResult(ctx, task.TaskID, taskResult.Url, proxy)

Constraint: ModelAPI documents HTTPS media inputs but no upstream asset-library API.
Rejected: Persisting signed URLs or fabricating AssetBinding rows | Both would misrepresent provider state and expire independently.
Confidence: high
Scope-risk: moderate
Directive: Keep production channel binding; skip only ModelAPI asset materialization.
Tested: git diff --check on the design document
Not-tested: Implementation and runtime behavior are handled by the following plan.
Constraint: ModelAPI exposes URL inputs but no upstream asset-library API; tests must remain offline.
Rejected: Reusing provider AssetBinding rows for signed URLs | it would persist ephemeral credentials and invent an unsupported upstream contract.
Confidence: high
Scope-risk: moderate
Directive: Keep source-url:modelapi separate from binding-backed materializers and sign only after all references validate.
Tested: Plan self-review against the committed design specification and placeholder scan.
Not-tested: Production code is intentionally unchanged by this planning commit.
URL-native asset inputs now stay usable across provider execution, archived results are fenced by renewable leases, and deterministic capacity rejection can fall back without changing other task providers.

Constraint: ModelAPI exposes URL inputs and asynchronous video results while Flatkey must preserve its own public URLs and avoid live verification charges.

Rejected: Treating all task-provider 4xx responses as known rejection | It changes queued retry behavior outside ModelAPI Seedance.

Confidence: high

Scope-risk: moderate

Directive: Keep ModelAPI known-rejection handling limited to channel type 111 plus HTTP 429, and keep provider input signing separate from user download TTLs.

Tested: targeted Go behavior tests; affected-package compile checks; scoped go vet; go build ./...; git diff --check.

Not-tested: Live ModelAPI submission and live GCS/IAM signing were intentionally skipped to preserve upstream balance and avoid production side effects.
@think-back

Copy link
Copy Markdown
Collaborator Author

已在 4eb5d9344 修复这条评论中的两个阻塞项:

  1. service/video_result_storage.go:代理 HTTP client 现在先 clone,再设置归档超时和 videoResultCheckRedirect,重定向链继续执行 URL/SSRF 校验。
  2. service/task_polling.go / model/task.go:ModelAPI 归档前先原子 claim task lease;归档期间按 lease TTL 动态 heartbeat 续租,丢 lease 会取消 archive context;最终状态 CAS 使用最新 owner/expiry fence,旧 owner 也无法释放新 owner 的 lease。

补充修正:provider-facing 素材 GCS URL 使用独立 12 小时 TTL,避免异步排队超过通用 1 小时下载上限;容量语义失败只在 ModelAPISeedance + 429 进入现有渠道 fallback,不改变其他 task provider。

验证已在 E 盘缓存和黑洞代理环境完成:定向测试、受影响包编译、scoped go vetgo build ./...git diff --check 均通过。未调用真实 ModelAPI/GCS,临时 mock/测试文件未提交。

@think-back
think-back merged commit 8d2fcf4 into main Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants