feat(task): replace built-in task adaptors with a sandboxed JS plugin system - #7076
Merged
Conversation
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.
Contributor
|
Important Review skippedToo 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (334)
You can disable this status message by setting the 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. Comment |
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>
5 tasks
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔗 关联任务 / Related Issue
🚀 变更类型 / Type of change
📝 变更描述 / Description
将任务系统(视频/音乐等异步任务平台)从硬编码的 Go 适配器迁移为沙箱化 JS 插件架构,任务平台的接入、路由、计费与管理全部插件化:
插件引擎与运行时
pkg/jsplugin),插件通过meta声明 key、渠道类型、模型、协议与计费 schema;10 个原厂插件(alibaba/kling/jimeng/vidu/doubao/google/vertex-ai/hailuo/sora/sunoapi)内嵌随二进制发布。协议与选择期门控
protocols声明强制显式supports(如 openai_responses 的stream/sync/background),加载期双向校验 hook 与声明一致;插件市场等外部系统可静态解析插件真实支持的模式。计费
final_unit_deduction)支持小数结算。管理与权限
task_plugin.bind(默认仅 root,可通过权限编辑器授予),控制 type-61 渠道的创建/编辑/复制与插件列表查询;插件管理本身保持 root-only。TaskPluginEnabled总开关(关闭后清空路由,等价于停用整个任务系统)、自定义插件开关、出厂插件按 key 禁用(与自定义插件相同的在用守卫:绑定渠道 + 在途任务)。task_plugin_system_disabled;单插件禁用 →task_plugin_disabled并指名插件 key(旧数字渠道类型自动解析,如 17 → "alibaba");未知平台保留invalid_api_platform。其他
📸 运行证明 / Proof of Work
✅ 提交前检查项 / Checklist
New feature,我已关联对应 Issue;若尚无 Issue,我已先自行创建。