Skip to content

feat(task): replace built-in task adaptors with a sandboxed JS plugin system - #7076

Merged
Calcium-Ion merged 13 commits into
mainfrom
feat/plugin-task-upstream
Aug 29, 2026
Merged

feat(task): replace built-in task adaptors with a sandboxed JS plugin system#7076
Calcium-Ion merged 13 commits into
mainfrom
feat/plugin-task-upstream

Conversation

@Calcium-Ion

Copy link
Copy Markdown
Member

🔗 关联任务 / Related Issue

  • Closes #

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

📝 变更描述 / Description

将任务系统(视频/音乐等异步任务平台)从硬编码的 Go 适配器迁移为沙箱化 JS 插件架构,任务平台的接入、路由、计费与管理全部插件化:

插件引擎与运行时

  • 基于 goja 的沙箱 JS 插件引擎(pkg/jsplugin),插件通过 meta 声明 key、渠道类型、模型、协议与计费 schema;10 个原厂插件(alibaba/kling/jimeng/vidu/doubao/google/vertex-ai/hailuo/sora/sunoapi)内嵌随二进制发布。
  • 双层注册表:出厂层 + 自定义覆盖层(同 key 覆盖出厂),发布不可变 routing generation,请求全程 pin 在同一代插件上。
  • 任务中继(submit/query/回调/结算)迁移至插件适配器,声明式路由(declarative routes + shared endpoints),多实例下沿用既有 ClaimSystemTask 租约做上游轮询。

协议与选择期门控

  • protocols 声明强制显式 supports(如 openai_responses 的 stream/sync/background),加载期双向校验 hook 与声明一致;插件市场等外部系统可静态解析插件真实支持的模式。
  • 请求形态(stream/background 标志)在渠道选择期即过滤不支持的插件,不满足时返回 400 并列出该模型支持的形态——在任何插件 hook 执行和计费之前。

计费

  • 插件只声明计费事实(usage facts:秒数、档位、credits 等),价格由运营方表达式决定;预扣与结算复用 quota 饱和/审计既有不变量,上游权威扣费数(如 kling final_unit_deduction)支持小数结算。

管理与权限

  • Root 插件控制台:上传(文件/URL/市场)、版本管理、启停、详情(元数据/端点/源码);插件市场支持官方源与自定义源。
  • 新增 authz 权限 task_plugin.bind(默认仅 root,可通过权限编辑器授予),控制 type-61 渠道的创建/编辑/复制与插件列表查询;插件管理本身保持 root-only。
  • 三级开关:TaskPluginEnabled 总开关(关闭后清空路由,等价于停用整个任务系统)、自定义插件开关、出厂插件按 key 禁用(与自定义插件相同的在用守卫:绑定渠道 + 在途任务)。
  • 禁用后的错误可行动化:总开关关闭 → task_plugin_system_disabled;单插件禁用 → task_plugin_disabled 并指名插件 key(旧数字渠道类型自动解析,如 17 → "alibaba");未知平台保留 invalid_api_platform

其他

  • 插件描述与 usage schema 支持多语言(LocalizedText,BCP-47);制品存储后端 + 签名访问层;前端插件控制台、渠道抽屉插件绑定、插件感知的计费编辑器,i18n 覆盖 7 种语言。

📸 运行证明 / Proof of Work

Snapzy_2026-08-29_18-50-22_950

✅ 提交前检查项 / Checklist

  • 人工确认: 无论描述是否由 AI 生成,我已审阅全部内容,并声明对其准确性与完整性负责。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • 新功能关联 Issue: 若此 PR 标记为 New feature,我已关联对应 Issue;若尚无 Issue,我已先自行创建。
  • 事前沟通: 若改动较大或涉及方向性变更,已在关联 Issue 中与维护者沟通并达成一致。
  • 功能范围: 本 PR 不是 Coding Plan、逆向渠道、第三方封装接口,也不是对 Codex 渠道类型的改动。
  • 范围聚焦: 本 PR 为一项聚焦改动,未包含无关代码。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

Introduce the task-plugin foundation as a self-contained layer, with no
host wiring yet (routing, billing, and management land in follow-ups):

- pkg/jsplugin: goja-based sandboxed engine, plugin registry with
  generation tracking, declarative route table, fixture replay harness,
  and usage-schema metadata (fields, examples, billing clamps).
- plugins/: ten embedded factory plugins (alibaba, doubao, google,
  hailuo, jimeng, kling, sora, sunoapi, vertex-ai, vidu) registered at
  init via embed.FS.
  Sora, doubao, and alibaba accept multipart /v1/videos alongside JSON:
  full scalar passthrough, single-value form semantics, metadata as one
  JSON-object-string field, and per-vendor file policies (sora forwards
  a single input_reference; doubao/alibaba direct files to URLs).
  Doubao pre-charges at the documented upper bound (15s x 1080p) and
  settles down from real usage.
- pkg/billingexpr: task usage inputs for tiered expressions, plus
  UsedUsageKeys for schema validation of u() references.
- plugin_cli.go: offline "plugin" subcommand for fixture replay.
- docs/plugin-api: frozen v1 plugin contract (markdown, .d.ts, JSON
  schema).
- constant: ChannelTypeTaskPlugin and bounds-checked GetChannelBaseURL,
  adopted by existing callers that indexed ChannelBaseURLs directly.
- meta.icon supports LobeHub icon names and text avatars ("text" /
  "text:<label>") for logo-less vendors.
Task outputs (videos, images, audio) need durable storage and safe
serving independent of upstream URL lifetimes. Add the storage contract
before any producer exists:

- types.TaskArtifact plus service.TaskArtifactStore with Resolve /
  Persist / Serve, a disabled default, and configuration under
  setting/system_setting (backend selection, TaskPublicAddress for
  externally reachable content URLs).
- HMAC-signed access verification (service/task_artifact_access) keyed
  by common.CryptoSecret, enforced by a dedicated middleware; the access
  query parameter is redacted from request logs so signed URLs cannot
  be replayed from log storage.
…uting

Replace the eleven per-vendor Go task adaptors (ali, doubao, gemini,
hailuo, jimeng, kling, sora, suno, vertex, vidu, and the middleware
request converters) with a single jsplugin bridge adaptor that drives
the embedded plugins. Legacy vendor routes (/suno, /kling, /jimeng,
/v1/videos, ...) are preserved by mapping each legacy platform to its
plugin key, so existing clients keep working unchanged.

Host-side plumbing that makes the swap complete:

- Declarative routing: plugin route tables are compiled into a
  generation-safe dispatcher (router/plugin-router.go); shared
  endpoints run through pinned plugins resolved at auth time, and
  NoRoute falls through the dispatcher before the web handler.
- Task protocol bridge: /v1/responses-compatible submit/stream/retrieve
  for plugin tasks, including background mode, driven purely by
  database observation with client-connection ticks bounded by
  TASK_PLUGIN_PROTOCOL_* env knobs. Disconnected clients no longer
  cancel running tasks.
- Channel binding: task-plugin channels carry TaskPluginKey in channel
  settings; ability selection gains constraint filters (identity,
  origin-task affinity) so retries and fetches stay on the channel
  that owns the task.
- Billing: submit-time estimates come from plugin extractUsage into
  tiered-expression snapshots; settlement recomputes from real usage,
  failed tiered tasks are fully refunded, and consume logs carry
  billing_mode, expr_b64, matched_tier, and structured usage_facts.
- OpenAI-compatible projection: correct queued/in_progress/completed
  status mapping, model and completed_at fields, and artifact
  persistence wired into task polling (Gemini video proxy is subsumed
  by the generic artifact content endpoint).
