Files
Calcium-Ion c59f6fd553 feat(plugins): serve task plugins at the vendors' own paths and answer as the vendors do (#8117)
* feat(plugins): serve task plugins at the vendors' own paths

A vendor SDK or plain HTTP client can now point at the gateway and keep the
vendor's official paths. Every plugin route is also served under
/{plugin key}, and that address always runs that plugin.

- Plugins declare the vendor's official paths and the gateway adds the
  /{key} copy. Existing prefixed addresses keep working, including /ali
  for Alibaba Bailian.
- Several plugins may declare the same path. A submit runs on the plugin
  whose channel is selected; a query is answered by the plugin that owns
  the task.
- Under the gateway's own path segments (/api, /v1, /v1beta and so on)
  only built-in plugins get top-level paths; other plugins are served at
  their /{key} address. Plugin keys cannot reuse these segments.
- Jimeng uses the official Bearer API (/api/common/v3/submit_task_v2 and
  get_result_v2). The /jimeng/ route, which requests could not reach, is
  removed.
- MiniMax supports the official v1 query with ?task_id= and
  /v1/files/retrieve. The unimplemented GET /v1/files/{id} placeholder is
  removed. Vertex AI also accepts v1beta1 paths.
- Vidu routes accept Authorization: Token, and Google routes accept
  x-goog-api-key and ?key=. The key is redacted from logs.
- Gemini Veo results are read from generatedSamples, so their videos
  download again. Vertex AI videos returned inline are fetched again when
  downloaded.
- GET /v1/videos/{id} answers tasks created on xAI's routes in xAI's
  format, and :predictLongRunning requests reach the Veo plugin.
- Enabling, activating or promoting a plugin version is refused when it
  conflicts with another plugin, and force no longer skips that check.
- The plugin page lists each endpoint once, with a switch between the
  vendor path and the /{key} address. Legacy addresses are folded away.

* fix(logging): use the shared query credential masking for key=

The access log already masks credentials passed in the query string,
key included. A second redaction for the Gemini ?key= form rewrote it to
key=[REDACTED], so the log showed two masking styles and the masking
tests failed. Only the shared masking remains.

The Gemini native routes take ?key= out of the URL. With no query left,
the log fell back to the path gin recorded on arrival, which still
carried the key. The log now writes the path alone in that case, and the
plugin API document no longer says the key is logged as key=[REDACTED].

* feat(plugins): answer native plugin routes as the vendors do

A request sent to a vendor's own path now reaches the vendor as the client
wrote it, and the client gets the vendor's reply shape and status. The
gateway answers a native request with its own 400 only when it cannot tell
the model, cannot price the request, or cannot provide the feature; the
vendor checks everything else.

- New gateway rejections of that kind on native routes: callback_url on
  Ark, Kling, Vidu and MiniMax; Kling external_task_id and a body with
  model but no model_name; xAI storage_options, which was dropped
  silently; and for Jimeng, req_json on get_result_v2, a req_key the
  plugin cannot price, and frames other than 121 or 241. A value the
  plugin cannot read reserves the model's dearest tier, and settlement is
  unchanged.
- Native bodies are sent as the client wrote them. Plugins no longer fill
  in MiniMax-H3's duration, resolution and ratio or Vidu's
  movement_amplitude and images: null, drop Vidu's bgm: false, turn
  Jimeng's image and images into image_urls, or merge Ark's content.
  Kling requests no longer carry an added aspect_ratio 1:1, cfg_scale 0.5
  or model field; without a size Kling uses its own 16:9. DashScope
  requests go to the endpoint the client called.
- Replies keep each vendor's shapes and error codes. A native presenter
  may answer with the vendor's own status through a reply wrapper
  ({__status, __headers, __body}): xAI answers a pending video with 202,
  DashScope an unknown task with 200 UNKNOWN. A task the gateway failed
  shows as failed instead of still running. xAI's native query shows a
  video that failed but was charged as failed with xAI's usage, instead of
  done; billing is unchanged.
- A native client that asks the vendor for a stream (Seedream with
  stream: true, Wan interleaved output with X-DashScope-Sse: enable) gets
  the vendor's events as they arrive. Once an event has reached the
  client the task settles and is never refunded: a finished stream bills
  what was delivered, and a stream that stopped early bills the
  reservation and cuts the client's connection, so a downstream gateway
  keeps its reservation too. The client leaving does not cancel the
  upstream request, which TASK_PLUGIN_PROTOCOL_TIMEOUT_SECONDS bounds. A
  JSON reply from an older new-api upstream is still accepted.
