Skip to content

Task Plugin API v1 参考

Task Plugin v1 has two independent entry surfaces. meta.routes registers plugin-owned native URLs; meta.protocols claims host-owned protocols without registering or copying their URLs. apiVersion remains 1.

Manifest

js
export const meta = {
  apiVersion: 1,
  key: "vendor",
  name: "Vendor",
  version: "1.0.0",
  author: {name: "Author"},
  description: {en: "Video generation via the vendor API", zh: "通过厂商接口生成视频"},
  models: ["vendor-model"],
  fetchMode: "per_task",
  routes: [
    {method: "POST", path: "/vendor/v1/jobs", type: "submit", decode: "createJob", render: "jobCreated"},
    {method: "GET", path: "/vendor/v1/jobs/:task_id", type: "query", render: "jobStatus"},
  ],
  protocols: [{name: "openai_responses", supports: ["stream", "sync", "background"]}, "openai_video"],
};

submit and dynamic routes require decode and render. query routes require render, prohibit decode, and use taskIdParam (default task_id). Names refer only to callable members of native. Route conflicts use method plus normalized path shape and the route index is published atomically with its plugin generation.

routes[].models optionally restricts a submit or dynamic route to a subset of meta.models. The host matches the canonical top-level model body field before any JS hook runs; a missing, non-string, or unlisted model is rejected with 400 (plugin routes are exclusive paths, so there is no relay fallback). Declare it only when the model lives at the body top level — vendor formats that nest the model elsewhere should omit models and filter inside decode. query routes have no body and reject the field.

Each protocols entry claims a host protocol. A protocol that defines modes must be claimed in object form with an explicit supports; the bare-string form is valid only for protocols without modes (openai_video). supports declares which client request forms the plugin accepts on openai_responses: "stream" (stream: true), "sync" (neither flag; the call blocks until the terminal Response), "background" (background: true; the create returns a pending Response immediately). An unsupported request form is rejected with a 400 at channel selection, before any plugin hook runs and before billing. Retrieval (GET /v1/responses/:response_id) is not a mode: every created response is always retrievable.

{name, models} still narrows that protocol's endpoint bindings to a subset of meta.models and composes with supports. Unlisted models never enter the plugin on that protocol path — they fall through to the built-in Go relay. Cross-plugin endpoint conflicts are judged on the narrowed set, so two plugins may share one protocol path by claiming disjoint model sets.

Enabled uploads pre-flight the candidate against the live routing generation and reject the first channel-type, native-route, or protocol-model conflict (the error names the counterpart plugin). Set force: true or enabled: false to store the plugin anyway.

endpoints, routes[].renderer, global resolveRequest, global renderError, and global renderers are rejected. parseSubmitResponse returns only {taskId, taskData, immediate?, state?}; clientResponse is rejected.

icon is an optional LobeHub icon name string (for example Sora.Color). The values text and text:<label> request a generated text avatar instead (label defaults to the first two characters of name). Inline data URIs and remote URLs are rejected. icon is display-only and does not participate in routing or billing.

A plugin whose vendor has no LobeHub icon ships its logo as a sidecar file, icon.svg or icon.png, next to plugin.js; the manifest stays readable and the image never appears in source diffs. Built-in plugins embed the sidecar from plugins/tasks/<key>/. For uploads the admin UI reads the file and sends it in the upload request's separate icon field as data:image/png;base64,... or data:image/svg+xml;base64,...; marketplace indexes declare it as iconFile.path and the installer fetches it from the index's own origin. The gateway stores the logo apart from the source and serves it from GET /api/plugin/task/:key/icon (optionally ?version=), which the UI renders only through <img>. Limits: 512 KiB; PNG must carry the PNG signature; SVG must be well-formed XML with an svg root and no script or foreignObject elements, event-handler attributes, DOCTYPE, javascript: values, or absolute http(s) references. Supply artwork legible on both light and dark backgrounds (an SVG may use a prefers-color-scheme media query).

baseUrl is an optional default upstream address for channels of type 61 ("Task Plugin"). When an administrator binds the plugin and leaves the channel Base URL empty, the host copies this value onto the channel before validation and records base_url_source: plugin_default in the channel audit event; the persisted channel value is what every later request uses, so a later plugin update that changes baseUrl does not move existing channels. The value must be an absolute http or https URL without credentials, query, or fragment; the host is lowercased and must be ASCII (use punycode), trailing slashes are stripped, and the normalized value must not exceed 191 characters (the width of the channel base_url column on MySQL). Private, loopback, and plain http addresses are allowed for self-hosted upstreams; the admin UI flags them before binding. baseUrl is ignored on legacy channelTypes channels, which keep the built-in per-type default. The default host is not implicitly added to allowedHosts: if the administrator points the channel at a different host, plugin requests to the author's default host are rejected.