- Trusted-proxy resolution moves to common/ so the standalone plugin
  router engine can share it.

The old RelayTaskInfo response DTO and Suno action constants go away
with the adaptors; task submission now returns transport-independent
TaskSubmitResponse values parsed by the bridge.
Operator-facing plane on top of the runtime:

- Management APIs (root-only) for uploading, versioning, activating,
  disabling, and deleting task plugins, with preflight conflict checks
  and a dry-run sandbox against fixture inputs. Database-persisted
  plugins hot-reload across nodes via SyncTaskPlugins, gated by
  TASK_PLUGIN_OVERRIDE_ENABLED; a "plugin" CLI entrypoint is wired
  into main.
- Marketplace: configurable source list with URL import and sha256
  integrity checks against the published index.
- Pricing: model pricing exposes each plugin's usage schema and
  examples; task tiered expressions are validated against that schema
  with SmokeTestTaskExpr (literal u() keys must be declared, generated
  vectors must stay finite and non-negative); task_pricing_setting
  stores the per-model usage matrix.
- Channel admin: task-plugin channels are validated against the
  registry, pinned-channel retry keeps plugin identity, and channel
  test/billing/model listings understand plugin-backed channel types.
- Log hygiene: root-only diagnostics under other.root_info are
  stripped for non-root viewers via FormatAdminLogs.
Frontend counterpart for the plugin runtime:

- Task plugins page: card/table views, detail sheet with source viewer
  (CodeMirror) and diff, upload dialog with preflight conflicts, dry-run
  sandbox, marketplace panel with source management and integrity-
  checked installs, and LobeHub/text-avatar plugin icons.
- Pricing: task usage matrix editor and tiered-expression editor driven
  by each plugin's usage schema; the public pricing page renders the
  tier matrix with combination labels expanded from uniform tiers.
- Usage logs: task log details show billing parameters again
  (billing_mode, decoded expression, matched tier, structured
  usage_facts) and highlight the matched matrix row by label with a
  facts-based fallback; task rows are labeled Async instead of
  Stream/Non-stream; artifact previews and plugin-author attribution
  render in the task details dialog; mobile cards get a task layout.
- Channels: task-plugin channel type with plugin binding controls.
- i18n: new keys synced across the seven locales; '1M token' and
  'credit' stay English everywhere by policy and join the sync skip
  list; generated sync reports are gitignored.
- Dev server proxies /v1 so plugin protocol endpoints work under
  rsbuild.
Task-plugin rejections previously collapsed every failure into a fixed
"Invalid request" body while the underlying error text was dropped
before logging, leaving both callers and admins blind.

- pkg/jsplugin: JS hook throws now produce a typed *HookError whose
  Message is the sanitized JS error message (engine prefixes stripped,
  512-rune cap, control characters scrubbed; extraction is panic-safe
  against throwing getters). Timeouts, interrupts, and missing-hook
  errors stay plain errors and are never surfaced.
- middleware: every rejection site passes a specific detail through the
  sanitized error path (hook message, host request-decode reason, or a
  fixed "plugin returned an invalid route result" for structural plugin
  bugs). 5xx responses always keep the generic message. The
  native.error render hook receives the real message plus requestId,
  and host-fallback bodies append the request id.
- controller: the protocol bridge passes validation messages through on
  400 responses with invalid_request codes only.
- Rejections now log at WARN with the full wrapped error text so admins
  can diagnose without DEBUG logging.
- logger.LogWarn accepts printf args like LogDebug.
…lign google/vertex video billing

Host: BuildRequestBody's JSON branch now resolves {__fileRef, encoding,
mimeType?, maxBytes?} placeholders by inlining the referenced multipart
file as raw base64 or a data URL. Resolution is strictly scoped to the
current request's multipart form (unknown refs and extra keys are
rejected), per-file and total inlined bytes are capped, and plugins
still never see file bytes. This unblocks vendors whose native APIs
accept only JSON-embedded images (Gemini inlineData, Vertex
bytesBase64Encoded, and the batch-2/3 vendors).

google/vertex-ai plugins:
- openai_video.decodeRequest accepts multipart (sora conventions:
  first() single-value semantics, metadata as a JSON object string);
  JSON-branch behavior is unchanged for JSON callers.
- OpenAI seconds is normalized into requestBody.duration once, so
  extractUsage and buildSubmitRequest read the same field; billed facts
  now always match the final outbound request. Veo enums are enforced
  (seconds 4/6/8, resolution 720p/1080p/4k) instead of the 1..3600
  tolerance.
- google emits Gemini-current inlineData image objects and
  numberOfVideos (was Vertex-style bytesBase64Encoded and the
  undocumented sampleCount); vertex keeps its correct shape and
  enforces image/jpeg|png.
- vertex gains the generate_audio billing fact (audio and muted tiers
  are priced differently); registry usageSchema supports boolean fields
  and expression smoke vectors enumerate false/true for them.
- extractUsageOnComplete now returns null on both plugins: the poll
  responses carry no usage fields, so settlement reuses submit-time
  facts instead of guessing.

Known gap (deliberate, next batch): JSON callers passing
input_reference/image strings still lose the image on the openai_video
path; multipart is fixed first because the official SDK sends
multipart. Model catalogs are intentionally untouched.
jimeng:
- openai_video.decodeRequest accepts multipart (first() single-value
  semantics, metadata as a JSON object string); JSON-branch behavior is
  unchanged for JSON callers. OpenAI seconds normalizes into
  requestBody.duration once.
- Seconds are enforced per final req_key: S2.0 (jimeng_vgfm_*_l20) is
  fixed at 5s, 3.0 req_keys allow 5 or 10 (frames 121/241).
- Multipart input_reference becomes a base64 __fileRef placeholder in
  binary_data_base64[] (4.7MB cap); JSON image URLs keep image_urls[].
- Two real req_key defects fixed: submitting images on S2.0 now
  switches to the official jimeng_vgfm_i2v_l20, and buildQueryRequest
  uses the submit req_key persisted on taskData instead of a hardcoded
  t2v key (i2v and 3.0 tasks previously queried the wrong capability).
- Empty aspect_ratio is no longer sent; OpenAI size maps onto the
  official enum when derivable.
- Billing facts are {seconds, product} derived from the same functions
  that build the outbound request, so pre-charge always matches what is
  sent upstream. product is an enum tier (s2_pro/v30_720p/v30_1080p/
  v30_pro) since Volcengine prices per tier-second. The query response
  has no stable usage fields, so extractUsageOnComplete returns null.

hailuo:
- Same multipart convention and single seconds->duration normalization.
- Official combination matrix enforced per model: 2.3-Fast is
  image-to-video only; 6s allows 768P/1080P (plus 512P on 02 i2v);
  10s allows 768P (plus 512P on 02 i2v). 01-series models stay
  permissive because the official tables contradict themselves on
  their 1080P support (noted in a comment).
- Resolution normalization fixed: 720-sized requests map to 768P on
  2.3/02 (their real tier) and 720P only on the 01 series;
  T2V-01-Director default corrected to 720P.
- Multipart input_reference becomes a dataUrl placeholder on the native
  metadata.first_frame_image field; JSON input_reference/image URLs now
  land there too instead of flipping the action while dropping the
  image.
- extractUsageOnComplete calibrates resolution from
  video_width/video_height only and returns null otherwise; it never
  fabricates duration.

usageExamples labels list fact combinations only. Vendor list prices
never belong in plugin code or metadata: pricing is the operator's
expression, and hardcoded currency figures go stale and mislead.
…horitative settlement

