# 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[].retainResult` is an optional boolean on `submit` and `dynamic` routes, default `true`. Declare `retainResult: false` only on routes where every successful submission completes immediately (`parseSubmitResponse` returns a terminal `immediate`), typically a vendor's synchronous image API that has no task to re-query. For such a result the host does not persist `task.data`, and the task is thereafter not found on every retrieval surface: the plugin's own `query` routes, `GET /v1/responses/:response_id`, `GET /v1/videos/:task_id` and its content, and artifact listing and content. The task row remains for billing and the task log. The submit presenter still receives the complete in-memory `task.data`, and `extractUsageOnComplete` runs before persistence, so settlement is unchanged. An asynchronous result on a declaring route is retained and retrievable as usual and logs a warning. `query` routes reject the field. `openai_responses` and `openai_video` always retain their results because their deliverables are served through host artifact URLs; a plugin that wants previews with less storage should instead return a compact `taskData` from `parseSubmitResponse`. `openai_image` delivers its images inline in the create response, so an immediate terminal result on that protocol is discarded exactly as with `retainResult: false`, while a result completed by in-request polling keeps the snapshot the poller wrote. This is an additive apiVersion 1 extension: older hosts reject the unknown field. `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`, `openai_image`). `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. Multiple plugins may claim the same model on the same host protocol path, including type-61 plugins and plugins with legacy `channelTypes`. Candidates are ordered by ascending plugin key. The selected channel determines the executing plugin: its channel type for legacy channels, `setting.task_plugin_key` for type-61 channels, or the bound entry of `setting.task_plugin_key` / `setting.task_extend_plugin_keys` for type-60 New API channels. A type-60 channel is extended with any number of plugins because the upstream gateway may host many; when several bound plugins share the model, the first bound candidate in ascending key order executes, independent of which candidate an earlier attempt pinned. Enabled uploads pre-flight the candidate against the live routing generation and reject channel-type, native-route, or incompatible protocol conflicts (the error names the counterpart plugin). Sharing a model on the same host protocol is allowed. 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: