feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
aa6c3f50f4
commit
ceddfd3d6b
828 files changed
+122253
-13702
No files matched your search
@@ -54,11 +54,15 @@ query is redacted before request logging.
|
||||
Deployment boundaries and concurrency environment variables are documented in
|
||||
[v1.md](./v1.md#generic-task-management-api).
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
Vendored
+35
-5
@@ -1,4 +1,20 @@
|
||||
export type JSONValue = null | boolean | number | string | readonly JSONValue[] | {readonly [key: string]: JSONValue};
|
||||
export type HostCapability = "json-clone@1" | "submit-sse-delta@1";
|
||||
export type MutableJSON<T> = T extends readonly (infer Item)[] ? MutableJSON<Item>[] : T extends object ? {-readonly [Key in keyof T]: MutableJSON<T[Key]>} : T;
|
||||
export interface HostUtils {
|
||||
hasCapability(name: string): boolean;
|
||||
/** Independent native JS containers; at most 1 MiB encoded JSON, depth 32 and 32,768 nodes. */
|
||||
json: {clone<T extends JSONValue>(value: T): MutableJSON<T>};
|
||||
unixNow(): number;
|
||||
uuid(): string;
|
||||
hmacSHA256(message: string, secret: string): string;
|
||||
jwtSignHS256(claims: Record<string, JSONValue>, secret: string): string;
|
||||
base64(value: string): string;
|
||||
base64URL(value: string): string;
|
||||
base64URLDecode(value: string): string;
|
||||
volcSignV4(request: {method: string; url: string; headers?: Record<string, string>; body?: string; accessKey: string; secretKey: string; region?: string; service?: string; timestamp?: number}): Record<string, string>;
|
||||
}
|
||||
declare global {const utils: HostUtils;}
|
||||
export type FileReference = Readonly<{ref: string; field: string; filename: string; mimeType: string; size: number}>;
|
||||
export type FilePlaceholder = Readonly<{__fileRef: string; encoding: "base64" | "dataUrl"; mimeType?: string; maxBytes?: number}>;
|
||||
export type DecodedBody =
|
||||
@@ -20,15 +36,20 @@ export type ProtocolClaim =
|
||||
| {name: "openai_responses"; supports: readonly ResponsesMode[]; models?: readonly string[]}
|
||||
| {name: "openai_video"; models?: readonly string[]};
|
||||
export type LocalizedText = string | ({ en: string } & Record<string, string>);
|
||||
export type UsageFieldSchema = {type: "number"; unit: "second" | "count" | "token" | "credit"; description?: LocalizedText} | {type: "boolean"; description?: LocalizedText} | {enum: readonly string[]; description?: LocalizedText; enumLabels?: Readonly<Record<string, LocalizedText>>};
|
||||
export type UsageExample = {label: string; facts: Readonly<Record<string, string | number>>};
|
||||
export interface Meta {sortPriority?: number; website?: string; apiVersion: 1; key: string; name: string; icon?: string; description?: LocalizedText; version: string; author: {name: string; url?: string}; baseUrl?: string; channelTypes?: readonly number[]; models: readonly string[]; fetchMode: "per_task" | "batch"; allowedHosts?: readonly string[]; routes?: readonly NativeRoute[]; protocols?: readonly ProtocolClaim[]; usageSchema?: Readonly<Record<string, UsageFieldSchema>>; usageExamples?: readonly UsageExample[]; auth?: "none" | "api_key" | "vertex_oauth" | {type: "none" | "api_key" | "oauth2_jwt"}}
|
||||
export type UsageFieldSchema =
|
||||
| {type: "number"; unit: "count"; unitLabel?: LocalizedText; description?: LocalizedText}
|
||||
| {type: "number"; unit: "second" | "token" | "credit"; unitLabel?: never; description?: LocalizedText}
|
||||
| {type: "boolean"; unitLabel?: never; description?: LocalizedText}
|
||||
| {enum: readonly string[]; unitLabel?: never; description?: LocalizedText; enumLabels?: Readonly<Record<string, LocalizedText>>};
|
||||
export type UsageExample = {label: string; facts: Readonly<Record<string, string | number | boolean>>};
|
||||
export type UsageProfile = {models: readonly string[]; schema: Readonly<Record<string, UsageFieldSchema>>; examples?: readonly UsageExample[]};
|
||||
export interface Meta {requiredCapabilities?: readonly HostCapability[]; submitResponseTypes?: readonly ("json" | "sse")[]; sortPriority?: number; website?: string; apiVersion: 1; key: string; name: string; icon?: string; description?: LocalizedText; version: string; author: {name: string; url?: string}; baseUrl?: string; channelTypes?: readonly number[]; models: readonly string[]; fetchMode: "per_task" | "batch"; allowedHosts?: readonly string[]; routes?: readonly NativeRoute[]; protocols?: readonly ProtocolClaim[]; usageSchema?: Readonly<Record<string, UsageFieldSchema>>; usageExamples?: readonly UsageExample[]; usageProfiles?: readonly UsageProfile[]; auth?: "none" | "api_key" | "vertex_oauth" | {type: "none" | "api_key" | "oauth2_jwt"}}
|
||||
export interface TaskView {task_id: string; status: string; progress?: string; fail_reason?: string; created_at?: number; updated_at?: number; data?: unknown; properties?: Record<string, unknown>}
|
||||
export interface DriverContext {requestBody: unknown; requestHeaders: Readonly<Record<string, string>>; action: string; model: string; upstreamModel: string; baseUrl: string; apiKey?: string; authHeader: string; files: readonly FileReference[]; publicTaskId: string; originTasks?: readonly {taskId: string; upstreamTaskId: string; action: string; status: string; data: unknown}[]}
|
||||
export interface TaskQueryContext {taskId: string; publicTaskId: string; action: string; model: string; upstreamModel: string; baseUrl: string; apiKey?: string; authHeader: string; auth?: unknown; data: unknown; state: unknown}
|
||||
export interface BatchQueryContext {baseUrl: string; apiKey?: string; authHeader: string; auth?: unknown; tasks: readonly TaskQueryContext[]}
|
||||
export type HookHTTPResponse = {readonly status: number; readonly headers: Readonly<Record<string, string>>}
|
||||
export interface RequestDescriptor {url: string; method?: string; headers?: Record<string, string>; /** JSON body may contain FilePlaceholder objects at any depth; the host replaces each with a Base64 or data-URL string. */ body?: unknown; credentialless?: boolean; action?: string; model?: string; rewriteModel?: string; bodyType?: "json" | "multipart"; parts?: readonly {name: string; value?: unknown; fileRef?: string; filename?: string}[]}
|
||||
export interface RequestDescriptor {responseType?: "json" | "sse"; url: string; method?: string; headers?: Record<string, string>; /** JSON body may contain FilePlaceholder objects at any depth; the host replaces each with a Base64 or data-URL string. */ body?: unknown; credentialless?: boolean; action?: string; model?: string; rewriteModel?: string; bodyType?: "json" | "multipart"; parts?: readonly {name: string; value?: unknown; fileRef?: string; filename?: string}[]}
|
||||
export interface UpstreamResponse {statusCode: number; headers: Readonly<Record<string, readonly string[]>>; body: unknown}
|
||||
export interface NormalizedTaskResult {taskId?: string; status: "NOT_START" | "SUBMITTED" | "QUEUED" | "IN_PROGRESS" | "SUCCESS" | "FAILURE" | "UNKNOWN"; progress?: string; reason?: string; url?: string; remoteUrl?: string; completionTokens?: number; totalTokens?: number}
|
||||
export interface TaskArtifact {key: string; type: "video" | "audio" | "image" | "file"; mimeType?: string}
|
||||
@@ -39,6 +60,15 @@ export declare const protocols: {
|
||||
openai_video?: {decodeRequest(ctx: ProtocolDecodeContext): SubmitIntent; render(ctx: unknown, task: TaskView): unknown};
|
||||
};
|
||||
export declare function buildSubmitRequest(ctx: DriverContext): RequestDescriptor;
|
||||
export interface SubmitEvent {event: string; id: string; data: string}
|
||||
export type JSONPath = readonly (string | number)[];
|
||||
export type JSONChange =
|
||||
| {op: "set" | "append"; path: JSONPath; value: JSONValue}
|
||||
| {op: "appendText"; path: JSONPath; value: string};
|
||||
export interface SubmitEventDeltaResult {changes: readonly JSONChange[]; state: JSONValue; done: boolean}
|
||||
/** With submit-sse-delta@1, state is small control data; changes build the separate response body. */
|
||||
export declare function parseSubmitEventDelta(ctx: DriverContext, event: SubmitEvent, previousState: JSONValue | null): SubmitEventDeltaResult;
|
||||
export declare function parseSubmitEvent(ctx: DriverContext, event: SubmitEvent, previousState: JSONValue | null): {state: JSONValue; done: boolean};
|
||||
export declare function parseSubmitResponse(ctx: DriverContext, response: UpstreamResponse): {taskId: string; taskData?: unknown; immediate?: NormalizedTaskResult; state?: unknown};
|
||||
export declare function buildQueryRequest(ctx: TaskQueryContext): RequestDescriptor;
|
||||
export declare function buildBatchQueryRequest(ctx: BatchQueryContext, tasks: readonly TaskQueryContext[]): RequestDescriptor;
|
||||
@@ -46,6 +76,6 @@ export declare function parseTaskResult(ctx: TaskQueryContext, body: unknown, re
|
||||
export declare function parseBatchResult(ctx: BatchQueryContext, body: unknown, response: HookHTTPResponse): readonly (NormalizedTaskResult & {taskId: string; data?: unknown; state?: unknown})[];
|
||||
export declare function extractUsage(ctx: DriverContext & {usagePurpose?: "facts" | "billing_ratios"}): Readonly<Record<string, string | number | boolean>> | null;
|
||||
export declare function extractUsageOnSubmit(ctx: DriverContext, taskData: unknown): Readonly<Record<string, string | number | boolean>> | null;
|
||||
export declare function extractUsageOnComplete(task: TaskView, result: NormalizedTaskResult, data: unknown): Readonly<Record<string, string | number | boolean>> | null;
|
||||
export declare function extractUsageOnComplete(task: TaskQueryContext, result: NormalizedTaskResult, data: unknown): Readonly<Record<string, string | number | boolean>> | null;
|
||||
export declare function listArtifacts(task: {taskId: string; status: string; action: string; data: unknown; producerVersion: string}): readonly TaskArtifact[];
|
||||
export declare function buildContentRequest(ctx: DriverContext & {artifactKey: string; data: unknown; state?: unknown; upstreamTaskId: string; clientRequest: {method: "GET" | "HEAD"; headers: Readonly<Record<string, string>>}}): RequestDescriptor;
|
||||
+247
-4
@@ -28,9 +28,9 @@ export const meta = {
|
||||
|
||||
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.
|
||||
`{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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -57,13 +57,97 @@ A plugin whose vendor has no LobeHub icon ships its logo as a sidecar file, `ico
|
||||
|
||||
`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.
|
||||
|
||||
Before channel distribution, the host calls every mode-compatible candidate's
|
||||
`decodeRequest`. Only candidates returning a valid submit intent for the pinned
|
||||
model remain eligible. If all reject the request, the host returns the candidate
|
||||
failures, grouping identical messages with their plugin keys. A single candidate
|
||||
retains its original failure message. After channel selection, the executing plugin decodes again to build
|
||||
its own normalized request. Decoders must therefore tolerate repeated calls.
|
||||
Channel-type ownership remains unique; sharing models does not relax this rule.
|
||||
|
||||
Shared models may use different usage schemas and prices for each plugin.
|
||||
Administrators can save a provider expression in
|
||||
`billing_setting.plugin_billing_expr`, keyed by `<pluginKey>::<model>`.
|
||||
Without an override, the provider inherits the model-level expression. If the
|
||||
effective expression references a literal `u()` key absent from that provider's
|
||||
selected schema, the shared model is unconfigured for that provider; submission
|
||||
returns 400 `model_price_error` until compatible pricing is saved. Pricing pages
|
||||
show the provider variants and their configuration status. Model-level usage
|
||||
labels continue to come from the first plugin in ascending key order. Saving a
|
||||
model expression checks every candidate without its own override in the same
|
||||
draft. Provider overrides are not included in model-price synchronization.
|
||||
|
||||
Saved overrides remain visible and effective when only one provider is left.
|
||||
An override whose plugin is unavailable or no longer declares the model is
|
||||
marked `stale` in the administration snapshot. Its unchanged value can be kept
|
||||
while saving other prices, or explicitly removed; adding or changing an override
|
||||
still requires a current plugin/model binding. Public variants distinguish
|
||||
`ratio` fallback pricing from `tiered_expr`, so a provider using a configured
|
||||
per-call price is not marked unconfigured merely because it has no expression.
|
||||
|
||||
Numeric `usageSchema` fields declare a host-owned unit of `second`, `count`, `token`, or `credit`. Boolean fields declare `{type: "boolean"}`.
|
||||
|
||||
Number fields with `unit: "count"` may additionally declare `unitLabel`, an
|
||||
optional LocalizedText display unit. This distinguishes counted objects without
|
||||
changing the canonical unit, usage facts, expression keys, prices, or settlement:
|
||||
|
||||
```js
|
||||
image_count: {
|
||||
type: "number",
|
||||
unit: "count",
|
||||
unitLabel: {en: "image", zh: "张", "zh-TW": "張"},
|
||||
description: {en: "Image generation unit price", zh: "图片生成单价"},
|
||||
}
|
||||
```
|
||||
|
||||
Labels are short unit names or counting words, such as `image` / `张` or
|
||||
`song` / `首`, without prices, quantities, slashes, or trailing punctuation.
|
||||
They follow the same locale normalization, required English fallback, and
|
||||
256-rune-per-locale limit as field descriptions. A bare string is normalized to
|
||||
an English map. The UI uses the current language, its primary language, then
|
||||
English; omitted labels retain the host's existing count-unit display.
|
||||
Labels are plugin data, not frontend translation keys. They apply equally to
|
||||
`usageSchema` and `usageProfiles[].schema`, including pricing, calculators,
|
||||
plugin details, and usage-log pricing. Other units, booleans, and enums cannot
|
||||
declare `unitLabel`; token prices retain their per-million display scale.
|
||||
|
||||
This is an additive extension of plugin API version 1. Updated hosts accept
|
||||
existing plugins unchanged; older hosts reject the unknown `unitLabel` property.
|
||||
Upgrade the host before installing a plugin that declares it.
|
||||
|
||||
`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.
|
||||
|
||||
### Usage profiles by model
|
||||
|
||||
`meta.usageProfiles` optionally contains `{models, schema, examples?}` entries.
|
||||
Each profile provides the complete schema and display examples for its models,
|
||||
replacing `usageSchema` and `usageExamples` without merging or inheriting them.
|
||||
Unmatched models use the plugin defaults. Models must use the declared spelling
|
||||
from `meta.models` and belong to at most one profile. Empty model lists and
|
||||
missing/null schemas are rejected; an empty schema object is valid. Token-unit
|
||||
profiles require their own examples. The same field, description, and example
|
||||
validation applies inside a profile as at the plugin level.
|
||||
|
||||
Define constants by use case (for example `IMAGE_USAGE_SCHEMA` and
|
||||
`VIDEO_USAGE_SCHEMA`) and reference them from the profiles. These are ordinary
|
||||
JavaScript constants: the host executes the module before reading `meta`.
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
supporting profiles before installing a plugin that declares them.
|
||||
|
||||
## 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.
|
||||
`meta.description` and each `usageSchema` field `description` and `unitLabel` 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:
|
||||
|
||||
@@ -116,6 +200,37 @@ Descriptions are short, user-facing phrases without trailing punctuation in any
|
||||
- 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.
|
||||
|
||||
## 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`.
|
||||
|
||||
Plugins using new APIs declare exact versioned capabilities:
|
||||
|
||||
```js
|
||||
requiredCapabilities: ["json-clone@1", "submit-sse-delta@1"]
|
||||
```
|
||||
|
||||
The host rejects unknown, duplicate, or unsupported capability versions at load.
|
||||
Omitting the field preserves existing behavior. `utils.hasCapability(name)`
|
||||
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
|
||||
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
|
||||
such as `__proto__` remain ordinary own data properties. The tool uses the
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Request body
|
||||
|
||||
Every decoder receives one host-parsed body:
|
||||
@@ -180,7 +295,7 @@ The host registry owns these bindings:
|
||||
|
||||
`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.
|
||||
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
|
||||
|
||||
@@ -224,3 +339,131 @@ The host classifies the HTTP status before trusting a non-terminal parse:
|
||||
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.
|
||||
|
||||
## Immediate completion and submission streams
|
||||
|
||||
A driver may return `immediate: {status: "SUCCESS" | "FAILURE", ...}` from
|
||||
`parseSubmitResponse`. For immediate success the host calls the existing
|
||||
`extractUsageOnComplete(ctx, result, taskData)` with a `TaskQueryContext`, using
|
||||
both model identities, the submitted task ID, parsed data and returned plugin
|
||||
state. The same model-specific usage validation as polling applies. Plugins
|
||||
return usage quantities, never host quota or a charge.
|
||||
|
||||
For task expressions the host overlays valid completion facts onto the frozen
|
||||
submission facts, evaluates the frozen expression and reserves any increase
|
||||
before persistence. The final quota, usage and tier are recorded before the
|
||||
single BillingSession settlement. Zero usage is retained. Invalid facts or an
|
||||
expression evaluation failure retain the reservation and produce a warning.
|
||||
Immediate failure settles to zero. Terminal tasks are not polled. Legacy
|
||||
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
|
||||
`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.
|
||||
|
||||
```ts
|
||||
parseSubmitEvent(ctx: DriverContext,
|
||||
event: {event: string; id: string; data: string},
|
||||
previousState: JSONValue | null
|
||||
): {state: JSONValue; done: boolean}
|
||||
```
|
||||
|
||||
The host accepts `text/event-stream`, joins multiple `data:` lines with newlines,
|
||||
handles CRLF, comment heartbeats, event names (default `message`) and event IDs.
|
||||
It does not parse provider JSON, interpret provider completion markers or sum
|
||||
usage. The plugin does those operations and returns explicit JSON state on every
|
||||
call; pooled runtimes do not guarantee module globals survive between events.
|
||||
Treat `previousState` as read-only JSON input. Construct owned arrays/objects
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
`response.body` to the existing `parseSubmitResponse`, retaining the upstream
|
||||
HTTP status and headers. The normal immediate-completion path then persists,
|
||||
settles and presents the result. SSE state is request-local; durable plugin
|
||||
state is still only the `state` returned by `parseSubmitResponse`.
|
||||
|
||||
EOF without `done`, invalid framing/state, read failures, hook failures and size
|
||||
limits fail submission. Events, accumulated JSON state and persisted task data
|
||||
are each limited to 1 MiB; data is never truncated. The existing streaming idle
|
||||
timeout applies (30 seconds if unset), as do the submission context deadline
|
||||
and per-hook execution timeout. Once a successful SSE HTTP response is accepted,
|
||||
failures are non-retryable, including 5xx-shaped local errors: starting the
|
||||
provider operation again could duplicate billable work.
|
||||
|
||||
No upstream events are forwarded to clients. Native presenters still return
|
||||
JSON, and Responses uses its existing presentation flow after the durable
|
||||
submission barrier. Native cancellation closes the upstream reader; Responses
|
||||
retains its existing independently bounded submission context. This interface
|
||||
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.
|
||||
|
||||
```ts
|
||||
parseSubmitEventDelta(ctx: DriverContext,
|
||||
event: {event: string; id: string; data: string},
|
||||
previousState: JSONValue | null
|
||||
): {changes: JSONChange[]; state: JSONValue; done: boolean}
|
||||
```
|
||||
|
||||
`state` contains control data needed to interpret the next event, limited to
|
||||
64 KiB encoded JSON. Only this snapshot is passed back to JavaScript. The host
|
||||
maintains a separate result, initially null, and applies `changes` in order:
|
||||
|
||||
```js
|
||||
{op: "set", path: [], value: {text: "", images: []}}
|
||||
{op: "appendText", path: ["text"], value: "A new text fragment"}
|
||||
{op: "append", path: ["images"], value: {url: "https://cdn.example/image.png"}}
|
||||
```
|
||||
|
||||
- `set` replaces the root at an empty path, replaces an existing array element,
|
||||
or adds/replaces an object member. Parent containers must already exist.
|
||||
- `append` appends one JSON value to an existing array.
|
||||
- `appendText` appends a string to an existing string. The host retains text in
|
||||
Go and does not send accumulated text back to the plugin.
|
||||
- Paths are arrays of string object keys or non-negative integer array indices.
|
||||
Indices must address existing elements; sparse arrays and implicit parent
|
||||
creation are unsupported. Keys are data, never property-access expressions.
|
||||
|
||||
Each result contains exactly `changes`, `state`, and boolean `done`. Each change
|
||||
contains exactly `op`, `path`, and `value`. There are at most 256 changes per
|
||||
event, 32 path segments, 32 levels below the result root, 32,768 result nodes,
|
||||
and 1 MiB of encoded result JSON. Size accounting includes escaped keys and
|
||||
strings, punctuation and replacements, and is checked after every operation.
|
||||
An oversized intermediate operation fails even if a later operation would
|
||||
shrink the result. Invalid operations invalidate the submission; partial
|
||||
results are never published.
|
||||
|
||||
The host counts the encoded sizes of changed values rather than encoding the
|
||||
complete accumulated result on every event. At `done: true`, it materializes the
|
||||
ordinary JSON result and verifies its actual encoding against the recorded
|
||||
size before passing it as `response.body` to `parseSubmitResponse`. The result
|
||||
then follows the same persistence, usage validation and settlement path as the
|
||||
snapshot mode. State and builders belong to one request and are released on
|
||||
completion, cancellation or error; no JS slot is held during upstream I/O.
|
||||
|
||||
The plugin still interprets vendor payloads, decides which fields to update,
|
||||
distinguishes cumulative usage from increments, and decides completion. Alibaba
|
||||
uses this mode for interleaved image generation, keeping only content indices,
|
||||
text adjacency and finish reasons in its control state.
|
||||
@@ -18,15 +18,47 @@
|
||||
{"type": "object", "additionalProperties": false, "required": ["name", "supports"], "properties": {"name": {"const": "openai_responses"}, "supports": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"enum": ["stream", "sync", "background"]}}, "models": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}},
|
||||
{"type": "object", "additionalProperties": false, "required": ["name"], "properties": {"name": {"const": "openai_video"}, "models": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}
|
||||
]}},
|
||||
"routes": {"type": "array", "items": {"$ref": "#/$defs/route"}}, "usageSchema": {"type": "object", "additionalProperties": {"$ref": "#/$defs/usageField"}}, "usageExamples": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["label", "facts"], "properties": {"label": {"type": "string"}, "facts": {"type": "object"}}}}, "auth": {}
|
||||
"submitResponseTypes": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"enum": ["json", "sse"]}},
|
||||
"requiredCapabilities": {"type": "array", "uniqueItems": true, "items": {"enum": ["json-clone@1", "submit-sse-delta@1"]}},
|
||||
"routes": {"type": "array", "items": {"$ref": "#/$defs/route"}}, "usageSchema": {"type": "object", "additionalProperties": {"$ref": "#/$defs/usageField"}}, "usageExamples": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["label", "facts"], "properties": {"label": {"type": "string"}, "facts": {"type": "object"}}}}, "usageProfiles": {"type": "array", "items": {"$ref": "#/$defs/usageProfile"}}, "auth": {}
|
||||
},
|
||||
"allOf": [{"if": {"required": ["requiredCapabilities"], "properties": {"requiredCapabilities": {"contains": {"const": "submit-sse-delta@1"}}}}, "then": {"required": ["submitResponseTypes"], "properties": {"submitResponseTypes": {"contains": {"const": "sse"}}}}}],
|
||||
"$defs": {
|
||||
"jsonChange": {
|
||||
"type": "object", "additionalProperties": false, "required": ["op", "path", "value"],
|
||||
"properties": {
|
||||
"op": {"enum": ["set", "append", "appendText"]},
|
||||
"path": {"type": "array", "maxItems": 32, "items": {"oneOf": [{"type": "string"}, {"type": "integer", "minimum": 0}]}},
|
||||
"value": true
|
||||
},
|
||||
"allOf": [{"if": {"properties": {"op": {"const": "appendText"}}}, "then": {"properties": {"value": {"type": "string"}}}}]
|
||||
},
|
||||
"submitEventDeltaResult": {
|
||||
"type": "object", "additionalProperties": false, "required": ["changes", "state", "done"],
|
||||
"properties": {
|
||||
"changes": {"type": "array", "maxItems": 256, "items": {"$ref": "#/$defs/jsonChange"}},
|
||||
"state": true,
|
||||
"done": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"usageProfile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["models", "schema"],
|
||||
"properties": {
|
||||
"models": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
|
||||
"schema": {"type": "object", "additionalProperties": {"$ref": "#/$defs/usageField"}},
|
||||
"examples": {"type": "array", "maxItems": 16, "items": {"$ref": "#/properties/usageExamples/items"}}
|
||||
},
|
||||
"description": "Complete usage metadata for declared models. The host validates model membership, unique profile ownership and examples, including required token-unit examples."
|
||||
},
|
||||
"usageField": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {"enum": ["number", "boolean"]},
|
||||
"unit": {"enum": ["second", "count", "token", "credit"]},
|
||||
"unitLabel": {"$ref": "#/$defs/usageLabel", "description": "Optional localized display unit for number fields with count unit. Does not change usage quantities or billing."},
|
||||
"enum": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string"}},
|
||||
"description": {"$ref": "#/$defs/usageLabel"},
|
||||
"enumLabels": {
|
||||
@@ -35,6 +67,7 @@
|
||||
"description": "Optional display names keyed by declared enum values; the host validates membership. Missing entries display the original value."
|
||||
}
|
||||
},
|
||||
"allOf": [{"if": {"required": ["unitLabel"]}, "then": {"required": ["type", "unit"], "properties": {"type": {"const": "number"}, "unit": {"const": "count"}}}}],
|
||||
"oneOf": [
|
||||
{"required": ["enum"], "not": {"anyOf": [{"required": ["type"]}, {"required": ["unit"]}]}},
|
||||
{"required": ["type", "unit"], "properties": {"type": {"const": "number"}}, "not": {"anyOf": [{"required": ["enum"]}, {"required": ["enumLabels"]}]}},
|
||||
|
||||
Reference in new issue
Block a user