Host: validateUsageValue no longer truncates fractional credit/token
facts. It bound-checks via common.QuotaFromFloatChecked (int32
saturation preserved, negatives still rejected upstream) and returns
the original decimal when unclamped, so an upstream deduction like
kling final_unit_deduction "3.5" settles as 3.5 instead of 3.

kling:
- openai_video.decodeRequest accepts multipart (sora conventions:
  first() single-value semantics, metadata as a JSON object string);
  JSON-branch behavior unchanged. Multipart input_reference becomes a
  base64 __fileRef placeholder on the native image field (10MB cap);
  JSON input_reference/image strings land there too.
- Billing facts derive from the official unit-consumption table
  (units per output video second, per model x mode — not a currency
  price) and the resolved mode: kling-v2-master rejects std and
  defaults to pro; other models default std. The resolved mode is
  preserved onto the outbound request so estimate and submit agree.
- extractUsageOnComplete returns the exact decimal
  final_unit_deduction as the authoritative settlement fact, or null
  when the field is absent.

vidu:
- Same multipart convention; input_reference becomes a dataUrl
  placeholder in images[0] (15MB cap); JSON image URLs land there too.
- Official combination matrix enforced: vidu2.0 has no text2video and
  allows 4s (360p/720p/1080p) or 8s (720p only); viduq1 is fixed 5s
  1080p; q2 stays permissive (1..10s). Dimension-style resolution
  strings normalize onto the official tier enum.
- Submit-time facts are dimensions only ({duration, resolution});
  credits never come from a plugin-side formula. Settlement uses the
  upstream credits field from submit/query responses when present
  (schema declares it; submit-time facts legitimately omit it, covered
  by wantSubmitUsageKeys in the shared plugin test helper).
Host: new LocalizedText type for plugin display copy. Plugin source may
use a bare string (normalized to {"en": s}) or a locale map that must
include a non-empty "en". Both decode paths — meta.description and
usageSchema field descriptions — share one validator: at most 16
locales, BCP-47-shaped tags canonicalized for exact-match lookups
(zh-tw -> zh-TW, EN -> en, zh-hans -> zh-Hans), duplicate-after-
canonicalization rejected, values trimmed, control characters rejected,
rune caps of 512 (description) / 256 (usage field). API responses
always emit the object form; cloneMeta and pricing snapshots deep-copy
the maps.

plugins: all ten vendor manifests declare {en, zh} descriptions and
localize their usageSchema field descriptions.

docs/plugin-api: v1.md, v1.d.ts, and v1.schema.json document the
LocalizedText contract for description fields.

Marketplace defaults: the built-in source list is now the official
newapi.ai index plus the GitHub raw index; both count as first-party in
the console (the at-your-own-risk label stays for other sources).

web: resolveLocalizedText maps i18next language codes (en / zhCN /
zhTW / fr / ru / ja / vi) onto BCP-47 keys with fallback exact tag ->
primary subtag -> en -> first key in sorted order; consumers are the
plugin cards, plugins table, detail sheet, marketplace panel and cards,
usage schema table, and the task usage pricing editor. Plugin and
marketplace cards pick up the description line with an inline stats
row, and the usage schema table gains a compact stacked mode so long
schemas scroll inside the card instead of stretching the grid.
…ction-time gating

meta.protocols entries claiming a mode-bearing protocol must now use
object form with an explicit supports list; the bare-string form stays
valid only for protocols without modes (openai_video). openai_responses
defines the modes stream (stream: true -> renderEvents), sync (neither
flag -> renderFinal), and background (background: true -> renderFinal).
supports entries are validated (known mode, unique, at least one) and
normalized into host-table order; "retrieve" is called out as never
declarable since every created response is always retrievable. The mode
vocabulary is append-only under apiVersion 1.

Load-time verification is bidirectional: a hook required by a supported
mode but not exported rejects the plugin, and an exported mode hook
that no supported mode uses also rejects it — both errors name the
concrete fix (implement the hook, or adjust supports).

Selection-time gating (middleware): the required request form is parsed
from the JSON body's stream/background flags and protocol candidates
that don't support it are filtered out before pinning; when none
remain, the request fails with a 400 that names the forms the model
does support — before any plugin hook runs and before billing.

Dispatch (controller): the pinned plugin is re-checked against the
request form as defense in depth (500, since selection should have
filtered). Retrieval for stream-only plugins renders at terminal
SUCCESS via PluginResponsesMachine.FinalFromEvents — one renderEvents
call with no previous state, run on a scratch machine so a hook failure
leaves the failure-envelope path intact. renderEvents is now always
callable on the stream path (its presence is guaranteed by load-time
verification).

plugins: all ten vendor manifests migrate their openai_responses claim
to object form with supports: ["stream", "sync", "background"].

web: the plugin detail sheet gains a metadata/endpoints card driven by
lib/host-protocols.ts — host protocol endpoint table with per-plugin
supported request forms, native routes, and model-scope hints.
Upload dialog: the source textarea becomes a CodeMirror editor with
javascript highlighting, a byte counter, and a placeholder (no
autofocus, so the risk warning and file picker stay in view). The
native file input is replaced by a PluginSourcePicker drop zone, and
URL import moves into its own PluginUrlImportField component that owns
normalization and fetch-error copy. The dialog switches to the shared
Dialog wrapper and Field layout, and a parsed-metadata confirmation
panel replaces the raw result dump.

code-block: the CodeMirror editor gains javascript/jsx/typescript/tsx
language extensions plus placeholder and autoFocus props.

Plugin cards: a card answers "which plugin is this and is it live" —
identity, source and runtime badges, version pills, bound models
(capped at four with an overflow tooltip), and the enable toggle.
Manifest detail (billing parameters, endpoints, source) belongs to the
detail sheet, so the card no longer renders the usage schema and the
schema table drops its now-unused compact mode.
…kill switches

Authorization: new authz resource task_plugin.bind with no default
roles — root always passes, admins need an explicit grant through the
permission editor. It guards GET /api/task_plugin_options and the
create/update/copy paths of task-plugin (type 61) channels; the plugin
admin console itself stays root-only. The channel drawer hides the
type-61 option and the plugin picker without the permission, and keeps
the bound plugin key read-only when editing an existing channel.

Registry: two new switches compose with the existing custom-plugin
override toggle. TaskPluginEnabled is the master switch — when off,
prepareGeneration publishes an empty routing generation, so every
rebuild path (register, unregister, replace overrides, the other
toggles) is covered structurally and flipping it back republishes the
intact layers. TaskPluginDisabledFactoryKeys disables individual
factory plugins: the per-plugin console toggle now works for factory
rows too, with the same in-use guard (bound channels, in-flight tasks)
as custom plugins; disabled factory keys are filtered from routing,
shown as "disabled" in the console, and hidden from the channel plugin
picker. Both persist as options (env default TASK_PLUGIN_ENABLED) and
sync across nodes; updateOptionMap uses lock-free parse variants since
it already holds the option write lock.

Errors: when no adaptor serves a task platform, the rejection now says
why — task_plugin_system_disabled when the master switch is off,
task_plugin_disabled naming the plugin key (legacy numeric channel
types resolve through taskPluginKeys, so type 17 reports "alibaba")
when the resolved factory plugin is disabled, and the legacy
invalid_api_platform only for platforms that name nothing.

web: the console master toggle now drives TaskPluginEnabled; the
custom-plugin override switch is no longer exposed in the UI.
@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 334 files, which is 34 over the limit of 300.

To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8dab4a3e-2f1f-4958-9945-7ee5ed6d8277

📥 Commits