- Routes that keep no result (retainResult: false) accept replies up to
  MAX_FILE_DOWNLOAD_MB, so b64_json images fit.
- Native decoders get the client's request headers, without gateway
  credentials, as ctx.requestHeaders.
- Native routes accept a channel's model aliases and case variants of
  model names, as the gateway's own formats do, also on routes that list
  their models.
- Kling lists kling-v2-5-turbo, kling-v2-6 and kling-v3, priced in units
  per mode, sound and voice; Kling retired kling-v1, kling-v1-6 and
  kling-v2-master on 2026-09-15. Vidu lists its current models, from
  viduq4-preview to vidu1.5.
- Clients of /v1/videos and the other gateway formats can no longer pass
  a native body through the field the plugins use for it internally.

* feat(plugins): add Kling 4.0 to the kling plugin

Kling 4.0 takes the model in the path and has its own request shape, so
the plugin serves POST /text-to-video/kling-4.0, /image-to-video/kling-4.0
and /omni-video/kling-4.0 as native routes, and answers GET
/tasks?task_ids= for their tasks. /v1/videos and the Responses API send
kling-4.0 as text-to-video or image-to-video, with size picking 720p or
1080p.

Billing follows Kling's price list: units per output second by
resolution, or, with an input video, twice the output, or input plus
output when the input is longer. Kling picks the length when the request
has none and only Kling knows how long an input video is, so the
reservation covers the longest of each. A task settles on the units Kling
deducted, or, when Kling took cash, on the durations it reports.

Kling 4.0 Flash has no published price yet and is left out.

* chore(plugins): bump the task plugins this branch changes

Each of these plugins now declares its vendor's own paths and answers
native requests as the vendor does; kling also adds Kling 4.0. That is
new behavior in every one of them, so each takes a minor version:
alibaba 1.5.0, doubao 1.4.0, google 1.2.0, hailuo 1.4.0, jimeng 1.2.0,
kling 1.2.0, vertex-ai 1.2.0, vidu 1.2.0 and xai 1.2.0.
2026-10-30 21:28:16 +08:00
..

Task plugin API v1

Task plugins are single-file synchronous ECMAScript modules. The plugin contract is currently unreleased; v1.schema.json and v1.d.ts are the authoritative v1 contract.

Contract and lifecycle

Every plugin exports meta, buildSubmitRequest, parseSubmitResponse, and parseTaskResult. A per_task plugin also exports buildQueryRequest; a batch plugin exports buildBatchQueryRequest and parseBatchResult. meta.author.name is required and meta.author.url, when present, must be an absolute HTTP(S) URL. This is self-declared attribution; a future marketplace's verified publisher identity is a separate host-owned record. Plugins may declare authenticated vendor-native meta.routes and claim host-owned names through meta.protocols. Submit and dynamic routes name a native decoder and presenter; query routes name only a presenter. Protocol bindings are registered once by the host registry, and protocol decoders receive the host-parsed body union plus the pinned model. Shared protocol hooks are synchronous transformations; Go owns connections and wire framing.

The host selects a channel, invokes the request-building hook, validates the returned URL against the channel host, performs HTTP, and gives the decoded response to the matching parse hook. It owns persistence, retries, polling, billing, and settlement. Plugins only transform data and report usage facts. See v1.d.ts for signatures and v1.schema.json for machine-readable shapes.

Plugins that expose task outputs export listArtifacts(task) and buildContentRequest(ctx) together. Artifacts are projected on explicit reads from persisted Task.Data; they are never stored as a second source of truth. The list contains only stable key, type, and optional mimeType fields. The content hook receives the selected key, raw decoded task data, the explicit private upstream task id, the producer plugin version, channel authentication, and a safe client Range/conditional-header subset. Its URL and headers exist only for that proxy request.

When a Responses observation reaches persisted SUCCESS, the host also runs the pinned plugin's listArtifacts and injects a read-only ctx.artifacts[key] = {key, type, mimeType?, url} map into renderEvents or renderFinal. Each url is a long-lived host-signed capability URL, never the provider URL from Task.Data. Nonterminal and failed tasks receive no artifact map. Capability construction or rendering failure fails only that Responses observation; it cannot change the task, billing settlement, or refunds. The absolute URL uses TaskPublicAddress, falling back only to ServerAddress; multi-node deployments must share the effective CRYPTO_SECRET. Dashboard artifact reads return each content_url (or the legacy legacy_content_url) directly, without a temporary URL exchange. Capability generation and verification are stateless and have no expiry; after verification the host still loads the task, owner, and plugin needed to serve the artifact. Rotating CRYPTO_SECRET invalidates issued URLs. The access query is redacted before request logging. Deployment boundaries and concurrency environment variables are documented in v1.md.