allowedHosts lists extra hosts that plugin requests may target besides the channel base URL host. Each entry is host or host:port (IPv6 literals bracketed, for example [::1]:8080); schemes, paths, credentials, and queries are rejected. Entries are lowercased and must be unique after normalization. An entry with a port matches only requests to that port (default ports 80/443 match requests that omit them).

FieldTypeNotes
keystringRequired. Canonical plugin id, ≤ 30 characters.
namestringRequired. Display name.
iconstringOptional LobeHub icon or text / text:<label>. ≤ 128 characters. Image logos ship as a sidecar icon.svg / icon.png, not in the manifest.
descriptionLocalizedTextOptional plugin summary. See LocalizedText. ≤ 512 runes per locale.
versionstringRequired semver.
sortPrioritynumberOptional signed 32-bit integer, default 0. Higher values appear first; ties use ascending plugin key. Display only: does not change routing or override precedence.
websitestringOptional plugin website, independent of author.url and baseUrl. Empty is allowed; otherwise an absolute HTTPS URL with a valid ASCII hostname (use punycode for internationalized domains), without credentials, whitespace, control characters, or backslashes. Paths, queries, and fragments are allowed.
author{name, url?}Required name; url, when present, must be an absolute HTTP(S) URL.
baseUrlstringOptional default Base URL for type-61 Task Plugin channels. Absolute http(s) URL, no credentials/query/fragment, ASCII host, trailing slash stripped, ≤ 191 characters.
allowedHostsstring[]Optional extra request hosts, host or host:port. No schemes, paths, or credentials.

channelTypes lists the legacy channel types this plugin's driver can drive (for example, sora declares [55, 1] because the same OpenAI-type base URL and bearer key serve both chat and video). Every entry equally participates in channel selection, historical Task.Platform matching, and the byChannelType routing index; the same type value may not appear on two plugins. Third-party plugins normally omit channelTypes and live on type-61 "Task Plugin" channels bound by task_plugin_key. The previous split identity/compatibility field names are rejected.

Numeric usageSchema fields declare a host-owned unit of second, count, token, or credit. Boolean fields declare {type: "boolean"}.

meta.usageExamples is an optional display-only list of pricing examples. Each label is a human-readable spec name. Each facts object must be a complete vector over usageSchema: every declared key present, no undeclared keys, and values that pass the same validation as usage facts. Plugins whose schema contains a unit: "token" number field must declare at least one example. Examples never affect billing. Labels are spec names such as std · 10s, not storefront prices.

LocalizedText

meta.description and each usageSchema field description accept LocalizedText. A bare string is equivalent to {en: <string>}. A map must include a non-empty en value. The host normalizes both forms to a map; API responses always emit an object.

Enum fields may additionally declare enumLabels, a map from enum values to LocalizedText display names:

js
video_input: {
  enum: ["none", "video"],
  description: {en: "Reference video input", zh: "参考视频输入"},
  enumLabels: {
    none: {en: "No reference video", zh: "无参考视频"},
    video: {en: "With reference video", zh: "有参考视频"},
  },
}

enumLabels is optional and may cover only some options. It is valid only on enum fields, and every key must exactly match a declared enum value. Each label follows the same locale normalization, required English fallback and 256-rune-per-locale limit as a field description. Labels are short phrases without trailing punctuation. They never change usage facts, expression conditions or values submitted by UI controls. Boolean fields use their description with a localized yes/no state and cannot declare enumLabels.

The UI selects the current language, then its primary language, then English. If no option label is provided, it displays the original enum value; a missing field description falls back to the field name. Labels are plugin data, not frontend translation keys.

This is an additive extension of apiVersion: 1. Updated gateways continue to load plugins without enumLabels; older gateways with strict property validation reject plugins that declare it.

js
description: "Video generation via the vendor API"
description: {en: "Video generation via the vendor API", zh: "通过厂商接口生成视频"}

Rules:

  • Locale keys match ^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$. A map has at most 16 locales.
  • Keys are canonicalized to BCP-47 casing (ENen, zh-twzh-TW, zh-hanszh-Hans); two keys that collide after canonicalization are rejected as duplicates.
  • Each value is trimmed, must be non-empty, and must not contain control characters.
  • meta.description is at most 512 runes per locale. Each usageSchema field description is at most 256 runes per locale.
  • The frontend resolves a locale with exact tag → primary subtag → en (for example zh-TWzhen).
  • Description copy in any language must not include vendor currency prices. The same prohibition applies to usageExamples labels.

Description writing and translation conventions

Descriptions are short, user-facing phrases without trailing punctuation in any language. Keep translations equivalent in meaning. For usageSchema fields, use the following wording:

Field meaningWordingEnglish exampleChinese example
Numeric billing quantityBilling subject + unit priceSong generation unit price生成歌曲单价
ActionAction phraseGenerate songs生成歌曲
BooleanWhether a state is enabled or something is presentWhether audio is generated / Reference video present是否生成音频 / 存在参考视频
Other enum conditionShort condition nameOutput video resolution输出视频分辨率
  • A shared description must cover all supported actions. For Suno, clips.description is Song or lyrics generation unit price / 生成歌曲或歌词单价, and action.description is Generate songs or lyrics / 生成歌曲或歌词.
  • Boolean descriptions name the affirmative state represented by true; do not add a question mark or invert the meaning of the field.
  • Preserve distinctions such as input versus output. Units belong in unit; do not repeat unit explanations or numeric prices in the description.
  • Keep protocol limits, usage sources, estimation and settlement details in code comments or technical documentation, rather than in display descriptions.
  • For example, replace Requested video duration in seconds. / 请求的视频时长,单位为秒。 with Video generation unit price / 视频生成单价; replace Whether audio is generated. Default true. / 是否生成音频。默认为 true。 with Whether audio is generated / 是否生成音频.
  • These are authoring conventions for plugin metadata, not additional runtime validation rules. Existing LocalizedText wire types, usage quantities, enum values and billing semantics are unchanged; a numeric field still contains usage, not a price.

Request body

Every decoder receives one host-parsed body:

ts
{kind: "json", value}
{kind: "form", fields: Record<string, readonly string[]>}
{kind: "multipart", fields: Record<string, readonly string[]>, files: readonly FileReference[]}
{kind: "none"}

FileReference contains {ref, field, filename, mimeType, size}. The ref is opaque and request-scoped; file bytes, readers, temporary paths, and a second inbound requestBody representation are never exposed to JavaScript. The decoder creates a new canonical {kind:"submit", model, action?, requestBody?, originTaskIds?} intent. Driver hooks consume that normalized requestBody and remain surface-independent.

Request building

buildSubmitRequest returns a request descriptor. JSON bodies (the default) may embed a file placeholder at any object or array depth. The host replaces each placeholder with an encoded string before the request is sent; JavaScript never sees file bytes.

js
{ __fileRef: "request_file:input_reference", encoding: "base64" }                 // raw Base64
{ __fileRef: "request_file:input_reference", encoding: "dataUrl" }                // data:<mime>;base64,<data>
{ __fileRef: "request_file:input_reference", encoding: "dataUrl", mimeType: "image/png", maxBytes: 20971520 }

Placeholder objects may contain only __fileRef, encoding, mimeType, and maxBytes. encoding is base64 or dataUrl. __fileRef must resolve to an uploaded multipart file (request_file:<field>). mimeType optionally overrides the part Content-Type for dataUrl (otherwise the part header, else application/octet-stream). maxBytes is an optional vendor cap; the host also applies MAX_FILE_DOWNLOAD_MB (default 64MB) to each file and to the total inlined bytes. Multipart descriptors still stream files with parts[].fileRef.

Origin tasks

Decoders normalize vendor references (draft ids, continuation ids, gateway-issued asset references) into originTaskIds as public gateway task IDs. The host never parses vendor request bodies.

The host verifies ownership, requires every referenced task to belong to the same plugin platform set and one enabled channel, pins that channel for the submit (including retries), and injects the resolved rows into driver hooks as ctx.originTasks (taskId, upstreamTaskId, action, status, and data, including the private upstream id). Renderers and presenters never receive originTasks.

Native surface

js
export const native = {
  createJob(ctx) { return {kind: "submit", model: ctx.body.value.model, requestBody: ctx.body.value}; },
  jobCreated(ctx, task) { return {job_id: task.task_id, state: "queued"}; },
  jobStatus(ctx, task) { return task.data; },
  error(ctx, error) { return {code: error.code, message: error.message}; },
};

The host authenticates, checks ownership, persists the task, builds TaskView, and replaces known private task-id fields with the public ID before calling presenters. Native presenters own the vendor envelope. Authenticated native query presenters may intentionally pass through provider URLs from task.data.

Error contract

Hook Error messages are surfaced (truncated and sanitized) to API callers. Plugin authors should write validation failures as user-readable sentences.

Host protocols

The host registry owns these bindings:

ProtocolOperationBindingBody
openai_responsescreatePOST /v1/responsesJSON
openai_responsesretrieveGET /v1/responses/:response_idnone
openai_videocreatePOST /v1/videosJSON or multipart
openai_videoretrieveGET /v1/videos/:task_idnone
openai_videocontent`GETHEAD /v1/videos/:task_id/content`