Reviewing files that changed from the base of the PR and between 7037ac1 and 0da8a0c.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • web/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (334)
  • CLAUDE.md
  • THIRD-PARTY-LICENSES.md
  • common/api_type.go
  • common/api_type_task_plugin_test.go
  • common/init.go
  • common/trusted_proxies.go
  • constant/channel.go
  • constant/channel_test.go
  • constant/context_key.go
  • constant/env.go
  • constant/task.go
  • constant/task_test.go
  • controller/billing_option_test.go
  • controller/channel-billing.go
  • controller/channel-test.go
  • controller/channel.go
  • controller/channel_pin_retry_test.go
  • controller/channel_task_plugin_bind_test.go
  • controller/channel_task_plugin_validation_test.go
  • controller/channel_upstream_update.go
  • controller/log.go
  • controller/model.go
  • controller/option.go
  • controller/plugin_endpoint_test.go
  • controller/plugin_native_e2e_test.go
  • controller/plugin_protocol.go
  • controller/plugin_protocol_limiter.go
  • controller/plugin_protocol_limiter_test.go
  • controller/plugin_protocol_sdk_test.go
  • controller/plugin_protocol_test.go
  • controller/relay.go
  • controller/relay_task_plugin_test.go
  • controller/task.go
  • controller/task_generic_test.go
  • controller/task_log_view_test.go
  • controller/task_plugin.go
  • controller/task_plugin_debug.go
  • controller/task_plugin_debug_test.go
  • controller/task_plugin_test.go
  • controller/video_proxy.go
  • controller/video_proxy_gemini.go
  • docs/plugin-api/README.md
  • docs/plugin-api/v1.d.ts
  • docs/plugin-api/v1.md
  • docs/plugin-api/v1.schema.json
  • dto/channel_constraints.go
  • dto/channel_constraints_test.go
  • dto/plugin_protocol.go
  • dto/task.go
  • dto/task_plugin.go
  • e2e/doc_parse_test.go
  • go.mod
  • logger/logger.go
  • main.go
  • middleware/auth.go
  • middleware/body_cleanup.go
  • middleware/distributor.go
  • middleware/distributor_test.go
  • middleware/jimeng_adapter.go
  • middleware/kling_adapter.go
  • middleware/logger.go
  • middleware/task_artifact_access.go
  • middleware/task_artifact_access_test.go
  • middleware/task_plugin.go
  • middleware/task_plugin_origin_task_test.go
  • middleware/task_plugin_test.go
  • middleware/trusted_proxies.go
  • middleware/utils.go
  • model/ability.go
  • model/channel.go
  • model/channel_cache.go
  • model/channel_constraint.go
  • model/channel_constraint_test.go
  • model/log.go
  • model/log_format_test.go
  • model/main.go
  • model/option.go
  • model/option_task_plugin_test.go
  • model/pricing.go
  • model/pricing_usage_schema_test.go
  • model/task.go
  • model/task_cas_test.go
  • model/task_openai_video_test.go
  • model/task_plugin.go
  • model/task_plugin_channel_select_test.go
  • model/task_plugin_test.go
  • pkg/billingexpr/compile.go
  • pkg/billingexpr/compile_usage_test.go
  • pkg/billingexpr/expr.md
  • pkg/billingexpr/run.go
  • pkg/billingexpr/settle.go
  • pkg/billingexpr/task_usage_test.go
  • pkg/billingexpr/types.go
  • pkg/jsplugin/cli.go
  • pkg/jsplugin/cli_test.go
  • pkg/jsplugin/engine.go
  • pkg/jsplugin/engine_test.go
  • pkg/jsplugin/fixture.go
  • pkg/jsplugin/fixture_test.go
  • pkg/jsplugin/protocol_supports_test.go
  • pkg/jsplugin/registry.go
  • pkg/jsplugin/registry_disabled_factory_test.go
  • pkg/jsplugin/registry_master_enabled_test.go
  • pkg/jsplugin/registry_test.go
  • pkg/jsplugin/request.go
  • pkg/jsplugin/routing.go
  • pkg/jsplugin/routing_test.go
  • pkg/jsplugin/utils.go
  • plugins/.oxfmtrc.json
  • plugins/.oxlintrc.json
  • plugins/alibaba_responses_test.go
  • plugins/builtin_plugins_test.go
  • plugins/doubao_responses_test.go
  • plugins/embed.go
  • plugins/google_responses_test.go
  • plugins/hailuo_responses_test.go
  • plugins/jimeng_responses_test.go
  • plugins/kling_responses_test.go
  • plugins/sora_responses_test.go
  • plugins/sunoapi_responses_test.go
  • plugins/tasks/alibaba/plugin.js
  • plugins/tasks/doubao/plugin.js
  • plugins/tasks/google/plugin.js
  • plugins/tasks/hailuo/plugin.js
  • plugins/tasks/jimeng/plugin.js
  • plugins/tasks/kling/plugin.js
  • plugins/tasks/sora/plugin.js
  • plugins/tasks/sunoapi/plugin.js
  • plugins/tasks/vertex-ai/plugin.js
  • plugins/tasks/vidu/plugin.js
  • plugins/vertex_ai_responses_test.go
  • plugins/video_responses_test_helpers_test.go
  • plugins/vidu_responses_test.go
  • relay/channel/adapter.go
  • relay/channel/api_request.go
  • relay/channel/api_request_test.go
  • relay/channel/minimax/relay-minimax.go
  • relay/channel/replicate/adaptor.go
  • relay/channel/task/ali/adaptor.go
  • relay/channel/task/ali/adaptor_test.go
  • relay/channel/task/ali/constants.go
  • relay/channel/task/doubao/adaptor.go
  • relay/channel/task/doubao/constants.go
  • relay/channel/task/gemini/adaptor.go
  • relay/channel/task/gemini/billing.go
  • relay/channel/task/gemini/dto.go
  • relay/channel/task/gemini/image.go
  • relay/channel/task/hailuo/adaptor.go
  • relay/channel/task/hailuo/constants.go
  • relay/channel/task/hailuo/models.go
  • relay/channel/task/jimeng/adaptor.go
  • relay/channel/task/jsplugin/adaptor.go
  • relay/channel/task/jsplugin/adaptor_test.go
  • relay/channel/task/jsplugin/auth.go
  • relay/channel/task/jsplugin/auth_test.go
  • relay/channel/task/kling/adaptor.go
  • relay/channel/task/sora/adaptor.go
  • relay/channel/task/sora/adaptor_test.go
  • relay/channel/task/sora/constants.go
  • relay/channel/task/suno/adaptor.go
  • relay/channel/task/suno/models.go
  • relay/channel/task/vertex/adaptor.go
  • relay/channel/task/vidu/adaptor.go
  • relay/channel/volcengine/adaptor.go
  • relay/channel/zhipu_4v/adaptor.go
  • relay/common/relay_info.go
  • relay/common/relay_utils.go
  • relay/common/relay_utils_test.go
  • relay/constant/relay_mode.go
  • relay/plugin_protocol.go
  • relay/plugin_protocol_test.go
  • relay/relay_adaptor.go
  • relay/relay_adaptor_jsplugin_test.go
  • relay/relay_task.go
  • relay/relay_task_test.go
  • relay/task_platform_error_test.go
  • relay/task_platform_test.go
  • relaykit/dto/channel_settings.go
  • router/api-router.go
  • router/main.go
  • router/plugin-router.go
  • router/plugin_router_test.go
  • router/relay-router.go
  • router/retired_frontend_routes_test.go
  • router/task-plugin-protocol-router.go
  • router/task-router.go
  • router/task_plugin_options_router_test.go
  • router/task_plugin_protocol_router_test.go
  • router/task_router_test.go
  • router/video-router.go
  • router/video_router_test.go
  • router/web-router.go
  • service/authz/authz_test.go
  • service/authz/resources_task_plugin.go
  • service/channel_select.go
  • service/channel_select_test.go
  • service/codex_channel_models.go
  • service/task_artifact_access.go
  • service/task_artifact_access_test.go
  • service/task_artifact_store.go
  • service/task_artifact_store_test.go
  • service/task_billing.go
  • service/task_billing_test.go
  • service/task_plugin_audit.go
  • service/task_plugin_view.go
  • service/task_plugin_view_test.go
  • service/task_polling.go
  • service/task_polling_test.go
  • setting/billing_setting/tiered_billing.go
  • setting/billing_setting/tiered_billing_test.go
  • setting/system_setting/system_setting_old.go
  • setting/system_setting/task_artifact.go
  • setting/system_setting/task_artifact_store.go
  • setting/system_setting/task_artifact_store_test.go
  • setting/system_setting/task_artifact_test.go
  • setting/task_plugin.go
  • setting/task_plugin_test.go
  • setting/task_pricing_setting/config.go
  • setting/task_pricing_setting/config_test.go
  • types/task_artifact.go
  • web/.gitignore
  • web/package.json
  • web/rsbuild.config.ts
  • web/scripts/sync-i18n.mjs
  • web/src/components/ai-elements/code-block.tsx
  • web/src/features/channels/api.ts
  • web/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/src/features/channels/constants.ts
  • web/src/features/channels/lib/__tests__/channel-type-options.test.ts
  • web/src/features/channels/lib/channel-form.ts
  • web/src/features/pricing/__tests__/breakdown-tier-match.test.ts
  • web/src/features/pricing/__tests__/dynamic-price.test.ts
  • web/src/features/pricing/__tests__/task-expr.test.ts
  • web/src/features/pricing/__tests__/task-matrix-display.test.ts
  • web/src/features/pricing/__tests__/task-matrix.test.ts
  • web/src/features/pricing/components/dynamic-pricing-breakdown.tsx
  • web/src/features/pricing/components/model-billing-mode-badge.tsx
  • web/src/features/pricing/components/model-card.tsx
  • web/src/features/pricing/components/model-details.tsx
  • web/src/features/pricing/components/pricing-columns.tsx
  • web/src/features/pricing/components/pricing-sidebar.tsx
  • web/src/features/pricing/constants.ts
  • web/src/features/pricing/hooks/use-pricing-data.ts
  • web/src/features/pricing/lib/billing-expr.ts
  • web/src/features/pricing/lib/billing-mode.ts
  • web/src/features/pricing/lib/breakdown-tier-match.ts
  • web/src/features/pricing/lib/dynamic-price.ts
  • web/src/features/pricing/lib/filters.ts
  • web/src/features/pricing/lib/task-expr.ts
  • web/src/features/pricing/lib/task-matrix-display.ts
  • web/src/features/pricing/types.ts
  • web/src/features/system-settings/__tests__/task-public-address.test.ts
  • web/src/features/system-settings/general/system-info-section.tsx
  • web/src/features/system-settings/general/task-public-address.ts
  • web/src/features/system-settings/models/model-pricing-sheet.tsx
  • web/src/features/system-settings/models/model-ratio-table-columns.tsx
  • web/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • web/src/features/system-settings/models/task-pricing-matrix.tsx
  • web/src/features/system-settings/models/task-usage-pricing-editor.tsx
  • web/src/features/system-settings/site/index.tsx
  • web/src/features/system-settings/site/section-registry.tsx
  • web/src/features/system-settings/types.ts
  • web/src/features/task-plugins/__tests__/enabled-option.test.ts
  • web/src/features/task-plugins/__tests__/marketplace-panel.test.tsx
  • web/src/features/task-plugins/__tests__/marketplace.test.ts
  • web/src/features/task-plugins/__tests__/plugin-card.test.tsx
  • web/src/features/task-plugins/__tests__/plugin-detail-sheet.test.tsx
  • web/src/features/task-plugins/__tests__/plugin-icon.test.ts
  • web/src/features/task-plugins/__tests__/plugin-url.test.ts
  • web/src/features/task-plugins/__tests__/upload-dialog.test.tsx
  • web/src/features/task-plugins/__tests__/usage-schema-table.test.tsx
  • web/src/features/task-plugins/api.ts
  • web/src/features/task-plugins/components/javascript-viewer.tsx
  • web/src/features/task-plugins/components/marketplace-capabilities.tsx
  • web/src/features/task-plugins/components/marketplace-install-dialog.tsx
  • web/src/features/task-plugins/components/marketplace-panel.tsx
  • web/src/features/task-plugins/components/marketplace-plugin-card.tsx
  • web/src/features/task-plugins/components/marketplace-sources-dialog.tsx
  • web/src/features/task-plugins/components/plugin-card.tsx
  • web/src/features/task-plugins/components/plugin-detail-sheet.tsx
  • web/src/features/task-plugins/components/plugin-icon.tsx
  • web/src/features/task-plugins/components/plugin-metadata-card.tsx
  • web/src/features/task-plugins/components/plugin-sandbox.tsx
  • web/src/features/task-plugins/components/plugin-source-picker.tsx
  • web/src/features/task-plugins/components/plugin-url-import-field.tsx
  • web/src/features/task-plugins/components/plugins-table.tsx
  • web/src/features/task-plugins/components/source-diff.tsx
  • web/src/features/task-plugins/components/upload-dialog.tsx
  • web/src/features/task-plugins/components/usage-schema-table.tsx
  • web/src/features/task-plugins/index.tsx
  • web/src/features/task-plugins/lib/host-protocols.ts
  • web/src/features/task-plugins/lib/marketplace.ts
  • web/src/features/task-plugins/lib/plugin-icon.ts
  • web/src/features/task-plugins/lib/plugin-url.ts
  • web/src/features/task-plugins/types.ts
  • web/src/features/usage-logs/__tests__/access.test.ts
  • web/src/features/usage-logs/__tests__/artifacts.test.ts
  • web/src/features/usage-logs/__tests__/mobile-layout.test.ts
  • web/src/features/usage-logs/__tests__/task-details.test.ts
  • web/src/features/usage-logs/api.ts
  • web/src/features/usage-logs/components/__tests__/usage-facts.test.tsx
  • web/src/features/usage-logs/components/columns/common-logs-columns.tsx
  • web/src/features/usage-logs/components/columns/task-logs-columns.tsx
  • web/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/src/features/usage-logs/components/dialogs/task-details-dialog.tsx
  • web/src/features/usage-logs/components/plugin-author-link.tsx
  • web/src/features/usage-logs/components/task-artifacts.tsx
  • web/src/features/usage-logs/components/timing-metrics-cell.tsx
  • web/src/features/usage-logs/components/usage-logs-mobile-card.tsx
  • web/src/features/usage-logs/components/usage-logs-provider.tsx
  • web/src/features/usage-logs/components/usage-logs-table.tsx
  • web/src/features/usage-logs/constants.ts
  • web/src/features/usage-logs/lib/columns.ts
  • web/src/features/usage-logs/lib/query-params.ts
  • web/src/features/usage-logs/lib/task-artifacts.ts
  • web/src/features/usage-logs/lib/task-details.ts
  • web/src/features/usage-logs/lib/task-mobile-layout.ts
  • web/src/features/usage-logs/lib/utils.ts
  • web/src/features/usage-logs/types.ts
  • web/src/hooks/use-sidebar-data.ts
  • web/src/i18n/locales/_reports/_sync-report.json
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/zh.json
  • web/src/i18n/static-keys.ts
  • web/src/lib/__tests__/localized-text.test.ts
  • web/src/lib/admin-permissions.ts
  • web/src/lib/localized-text.ts
  • web/src/routeTree.gen.ts
  • web/src/routes/_authenticated/task-plugins/index.tsx

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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.