The host requires protocols.openai_video.render to return a JSON object and preserves provider extensions, including output URLs and metadata. Plugins may return task.data to expose the latest persisted upstream snapshot. The host still overwrites id, object, model, status, progress, created_at, and completed_at, and removes legacy task_id. This is field-level projection, not byte-for-byte HTTP forwarding: known private task IDs are replaced before rendering, polling may redact inline video data, and retrieval reads the latest saved snapshot rather than issuing a new upstream request. Artifact content endpoints remain available for proxied downloads.

Provider-authenticated content URLs must use the channel base host or a plugin-declared meta.allowedHosts entry. A public dynamic CDN URL may instead set credentialless: true; the host then permits only GET/HEAD with no plugin-supplied headers or body and applies SSRF checks to the initial URL and every redirect.

Registry publication is generation-atomic. A request pins one plugin generation for its full lifetime, while background polling may use a later active plugin version. New versions must continue parsing responses for in-flight tasks. Root administrators can inspect the local node with GET /api/plugin/task/runtime/status. The response includes the node-local generation, a deterministic revision of the active database overrides, the latest rebuild outcome, and plugin-level compile or routing errors. Generation numbers are local to a node; compare database revisions when diagnosing rollout lag between nodes. If the database snapshot is temporarily unavailable, the endpoint keeps serving node-local state and the last known revision with database_error set.

For live diagnosis, start the process with DEBUG=true and filter logs on task_plugin. Plugin registry, routing, endpoint ownership, channel selection, submit durability, polling adapters, and protocol observation emit safe key/value lifecycle events. Request-context events carry the request id; scheduled, background, and context-less work is labeled SYSTEM. Plugin console.log output is also forwarded in DEBUG mode. Hook-time output is prefixed with plugin key/version; module-initialization output may have an empty identity during initial upload validation. Do not print credentials, headers, request bodies, upstream payloads, or private URLs from plugin code; free-form console output cannot be redacted by the host.

Fixtures and dry runs

A fixture case is {name?, hook, member?, args, expected?, expectedError?}. Keep deterministic cases for every exported hook, its main error branch, batch behavior, renderers, usage, and content requests. Run a fixture locally with:

new-api plugin lint plugin.js
new-api plugin test plugin.js --fixture golden.json

Root administrators can open the plugin detail Sandbox tab, choose a hook, and submit an args JSON array. POST /api/plugin/task/:key/dryrun compiles the active database source or factory source in a temporary registry and invokes only that synchronous function. Dry runs never execute a request descriptor and therefore never contact an upstream service.

Upload and release

Upload from the root-only task plugin page or POST /api/plugin/task with {"source":"...","remark":"..."}. The server compiles the module, validates v1 metadata and required exports, and rejects invalid source before saving it. Use semantic plugin versions. Reusing a key/version with different source is rejected; activate or roll back a stored version through the management page.

For a third-party platform, create a channel of type Task Plugin, select the plugin key, provide an explicit base URL, and configure models. Clients may use the plugin's declared native routes. The generic management surface remains POST /v1/tasks/:pluginKey, GET /v1/tasks/:taskId, and GET /v1/tasks/:taskId/artifacts plus GET|HEAD /v1/tasks/:taskId/artifacts/:key/content.

Security boundary

Plugins have no global fetch, filesystem, require, imports, async functions, code compiled from strings (eval and the Function constructors throw an EvalError), or environment access. A few driver hooks may read upstream data with utils.fetch: GET or HEAD only, to the hosts a request descriptor may address. The host limits execution time, concurrency, input size, allowed request hosts, and resolves OAuth credentials outside JavaScript. Multipart files enter JavaScript only as opaque references.

This is not a hard memory-isolation boundary. A plugin sees data needed for the current request and can influence an authenticated upstream request. Uploading a plugin is an administrator-level trust decision equivalent to configuring a channel credential. Review source and version diffs before activation. Never run untrusted plugins merely because they compile.

Usage hooks may return facts such as seconds, resolution, or upstream units, but must never calculate prices or attempt quota settlement. The host owns all pricing and clamps billing conversions.