feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
+196
-25
@@ -24,11 +24,13 @@ export const meta = {
|
||||
|
||||
`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`). `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.
|
||||
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, or `setting.task_plugin_key` for type-61 channels.
|
||||
`{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.
|
||||
|
||||
@@ -42,6 +44,8 @@ A plugin whose vendor has no LobeHub icon ships its logo as a sidecar file, `ico
|
||||
|
||||
`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).
|
||||
|
||||
`upstreams` lists the kinds of upstream the driver's request builders can address. `vendor` is the provider API itself and is implied for every plugin; `new_api` is another New API gateway that has the same plugin installed. Only a plugin that declares `new_api` can be bound to a type-60 New API channel: the host rejects other bindings when the channel is saved, and the channel form lists only declaring plugins. A gateway does not serve vendor paths at their vendor location. It serves a plugin's wire format only on that plugin's own native routes, which carry the plugin's prefix because `/api` is a reserved host namespace and vendor paths would collide across plugins (`/doubao/api/v3/...`, `/ali/api/v1/...`, `/kling/v1/videos/...`), and it authenticates native routes and host protocols with `Authorization: Bearer <token>`. The host therefore tells the driver which kind of upstream the executing channel is through `ctx.upstream` (see Persisted data and driver hooks) instead of guessing a prefix from `meta.routes`; a plugin whose vendor paths already equal its native routes (sunoapi) or that only uses host protocols (sora) declares `new_api` without changing any URL. Values must be unique and drawn from the two kinds above; listing `vendor` is optional and descriptive. This is an additive apiVersion 1 extension: older hosts reject the unknown field, and a declaring plugin keeps building vendor URLs on hosts that never set `ctx.upstream`.
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `key` | string | Required. Canonical plugin id, ≤ 30 characters. |
|
||||
@@ -54,8 +58,9 @@ A plugin whose vendor has no LobeHub icon ships its logo as a sidecar file, `ico
|
||||
| `author` | `{name, url?}` | Required name; `url`, when present, must be an absolute HTTP(S) URL. |
|
||||
| `baseUrl` | string | Optional default Base URL for type-61 Task Plugin channels. Absolute `http(s)` URL, no credentials/query/fragment, ASCII host, trailing slash stripped, ≤ 191 characters. |
|
||||
| `allowedHosts` | string[] | Optional extra request hosts, `host` or `host:port`. No schemes, paths, or credentials. |
|
||||
| `upstreams` | `("vendor" \| "new_api")[]` | Optional upstream kinds the driver addresses. `vendor` is implied; `new_api` makes the plugin bindable to type-60 New API channels. |
|
||||
|
||||
`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.
|
||||
`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, and neither the Task Plugin (61) nor the New API (60) type may be declared. Third-party plugins normally omit `channelTypes` and live on type-61 "Task Plugin" channels bound by `task_plugin_key`, or are listed in a type-60 New API channel's `task_extend_plugin_keys` without claiming a channel type. The previous split identity/compatibility field names are rejected.
|
||||
|
||||
Before channel distribution, the host calls every mode-compatible candidate's
|
||||
`decodeRequest`. Only candidates returning a valid submit intent for the pinned
|
||||
@@ -136,10 +141,17 @@ Static consumers can use the compiled display metadata in the marketplace index.
|
||||
Model-level pricing and save validation select the declared target of an alias;
|
||||
ambiguous aliases retain the plugin defaults. Runtime validation selects the
|
||||
executing plugin's final upstream model, including each persisted task during
|
||||
batch polling. Existing saved expressions are not migrated. Single-plugin models retain their
|
||||
batch polling; when no profile declares that upstream name (for example a
|
||||
channel mapping that sends a declared model to a vendor endpoint ID), the
|
||||
client-facing model's profile applies. Existing saved expressions are not migrated. Single-plugin models retain their
|
||||
existing submission checks; shared models additionally enforce the per-provider
|
||||
field-reference compatibility rule above. Normal evaluation and quota safety
|
||||
checks continue to apply. Saving an expression uses its model's selected schema.
|
||||
checks continue to apply. Saving an expression uses its model's selected schema;
|
||||
an unchanged stored expression of a single-plugin model stays saveable after its
|
||||
profile narrows. A plugin that drops a fact from a profile keeps reporting it at
|
||||
a value that leaves existing charges unchanged: `0`, `false`, or the enum value
|
||||
first declared. The pricing editor reads stored expressions the same way and
|
||||
shows only the remaining fields.
|
||||
|
||||
This remains plugin API version 1. New hosts accept older plugins unchanged;
|
||||
older hosts reject the new `usageProfiles` field. Upgrade the host to a build
|
||||
@@ -203,13 +215,16 @@ Descriptions are short, user-facing phrases without trailing punctuation in any
|
||||
## Host utilities and capabilities
|
||||
|
||||
`utils` exposes synchronous host tools. Existing utilities are `unixNow`, `uuid`,
|
||||
`hmacSHA256`, `jwtSignHS256`, `base64`, `base64URL`, `base64URLDecode`, and
|
||||
`volcSignV4`; their signatures are declared in `v1.d.ts`.
|
||||
`hmacSHA256`, `jwtSignHS256`, `base64`, `base64URL`, `base64URLDecode`,
|
||||
`volcSignV4`, `json.clone`, and `fetch`; their signatures are declared in
|
||||
`v1.d.ts`.
|
||||
|
||||
Plugins using new APIs declare exact versioned capabilities:
|
||||
The host recognizes a feature from what a plugin exports or returns, such as
|
||||
an exported `parseSubmitEventDelta`. A plugin declares a capability only to
|
||||
switch on behavior that changes what its hooks receive or relaxes a host check:
|
||||
|
||||
```js
|
||||
requiredCapabilities: ["json-clone@1", "submit-sse-delta@1"]
|
||||
requiredCapabilities: ["json-order@1"]
|
||||
```
|
||||
|
||||
The host rejects unknown, duplicate, or unsupported capability versions at load.
|
||||
@@ -218,7 +233,12 @@ supports optional feature detection on hosts that provide this utility; it
|
||||
does not grant a declared capability. These are additive API v1 extensions;
|
||||
older strict hosts reject `requiredCapabilities`, so upgrade the host first.
|
||||
|
||||
`json-clone@1` provides `utils.json.clone(value)`: an independent JSON snapshot
|
||||
`json-clone@1` and `submit-sse-delta@1` remain accepted for plugins written for
|
||||
earlier hosts; new plugins need neither. `json-clone@1` has no effect.
|
||||
`submit-sse-delta@1` still requires `submitResponseTypes` to include `sse` and
|
||||
the plugin to export `parseSubmitEventDelta`.
|
||||
|
||||
`utils.json.clone(value)` returns an independent JSON snapshot
|
||||
with native mutable JavaScript objects and arrays. It accepts plain objects,
|
||||
arrays, null, booleans, finite numbers and strings. Undefined, functions,
|
||||
BigInt, dates, sparse arrays and cyclic structures are rejected. Object keys
|
||||
@@ -227,9 +247,137 @@ host's JSON number and string normalization and preserves explicit zero and
|
||||
false values. Limits are 1 MiB of encoded JSON, 32 levels below the root and
|
||||
32,768 total nodes, including containers and scalar values. It performs no I/O.
|
||||
|
||||
`json-order@1` keeps JSON member order for upstreams that treat it as
|
||||
meaningful, such as a model that reads a JSON object as text. The plugin's
|
||||
decoders receive a JSON request body (`ctx.body.value`) and its driver hooks
|
||||
the decoded `ctx.requestBody` with the members in the order the client sent
|
||||
them, and the host writes the plugin's JSON request descriptor bodies as
|
||||
`JSON.stringify` writes them: members in the order the plugin's objects hold
|
||||
them, members whose value is `undefined` left out, no HTML escaping. As in
|
||||
JavaScript, keys that are array indices come first, in ascending order.
|
||||
Without the capability, objects reach hooks with their members sorted by key
|
||||
and the host JSON codec writes bodies sorted the same way. Each decoder and
|
||||
driver hook call parses the ordered body again, which costs more on large
|
||||
bodies, so declare the capability only when the upstream needs it. A body
|
||||
that embeds a file placeholder is still written sorted.
|
||||
|
||||
`duration-auto@1` lets a request carry a duration of `-1`, which some vendors
|
||||
read as "the model picks the length" (Seedance on Volcengine Ark, wan3.0 on
|
||||
Bailian). Before billing, the host checks every member of the decoded request
|
||||
at any depth, including vendor parameters passed through `metadata`: a
|
||||
`duration` or `seconds` member (case, `_` and `-` are ignored, so
|
||||
`durationSeconds` counts) must be between 0 and 3600. For a plugin that
|
||||
declares the capability the host also accepts exactly `-1` there; other
|
||||
negative values and values above the limit are still rejected. Usage facts
|
||||
are unchanged and must stay non-negative, so the plugin never reports `-1` as
|
||||
a quantity. A declaring plugin reserves the longest duration the model can
|
||||
produce when it forwards `-1`, and settles the task on the usage the vendor
|
||||
reports, so the charge follows the length the model chose. A plugin that
|
||||
cannot settle on reported usage must not declare it.
|
||||
|
||||
Host functions are synchronous and must bound their own work: the JavaScript
|
||||
execution interrupt cannot preempt a running Go function. Request-scoped data
|
||||
must remain owned by the host request, not by pooled runtime globals.
|
||||
`utils.fetch` waits on the network without holding a JavaScript execution slot,
|
||||
and the wait does not count toward the hook's execution time.
|
||||
|
||||
### Reading upstream data with utils.fetch
|
||||
|
||||
```ts
|
||||
utils.fetch(request: {url: string; method?: "GET" | "HEAD"; headers?: Record<string, string>; responseType?: "json" | "bytes"}):
|
||||
{status: number; headers: Record<string, string>; body: JSONValue | string | ArrayBuffer}
|
||||
```
|
||||
|
||||
`utils.fetch` lets a driver hook read upstream data before it returns, for
|
||||
example a download link that the vendor hands out only through a separate
|
||||
lookup. The call is synchronous. The request takes the `url`, `method` and
|
||||
`headers` members of a request descriptor; the result has the shape of the
|
||||
response `parseTaskResult` receives: the HTTP status, the first value of each
|
||||
response header under its canonical name (`Content-Type`, `Location`), and the
|
||||
body.
|
||||
|
||||
- `body` is the parsed JSON when the response parses as JSON (`JSON.parse`
|
||||
semantics), otherwise the text. A `HEAD` response has an empty string.
|
||||
- `responseType: "bytes"` returns `body` as an `ArrayBuffer` of the response
|
||||
bytes instead, for binary data such as the header of a media file read with
|
||||
a `Range` header; a `HEAD` response has an empty buffer. The body limit is
|
||||
the same.
|
||||
- The URL may address the same hosts as a request descriptor: the channel Base
|
||||
URL host and `allowedHosts`.
|
||||
- Requests use the client and channel proxy of poll requests.
|
||||
- Redirects are not followed. A 3xx response is returned as it is; fetching its
|
||||
`Location` is a new request, checked and counted again.
|
||||
- HTTP error statuses are returned, not thrown.
|
||||
- Limits: at most 4 fetches per hook call, 15 seconds per fetch, 30 seconds
|
||||
for all fetches of one hook call, and 1 MiB per response body.
|
||||
|
||||
`utils.fetch` throws when:
|
||||
|
||||
- the hook may not fetch (see below);
|
||||
- the request has members other than `url`, `method`, `headers` and
|
||||
`responseType`;
|
||||
- the method is not `GET` or `HEAD` (default `GET`), or the `responseType` is
|
||||
not `json` (default) or `bytes`;
|
||||
- the URL is not an absolute `http(s)` URL or exceeds 64 KiB;
|
||||
- there are more than 64 headers, a header value exceeds 8 KiB, or a header is
|
||||
`Host`, `Content-Length`, `Accept-Encoding`, `Connection`,
|
||||
`Proxy-Connection`, `Keep-Alive`, `Proxy-Authorization`, `TE`, `Trailer`,
|
||||
`Transfer-Encoding` or `Upgrade`;
|
||||
- the host is not allowed;
|
||||
- the fetch times out, or the call runs out of fetches or fetch time;
|
||||
- the response body exceeds 1 MiB;
|
||||
- the transport fails.
|
||||
|
||||
Hook errors reach API callers, so these messages name the host only, never the
|
||||
path or query.
|
||||
|
||||
Fetch is read-only. A request that starts billable vendor work stays the
|
||||
`buildSubmitRequest` descriptor, the only request the gateway accounts for.
|
||||
`utils.fetch` is available only in driver hooks that run after channel
|
||||
selection and whose failure is harmless or retried: `buildSubmitRequest` (the
|
||||
submission is not sent), `buildQueryRequest`, `parseTaskResult`,
|
||||
`buildBatchQueryRequest` and `parseBatchResult` (one failed poll), and
|
||||
`buildContentRequest` (one failed download). Every other hook, and a dry run,
|
||||
which has no channel, throws `utils.fetch is not available in <hook>`. Hosts
|
||||
without this utility have no `utils.fetch`; calling it there throws a
|
||||
`TypeError`, and the hook fails like any other hook error. It needs no
|
||||
capability declaration.
|
||||
|
||||
## Hook arguments and results
|
||||
|
||||
The host passes plain JSON values to hooks without copying them: objects,
|
||||
arrays, strings, finite numbers, booleans and null. The engine converts each
|
||||
object or array when the hook first reads it and never writes JavaScript
|
||||
changes back into the host value. Other values (structs, byte slices,
|
||||
`json.Number`, raw JSON, invalid UTF-8, non-finite numbers, nil slices, or
|
||||
nesting deeper than 64 levels) are first normalized through the host JSON codec.
|
||||
|
||||
Plugin rules:
|
||||
|
||||
- Treat every argument as read-only input. Assigning to an argument changes
|
||||
only the current call's view; the host, other hooks and later calls never see
|
||||
it. Return the value the host should use, and use `utils.json.clone` when a
|
||||
mutable copy is needed.
|
||||
- Do not keep arguments, or objects and arrays read from them, in module
|
||||
variables after the hook returns. Runtimes are pooled across requests, so a
|
||||
kept value can surface in a later request, keeps the original request's data
|
||||
alive, and is still read from the host value it came from.
|
||||
|
||||
Host rules:
|
||||
|
||||
- Once a value has been passed to a hook, host code must not change it. A
|
||||
plugin may still hold it, and reading a Go map while another goroutine writes
|
||||
it is a fatal runtime error that `recover` cannot catch and that stops the
|
||||
whole process. Build a new value, or copy it first (`jsonValue` in the task
|
||||
adaptor copies plain JSON and normalizes everything else).
|
||||
- Hook results may be host values. A hook that returns an argument, or part of
|
||||
one, unchanged hands back the original host value rather than a copy. Copy a
|
||||
result before editing it in place; the request descriptor's file placeholders
|
||||
are replaced on a copy for this reason.
|
||||
- Integral numbers created in JavaScript come back as `int64` and other numbers
|
||||
as `float64`; host values returned unchanged keep their original Go type.
|
||||
Code reading numbers from hook results must accept every numeric type rather
|
||||
than asserting `float64`. The JSON encoding is the same either way.
|
||||
|
||||
## Request body
|
||||
|
||||
@@ -242,7 +390,9 @@ Every decoder receives one host-parsed body:
|
||||
{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.
|
||||
A JSON `value` enumerates its object members sorted by key, or in the client's order for a plugin that declares `json-order@1`.
|
||||
|
||||
`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. Every uploaded file has its own ref, including several files in one repeated field (`image[]`): the first file of a field is `request_file:<field>` and later files are `request_file:<field>#<index>` with a zero-based index. Plugins copy `files[].ref` instead of spelling refs by hand. 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
|
||||
|
||||
@@ -254,13 +404,15 @@ Every decoder receives one host-parsed body:
|
||||
{ __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`.
|
||||
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>`, or `request_file:<field>#<index>` for a later file of a repeated 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`.
|
||||
|
||||
The host treats every 2xx upstream status (`200`, `201 Created`, `202 Accepted`, ...) as a successful submission and calls `parseSubmitResponse` with `{statusCode, headers, body}`; any other status fails the submission with the upstream status and body before the hook runs.
|
||||
|
||||
## 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`.
|
||||
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). `data` is `null` for a task whose submit route declared `retainResult: false`. Renderers and presenters never receive `originTasks`.
|
||||
|
||||
## Native surface
|
||||
|
||||
@@ -290,11 +442,15 @@ The host registry owns these bindings:
|
||||
| `openai_video` | create | `POST /v1/videos` | JSON or multipart |
|
||||
| `openai_video` | retrieve | `GET /v1/videos/:task_id` | none |
|
||||
| `openai_video` | content | `GET | HEAD /v1/videos/:task_id/content` | none |
|
||||
| `openai_image` | generate | `POST /v1/images/generations` | JSON |
|
||||
| `openai_image` | edit | `POST /v1/images/edits` | JSON or multipart |
|
||||
|
||||
`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.
|
||||
|
||||
`protocols.openai_image` requires `decodeRequest` and `render` and defines no modes. Both operations are synchronous from the client's point of view. The host extracts and pins `ctx.model` (`ctx.operation` is `generate` or `edit`), the decoder returns the usual submit intent, and `buildSubmitRequest` runs against the vendor exactly as for native routes. A vendor answering synchronously returns a terminal `immediate` from `parseSubmitResponse`; a vendor task API returns a task id, and the host then polls the task inside the client request through the same query hooks and completion settlement the background poller uses (`buildQueryRequest`, `parseTaskResult`, `extractUsageOnComplete`), every few seconds until the task is terminal or the protocol timeout (`TASK_PLUGIN_PROTOCOL_TIMEOUT_SECONDS`, default 600 seconds) elapses. On timeout the client receives 504 `task_timeout`; the task remains durable and settles when the poller finishes it. A client that disconnects while waiting leaves the task to the poller as well. A terminal `FAILURE` is answered with 400 and the task's failure reason. On `SUCCESS` the host calls `render(ctx, task)` with the request context and the terminal `TaskView`; the hook must return an object whose `data` is an array of OpenAI image entries (`{url}` or `{b64_json}`, optionally `revised_prompt`). The host adds `created` when absent and owns `response_format`: for `b64_json` it downloads every `data[].url` and adds `b64_json` (the URL is kept; a failed download logs a warning and leaves the URL). `render` receives no `artifacts`; image URLs come from the vendor result. Billing follows the ordinary task contract: reserve on the requested count from `extractUsage`, settle to the actual count from `extractUsageOnSubmit` (immediate results, legacy pricing), `extractUsageOnComplete` (immediate results and polled results, task expressions); the consume log records the billed count as `other.image_count` when the facts carry `image_count`.
|
||||
|
||||
Responses protocol media uses host-injected `ctx.artifacts[key].url`. OpenAI Video renderers may return provider fields and URLs from `task.data`; the host preserves those extensions and metadata, but overwrites `id`, `object`, `model`, `status`, `progress`, `created_at`, and `completed_at` and removes legacy `task_id`. Renderers must return a JSON object. Returning `task.data` exposes the latest persisted upstream snapshot, subject to the host's task-ID replacement and inline-video redaction; it does not perform a fresh upstream query or forward HTTP headers/status codes.
|
||||
|
||||
## Persisted data and driver hooks
|
||||
@@ -303,6 +459,8 @@ The persisted field remains `task.data`; there is no `task.raw` alias. Driver ho
|
||||
|
||||
`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.
|
||||
|
||||
`ctx.upstream` is `{kind: "vendor"}` or `{kind: "new_api"}` on `DriverContext`, `TaskQueryContext`, `BatchQueryContext`, and the `buildContentRequest` context, for submission and for background polling alike. `new_api` means the executing channel is a type-60 New API channel, so the same plugin on the upstream gateway answers. In that case the driver prefixes the URLs it builds with its own native-route prefix (doubao `/doubao`, alibaba `/ali`, kling `/kling`, jimeng `/jimeng`), and the host has already set `authHeader` to `Bearer <channel key>` and `apiKey` to the channel key for every `meta.auth` type: the channel key of a New API channel is the gateway token, so the vendor auth scheme (`oauth2_jwt` token exchange, a vendor JWT built from `ak|sk`) is not run. With `vendor` nothing changes. The response the driver reads back is what its own native presenter renders on the gateway, where the host has replaced the private upstream id in `id` / `task_id` / `taskId` fields with the gateway's public task id; `parseSubmitResponse` must accept that shape, and the query URL must resolve on the plugin's own `query` route with that id. Kling and jimeng keep their `sk-` key-prefix heuristic for legacy channels pointed at a gateway; the explicit signal takes precedence when it says `new_api`. `ctx.upstream` is an additive apiVersion 1 extension; drivers must treat a missing field as `vendor`.
|
||||
|
||||
## 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`.
|
||||
@@ -314,7 +472,8 @@ Query and parse hooks use `TaskQueryContext`, not `DriverContext`. The host rebu
|
||||
| `action` | Normalized persisted action |
|
||||
| `model` | `Properties.OriginModelName` |
|
||||
| `upstreamModel` | `Properties.UpstreamModelName`, falling back to `model` |
|
||||
| `baseUrl` / `apiKey` / `authHeader` / `auth` | Channel credentials |
|
||||
| `baseUrl` / `apiKey` / `authHeader` / `auth` | Channel credentials; on a New API channel `authHeader` is `Bearer <channel key>` and `apiKey` is the key for every auth type |
|
||||
| `upstream` | `{kind: "vendor" \| "new_api"}` from the executing channel type |
|
||||
| `data` | Current `Task.Data` snapshot |
|
||||
| `state` | Plugin-owned `PrivateData.PluginState` |
|
||||
|
||||
@@ -360,7 +519,8 @@ per-call pricing keeps its existing meaning and submission-ratio hook.
|
||||
### Optional SSE response reader
|
||||
|
||||
`meta.submitResponseTypes` defaults to `["json"]`. A plugin that needs SSE
|
||||
includes `"sse"` and exports `parseSubmitEvent`. `buildSubmitRequest` selects
|
||||
includes `"sse"` and exports `parseSubmitEvent`, or `parseSubmitEventDelta` for
|
||||
host-managed accumulation (below). `buildSubmitRequest` selects
|
||||
`responseType: "sse"` on its request descriptor (default `"json"`); the selected
|
||||
type must be declared. This optional v1 manifest field requires a supporting
|
||||
host: older strict hosts reject it instead of silently misreading the stream.
|
||||
@@ -383,14 +543,23 @@ when accumulating, rather than mutating host-backed arrays in place.
|
||||
Idle runtimes survive garbage collection in a pool bounded by the plugin's
|
||||
configured concurrency. The host creates additional instances on demand and
|
||||
discards interrupted instances. Cancellation callbacks finish before an instance
|
||||
is returned to the pool. Hook bindings remain live; the host does not freeze
|
||||
exported functions or object members. No JavaScript execution slot is held while
|
||||
is returned to the pool, and the engine then drops its references to the
|
||||
finished call's arguments, so an idle instance does not keep the last request's
|
||||
data alive; values a plugin kept in module variables still keep theirs. Hook
|
||||
bindings remain live; the host does not freeze
|
||||
exported functions or object members. Whether an optional hook (the usage
|
||||
hooks, `listArtifacts`, `buildContentRequest`) is exported is decided once,
|
||||
when the plugin loads: an export that is `undefined` then counts as absent,
|
||||
and assigning it later does not make the host call it. No JavaScript execution slot is held while
|
||||
waiting for the next upstream event.
|
||||
|
||||
The host may copy plain JSON containers without re-encoding their string values;
|
||||
hook inputs and successive states remain separate snapshots. State size is still
|
||||
checked against its actual encoded JSON bytes after every event, including
|
||||
intermediate state that a later event might replace with a smaller value.
|
||||
The host passes the returned state to the next event as is (see
|
||||
[Hook arguments and results](#hook-arguments-and-results)); changes a hook makes
|
||||
to `previousState` without returning them are not kept. State that is not plain
|
||||
JSON, such as a typed array, is first normalized through its JSON encoding.
|
||||
State size is still checked against its actual encoded JSON bytes after every
|
||||
event, including intermediate state that a later event might replace with a
|
||||
smaller value.
|
||||
|
||||
A complete, blank-line-delimited event whose hook returns `done: true` terminates
|
||||
the reader and closes the upstream response. The last state is passed as
|
||||
@@ -415,10 +584,12 @@ does not add task creation before upstream completion or stream resumption.
|
||||
|
||||
### Optional host-managed SSE accumulation
|
||||
|
||||
Declaring `submit-sse-delta@1` requires `submitResponseTypes` to include `sse`
|
||||
and selects `parseSubmitEventDelta` instead of `parseSubmitEvent`. The descriptor
|
||||
still uses `responseType: "sse"`. Existing plugins without this capability keep
|
||||
the snapshot hook and its original contract.
|
||||
A plugin that includes `sse` and exports `parseSubmitEventDelta` without
|
||||
`parseSubmitEvent` uses `parseSubmitEventDelta`, and so does a plugin that
|
||||
declares `submit-sse-delta@1`. The descriptor still uses `responseType: "sse"`.
|
||||
A plugin that exports `parseSubmitEvent` and does not declare
|
||||
`submit-sse-delta@1` keeps the snapshot hook and its original contract, even if
|
||||
it also exports `parseSubmitEventDelta`.
|
||||
|
||||
```ts
|
||||
parseSubmitEventDelta(ctx: DriverContext,
|
||||
|
||||
Reference in new issue
Block a user