@Calcium-Ion
Calcium-Ion merged commit eb48396 into main Aug 29, 2026
3 checks passed
mrdjango added a commit to mrdjango/models-gateway that referenced this pull request Aug 29, 2026
Brings in 9 upstream commits, notably QuantumNous#7076 (sandboxed JS task plugin
system), which rewrites the task submit/billing pipeline that the
TensorGrid billing patches sit on.

Conflicts resolved in 2 files:

controller/relay.go
  Took upstream's executeTaskSubmission wholesale. Its reserve -> insert
  -> settle barrier supersedes our inline "settle then insert" block, and
  it already carries every field our fork added (BillingSource,
  SubscriptionId, TokenId, NodeName, BillingContext). Dropped our
  redundant `relayInfo.PriceData.Quota = result.Quota` — upstream sets it
  in relay/relay_task.go.

service/task_billing.go
  Kept upstream's 3-arg log-only LogTaskConsumption (task param carries
  the tiered-snapshot fields) and moved settlement to the caller, per
  upstream's design. To preserve eaf0871, the structured consume params
  must still reach the TensorGrid wallet so its credit event is complete
  at commit time, so params building is split into
  BuildTaskConsumeLogParams and passed to SettleBilling's variadic
  consumeParams. relay.go builds them once and shares them with settle
  and log, which also avoids emitting the quota-saturation warning twice.

