* 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.
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.