protocols.openai_responses always requires decodeRequest. The remaining hooks are derived from supports and verified exactly at load in both directions: "stream" requires renderEvents; "sync" or "background" requires renderFinal. A hook required by a supported mode but not exported, or an exported hook that no supported mode uses, rejects the plugin. For a plugin supporting only "stream", the host renders retrieval at terminal SUCCESS by calling renderEvents once with no previous state — the same first-call-at-terminal semantics streaming already requires. The mode vocabulary is append-only under apiVersion 1: new modes may be added (existing manifests are unaffected; a manifest declaring a mode loads only on gateways that define it), but a mode may never be removed, renamed, or re-mapped to different hooks without an apiVersion bump. protocols.openai_video requires decodeRequest for create and render for retrieve. The host extracts and pins ctx.model, normalizes ctx.stream, frames SSE, creates failure envelopes, and calls renderFinal only for SUCCESS.

background: true on create returns the pending Response immediately (host-synthesized, no plugin hook). Retrieval renders via renderFinal only at terminal status. Plugins declaring "background" need no create-time hook; the deliverable is rendered at retrieval via renderFinal. Plugins cannot observe the background field. Unlike the upstream OpenAI API, a plugin that does not declare "stream" rejects stream: true with a 400; the error names the supported forms.

Protocol media uses host-injected ctx.artifacts[key].url. Provider URLs from task.data are not protocol output. OpenAI Video projections are DTO-whitelisted and the host overwrites identity, lifecycle, timestamps, and removes URL-like metadata.

Persisted data and driver hooks

The persisted field remains task.data; there is no task.raw alias. Driver hooks (buildSubmitRequest, parseSubmitResponse, query/result, usage, artifact, and content hooks) stay flat and must not branch on the client path or protocol.

ctx.model is the billing and display identity (the origin name the client sent, including a channel-mapping alias). ctx.upstreamModel is the machine identity after channel model_mapping. Rate tables and model-keyed usage facts must use ctx.upstreamModel || ctx.model. Decode and render hooks that echo the client model must keep ctx.model. buildSubmitRequest must not set descriptor top-level model on a mapped pin; the host requires the plugin to echo the alias verbatim. Background polling has no relay info, so query hooks receive both identities from the persisted task properties, and ctx.upstreamModel falls back to ctx.model when the task was submitted without a channel mapping.

Polling contract

Query and parse hooks use TaskQueryContext, not DriverContext. The host rebuilds that context from the persisted task row. There is no query-side requestBody.

FieldSource
taskIdUpstream task id (PrivateData.UpstreamTaskID, else TaskID)
publicTaskIdGateway task id
actionNormalized persisted action
modelProperties.OriginModelName
upstreamModelProperties.UpstreamModelName, falling back to model
baseUrl / apiKey / authHeader / authChannel credentials
dataCurrent Task.Data snapshot
statePlugin-owned PrivateData.PluginState

Task.Data is the latest upstream response snapshot for presenters and artifacts. The host overwrites it on every successful parse. Values that must survive across poll rounds belong in state.

parseSubmitResponse, parseTaskResult, and each parseBatchResult item may return optional state. The host writes it only when the hook returns it. Omitting state preserves the previous value. Oversized state is rejected with a warning, not truncated.

buildBatchQueryRequest(ctx, tasks) and parseBatchResult receive tasks: TaskQueryContext[]. parseTaskResult / parseBatchResult also receive {status, headers} for the upstream HTTP response.

status: "UNKNOWN" means the plugin does not recognize the response. Do not write || "IN_PROGRESS" (or equivalent) for a missing table entry. The host treats UNKNOWN, hook errors, empty status, and unrecognized status strings as consecutive poll failures.

The host classifies the HTTP status before trusting a non-terminal parse:

Upstream HTTPHost action
2xxCall the parse hook
404 / 410Immediate FAILURE and refund
401 / 403Leave task status unchanged; increment PollFailures; LogWarn with channel id. Channels are not auto-disabled.
429 / 5xx / transport errorIncrement PollFailures
Other 4xxCall the parse hook with response.status. A still-non-terminal result is unrecognized and increments PollFailures.

A valid 2xx non-terminal parse resets PollFailures to 0. After TASK_POLL_MAX_FAILURES (default 20) consecutive failures the task becomes FAILURE and follows the existing refund chain. The 24h TASK_TIMEOUT_MINUTES sweep remains the outer deadline.

Marketplace index v1 plugin entries also accept optional sortPriority and website fields. Each source is sorted independently by descending priority and ascending key. Missing or invalid priorities fall back to 0; invalid websites are hidden. Installed plugins and channel binding options use plugin metadata. Website links open only on user interaction; the host does not fetch them.

基于 New API(AGPLv3)构建,Frontend design and development by New API contributors.