Verified: go build, go vet, go test ./... (39 pkgs), web typecheck, and
web vitest (397 tests) all pass. Upstream's TestLogTaskConsumption* and
our TensorGrid refund/settle tests pass together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mrdjango added a commit to mrdjango/models-gateway that referenced this pull request Aug 29, 2026
* feat: glm chanel /v1/responses (QuantumNous#7050)

* feat(ollama): passthrough Claude Messages and OpenAI Responses (QuantumNous#7051)

* docs: update PR template and remove PR Check workflow (QuantumNous#7053)

* docs: update PR template and remove PR Check workflow

* docs: add hidden agent issue and PR templates

* fix(web): restore admin unbinding for built-in providers (QuantumNous#6987)

* fix(web): align admin binding types

Refs QuantumNous#6985

* test(web): restore animation mock

* fix(billing): 修复时间规则恒真表达式导致倍率全天生效 (QuantumNous#6934)


Co-authored-by: seefs001 <i@seefs.me>

* fix(docker): add relaykit go.mod to dev build context (QuantumNous#7072)

* feat(task): replace built-in task adaptors with a sandboxed JS plugin system (QuantumNous#7076)

* fix(relay): 请求参数校验错误返回 HTTP 400 (QuantumNous#6774)

* fix(relay): return 400 for invalid request parameters

* fix(web): recheck setup status after page reload (QuantumNous#6968)

* feat(auth): encrypt password login transport

Closes QuantumNous#6743

---------

Co-authored-by: Seefs <40468931+seefs001@users.noreply.github.com>
Co-authored-by: zcxads666 <128150298+zcxads666@users.noreply.github.com>
Co-authored-by: seefs001 <i@seefs.me>
Co-authored-by: Uladzislau <53997152+VladKabiak@users.noreply.github.com>
Co-authored-by: Calcium-Ion <i@caion.me>
Co-authored-by: Alex Xiang <ax2@zicode.com>
mrdjango added a commit to mrdjango/models-gateway that referenced this pull request Aug 29, 2026
Upstream QuantumNous#7076 added an immediate-result path: when an adaptor returns
result.Immediate, the task row is inserted already in a terminal state.
For an immediate FAILURE that produced an uncollectable charge.

The task is inserted with status FAILURE, then SettleBilling runs
unconditionally, charging the user in full. Both refund entry points
select only non-terminal tasks:

  GetAllUnFinishSyncTasks      status != FAILURE AND status != SUCCESS
  GetTimedOutUnfinishedTasks   status NOT IN (FAILURE, SUCCESS)

so the task is invisible to RefundTaskQuota forever and the charge can
never be reversed. immediate.Status comes straight from plugin JS with no
host-side validation, and task plugins are admin-authorable, so FAILURE
is reachable; the bundled google plugin only emits SUCCESS today, which
is why this has not fired yet.

For a TensorGrid wallet the settlement also enqueues a credit event that
the sync worker delivers to the control plane as a real customer charge,
with no compensating refund event.

Zero the task quota on immediate failure and return before the settle
barrier, leaving durable=false so the existing deferred Billing.Refund
releases the reservation. This reuses the established refund path and
works for every funding source (TensorGrid wallet, plain wallet,
subscription). Zeroing Quota also keeps the persisted row honest and
prevents any later double refund.

Immediate SUCCESS stays billable and settles as before.

Verified: go build, go vet, go test ./... (39 pkgs). The new regression
test fails without the fix and passes with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chunfeng789 added a commit to chunfeng789/new-api that referenced this pull request Aug 30, 2026
* fix(web): restore admin unbinding for built-in providers (QuantumNous#6987)

* fix(web): align admin binding types

Refs QuantumNous#6985

* test(web): restore animation mock

* fix(billing): 修复时间规则恒真表达式导致倍率全天生效 (QuantumNous#6934)


Co-authored-by: seefs001 <i@seefs.me>

* fix(docker): add relaykit go.mod to dev build context (QuantumNous#7072)

* feat(task): replace built-in task adaptors with a sandboxed JS plugin system (QuantumNous#7076)

* fix(relay): 请求参数校验错误返回 HTTP 400 (QuantumNous#6774)

* fix(relay): return 400 for invalid request parameters

* fix(web): recheck setup status after page reload (QuantumNous#6968)

* feat(auth): encrypt password login transport

Closes QuantumNous#6743

* feat(chat): add AQBot preset (QuantumNous#7079)

* feat(auth): make password encryption opt-in QuantumNous#6743

* feat(task): resolve channel-mapped aliases and case variants for plugin models

Channel model_mapping keys exposed in a channel's model list now act as
first-class aliases for task-plugin models across the whole line:

- Derived alias view (model/task_model_alias.go): built from enabled
  channels' model_mapping, chain-following with cycle detection, declared
  names always win, cross-plugin conflicts dropped. Rebuilt on channel
  cache refresh, registry generation change, and a 60s TTL.
- Request path: PinTaskPluginEndpoint resolves declared-name case folds
  and mapping aliases before endpoint lookup (never rewriting the body
  until the endpoint is claimed), pins with MappedModel, and the decode
  contract accepts alias echoes without loosening model ownership for
  normal pins. Legacy /v1/tasks submit folds case variants the same way.
  Fixes aliases on POST /v1/responses silently falling through to the
  main relay against task channels.
- Mapping order: ModelMappedHelper now runs before the plugin submit
  hook builds and caches the upstream body, so channel model_mapping
  actually reaches the upstream request. Plugins receive the mapped
  name as ctx.upstreamModel in both decode and submit contexts.
- Billing: identity stays the origin name; when the alias has no tiered
  expression, the selected channel's mapping tail expression applies.
  Pricing page and billing-expr smoke tests resolve aliases to the
  owning plugin's usage schema.
- Case folding: ASCII-only fold with exact-match priority; same-plugin
  and cross-plugin fold collisions rejected at registration.
- Plugins: model-keyed rate tables, req_key derivation, and combo
  validation in doubao/kling/jimeng/hailuo/vidu/sunoapi now key on
  ctx.upstreamModel || ctx.model; render/echo paths keep ctx.model.

* fix(model): disable PostgreSQL prepared statements for pooler compatibility

GORM v1.25.2 closes cached prepared statements asynchronously on any SQL
error and immediately re-Parses the same deterministic name (pgx's
stmt_<sha256>) on the same client connection. Transaction-pooling proxies
(PgBouncer >=1.21 with max_prepared_statements, Neon, Supabase) respond
with FATAL "prepared statement name is already in use" (SQLSTATE 08P01)
and drop the connection. PreferSimpleProtocol only disables pgx's
implicit prepare and never covered GORM's explicit PrepareStmt cache.

- PostgreSQL now runs with PrepareStmt disabled entirely; named prepared
  statements are fundamentally session state and cannot be made safe
  under transaction pooling. Parse/plan cost is noise for this workload.
- Upgrade gorm to v1.25.12 so MySQL/SQLite statement caches (still
  enabled) no longer churn close/re-prepare on ordinary SQL errors;
  v1.25.9+ restricts eviction to driver.ErrBadConn. Deliberately not
  v1.26+, whose LRU eviction has an open use-after-close race (#7831).
- sanitizeDBError now attaches a remediation hint on 08P01/42P05 so
  affected deployments can self-diagnose from the log line.

* fix(ali): honor image response format (QuantumNous#5513) (QuantumNous#7048)

* feat(web): factory task plugins update only with the system

Marketplace install/upgrade on a factory-served plugin actually created
a permanent override shadowing every future built-in release. The card
now shows an informational "Updates with the system" badge instead of
the action, while keeping the built-in vs marketplace version line and
the upgradable state badge visible. Deliberate overrides are untouched:
upload and marketplace actions on overridden or third-party plugins
behave as before, and the plugins table now hints when an override
lags behind the shipped built-in version so operators know deleting it
restores the newer factory plugin.

* fix(subscription): 无有效订阅时前端如实显示「仅用订阅」偏好 (QuantumNous#6222) (QuantumNous#7086)

Co-authored-by: Claude <noreply@anthropic.com>

* fix(sqlite): enable WAL + working busy timeout + _txlock=immediate to stop concurrent write lockouts (QuantumNous#7030)

* fix(sqlite): enable WAL + working busy timeout + _txlock=immediate to stop concurrent write lockouts

* fix(model): return string from JSON column Valuers for pg simple protocol

With PrepareStmt disabled, PostgreSQL queries run over pgx's simple
protocol, which encodes every []byte parameter as a bytea hex literal
('\x...'). driver.Valuer implementations returning []byte from
json.Marshal therefore fail json-column writes with SQLSTATE 22P02
(reported on the channels UPDATE path via ChannelInfo).

Reproduced against a live PostgreSQL 16: []byte Valuer into a json
column fails under simple protocol, string succeeds; []byte into a
text column silently stores the hex literal (no such path exists in
the repo today — audited all Valuers, json.RawMessage fields, and raw
SQL call sites).

- ChannelInfo, Properties, TaskPrivateData, JSONValue Value() now
  return string; zero-value nil semantics unchanged. Task.Data
  (bare json.RawMessage) is unaffected — database/sql's default
  converter already passes it as expected.
- Their Scan() counterparts now accept both []byte and string via a
  shared jsonScanBytes helper: SQLite returns string for these columns
  once Value() emits string, and the old []byte-only assertions
  silently zeroed the field (caught by the model test suite).
- Add regression tests locking both contracts: json-column Valuers
  must return string (or nil for zero values), Scanners must accept
  []byte and string.

Verified end-to-end against PostgreSQL 16 with the real model types:
Channel create/update/read-back, Task json fields, PrefillGroup items.

* fix(relay): bound the wait for upstream response headers (fixes unbounded heap growth → OOM) (QuantumNous#6949)

* fix(relay): bound the wait for upstream response headers (fixes unbounded heap growth)

The relay transport sets a dial timeout, a TLS handshake timeout and an expect-continue
timeout, but nothing bounds how long it waits for the upstream *response headers* after
the request has been written. An upstream that accepts the connection and then never
answers -- without sending FIN/RST, which is what happens when a NAT/firewall silently
drops the flow or the provider hangs -- parks the goroutine in
net/http.(*persistConn).roundTrip forever.

That goroutine keeps the whole request alive, which in practice means three copies of the
request body stay reachable for the lifetime of the process: the raw bytes from
io.ReadAll in CreateBodyStorageFromReader, the decoded messages held as json.RawMessage,
and the re-marshalled upstream body from common.Marshal. BodyStorageCleanup cannot help
here: it runs after c.Next() returns, and for these requests c.Next() never returns.

Measured on v1.0.0-rc.23 in production (see QuantumNous#6947 for the full evidence):

  - 23 goroutines stuck in persistConn.roundTrip on a single 40h-old instance,
    blocked between 353 and 1894 minutes (5.9h to 31.5h)
  - 96.9% of the live heap, sampled after a forced GC, attributable to those three
    body copies (HeapAlloc 892 MiB surviving three GC cycles; HeapObjects dropping
    30x while bytes dropped only 25%)
  - the live floor grows with uptime: 33.7 MiB at 0.1h, 89.2 at 13.8h, 510.0 at 40.1h,
    955.2 at 146.8h, OOMKilled at 172.9h -- same image, same config, same load

Doubling the memory limit and adding GOMEMLIMIT only moved the OOM from 132h to 172.9h.

RELAY_TIMEOUT (http.Client.Timeout) cannot be used for this: it covers the whole response
read and would cut legitimate long streaming calls, which is why it defaults to 0.
ResponseHeaderTimeout only bounds the wait for the headers; streaming after they arrive is
unaffected.

The default is deliberately generous. Non-streaming upstreams usually send the response
headers only once generation has finished, so the value has to leave room for a long
completion. 1800s is 12x shorter than the shortest hang observed here while leaving
several times the headroom a normal non-streaming request needs; 0 restores the previous
unbounded behaviour.

The assignment goes next to the other transport.* lines rather than inside the else
branch: newRelayHTTPTransport() normally takes the http.DefaultTransport.Clone() path,
and DefaultTransport does not set ResponseHeaderTimeout either.

This repo already sets ResponseHeaderTimeout on its other outbound transports
(controller/model_sync.go, controller/ratio_sync.go); the relay path appears to have
been missed.

Refs QuantumNous#6947. Likely also the root cause of QuantumNous#6731, which reported the same symptom
(production OOM on /v1/responses after ~64h) but was closed for template reasons.

* review: clamp overflowing timeout values and switch the test to testify

Addresses the two CodeRabbit findings on this PR.

Overflow (common/init.go:113): a RELAY_RESPONSE_HEADER_TIMEOUT beyond ~9.2e9 seconds
overflows time.Duration and can wrap into a *tiny positive* timeout, which would cut
every relay request instead of only the stuck ones. The value is now clamped before the
conversion, with regression tests for both the negative and the overflowing input.

I did not add fail-on-startup validation for negative values, for two reasons: the
existing `if seconds > 0` guard already treats them as "disabled", and the neighbouring
env-driven timeouts in this file are less strict still -- RelayIdleConnTimeout is
converted with no guard at all. Failing startup on a bad value would be a behaviour
change out of step with the rest of the file; happy to add it if you'd prefer that
direction repo-wide.

Test style: switched to testify (require.Equal / require.Zero / require.Positive), which
is what every other test under service/ uses.

go build, go vet and go test ./common/... ./service/... pass.
(`go build ./...` fails on the `web/dist` embed both with and without this change -- the
frontend bundle is not checked in.)

* fix initialize database

* fix(model): drop leftover prefill_groups unique constraints before AutoMigrate (QuantumNous#7100)

* Revert "fix(model): drop leftover prefill_groups unique constraints before Au…" (QuantumNous#7101)

This reverts commit 69a41ee.

* fix(model): drop leftover prefill_groups unique constraints before AutoMigrate

---------

Co-authored-by: zcxads666 <128150298+zcxads666@users.noreply.github.com>
Co-authored-by: seefs001 <i@seefs.me>
Co-authored-by: Uladzislau <53997152+VladKabiak@users.noreply.github.com>
Co-authored-by: Calcium-Ion <i@caion.me>
Co-authored-by: Alex Xiang <ax2@zicode.com>
Co-authored-by: Seefs <40468931+seefs001@users.noreply.github.com>
Co-authored-by: 憧憬Licoy <licoycn@gmail.com>
Co-authored-by: PuppetKL <154485567+PuppetKL@users.noreply.github.com>
Co-authored-by: ruiyunzhao <91191418+CR-Yun@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Xayinn <129403670+LinineTy@users.noreply.github.com>
Co-authored-by: txgo <tianxi.liu@gmail.com>
yiranxiaohui pushed a commit to yiranxiaohui/new-api that referenced this pull request Sep 2, 2026
Sync QuantumNous/new-api main up to "fix(docker): add relaykit go.mod to
dev build context (QuantumNous#7072)". Stops just before QuantumNous#7076 (sandboxed JS task
plugin system), which conflicts with this fork's Go task adaptors
(New API Video channel type 61, xAI video) and needs a separate port.

Conflicts resolved:
- middleware/model-rate-limit.go: keep fork's reservation-based
  CheckModelRequestRateLimit, adopt upstream's overflow-safe
  rateLimitDurationSeconds for the shared duration computation.
- .github/workflows/electron-build.yml, release.yml: keep deleted
  (dropped by fork; image build workflow is sufficient).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
yiranxiaohui pushed a commit to yiranxiaohui/new-api that referenced this pull request Sep 2, 2026
… and port fork video features

Sync QuantumNous/new-api main through 0ed497f via the resolved PR QuantumNous#6914
branch. Upstream replaced every Go video task adaptor with the sandboxed
JS task plugin system and reserved channel type 61 for "Task Plugin".

Fork features ported onto the plugin system:

- New API Video channel: renumbered to ChannelTypeNewAPIVideo = 62 with a
  one-time, idempotent migration of channel rows and task platforms from
  the legacy 61 (guarded by an options marker so real Task Plugin
  channels are never touched). Reimplemented as factory plugin
  plugins/tasks/newapi-video: POST /v1/video/generations submit, JSON-only
  bodies, per-call billing (no multipliers), tolerant result-URL
  extraction, and channel-backed content proxying for upstreams that
  return their own /content endpoint.
- xAI Grok Imagine video: reimplemented as factory plugin plugins/tasks/xai
  (channelTypes [48]) with the native generation body, OpenAI video
  size -> aspect_ratio/resolution translation, multipart input_reference
  via file placeholders, and the per-model resolution ratios. The native
  POST /v1/videos/generations entry collides with the host's
  /v1/videos/:task_id shape, so it is bound host-side through the new
  middleware.PinHostOwnedPluginRoute instead of meta.routes.
- Removed relay/channel/task/{sora,xai}, the ClientProtocol task field,
  and the fork's video-proxy channel branches (now plugin artifacts).

Conflict resolutions:
- controller/relay.go: keep service.ProcessChannelError wrapper plus the
  fork's per-channel concurrency slot; adopt upstream stage/refund flow.
- relay/claude_handler.go: adopt upstream ApplyReasoningModelSuffix and
  keep the fork's post-processing (history-aware thinking strip, clear_
  thinking edit removal, sampling normalisation, developer role,
  tool normalisation).
- relay/common/relay_info.go: upstream fields plus fork UserRatio.
- router/web-router.go: upstream plugin dispatcher chain plus the fork's
  index injector on "/" and "/index.html".
- model/*, middleware/distributor.go, service/channel_select.go,
  relay/channel/openai/*: upstream.
- i18n locale files: union of both sides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kiminbaek pushed a commit to kiminbaek/new-api that referenced this pull request Sep 5, 2026
qianyexiaoqian pushed a commit to qianyexiaoqian/qianye-newapi that referenced this pull request Sep 5, 2026
上游 v1.0.0-rc.25 → v1.0.0-rc.33(49 个提交)。冲突 72 处,逐处按「保住 fork 行为、
吃下上游架构」裁定。要点:

- 任务适配器整层换成 JS 插件沙箱(QuantumNous#7076):删掉 ali/doubao/hailuo/jimeng/kling/sora/
  vidu 七个 Go 适配器与随行的 fork 计费守卫测试。它们守的两条不变式在新宿主里已由
  上游覆盖:时长/数量乘数在 relay/channel/task/jsplugin 与 pkg/jsplugin 里对
  metadata.duration、metadata.parameters.duration 及任何未声明的数值事实统一按
  MaxTaskDurationSeconds / MaxImageN 定界;模型由 ctx.upstreamModel 决定,内置插件
  一律不从 metadata 取模型。
- 额度上界:保留 fork 的 MaxQuota = 2^43 与那套编译期推导;上游新增的钱包域
  MaxWalletQuota 在这里钉成 = MaxQuota(2^53 会顶穿万分比乘积)。saturateQuota 的
  >= / <= 语义按 fork 的「上界本身即饱和」保留,上游的钱包用例随之改判。
- 消费日志 other 换成 model.LogOther(上游 057f71c 的特权元数据隔离):fork 的
  attach* 标记、logmetrics 两个 hook、violation 扣费日志、佣金排除口径全部改走
  SetPublic/SetAdmin;为返佣与任务台账这类每条日志都要读一次的消费方补了
  PublicValue/PublicBool/PublicString(Snapshot 每次拷四张 map,在同步计费链路上不合适)。
- 充值:保留 fork 的 CreditQuota + creditTopUpQuotaTx 一套(询价与下单同闸、TOKENS
  取整、钱包容量原子谓词),删掉上游重复的一套;兑换码余额分支改走同一个 helper,
  于是它也有了钱包容量判据。
- Midjourney:保留 fork 的请求前原子预留,删掉上游与之冲突的 Prepare/Settle 两段式
  (settle 会二次扣款,且拒绝订阅出资)及其 5 条用例;退款那条用例改用 fork 的资金流复刻。
- 客户端 IP:插件内层 gin.Engine 改为继承 common.ActiveClientIPPolicy 已装载的网段,
  不再自行解析 TRUSTED_PROXIES;上游新增的 task_artifact_access 限流键改走 common.ClientIP。
- 选路:fork 的「分组名与模型名逐字相等」过滤前移到 DB 查询之后,与上游新的
  filterAbilitiesByConstraints 串联。
- 计费表达式:保留 fork 的探针/时钟/字符串字面量烟测,补上上游的 CompileFromCache 与
  u() 用量键拒绝,并把上游的 SmokeTestTaskExpr 接到 fork 的请求探针上。
- 前端:7 份 locale 按键集三方合并(+221 上游新键,fork 删除的键不复活);package.json
  的 test 仍是 bun scripts/run-tests.mjs,上游的 vitest 放到 test:vitest。

验证:go build ./...、cd relaykit && GOWORK=off go build ./...、go vet ./... 全绿。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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