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.
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 treats protocols.openai_video.render as a standard DTO, not an arbitrary
JSON passthrough. Unknown top-level fields and legacy task_id are removed,
id is forced to the public task id, and case-insensitive url entries are
removed from metadata. Provider output URLs belong only behind artifact
capabilities.
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 fetch, filesystem, require, imports, async functions, or
environment access. 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.