Development Guide
Write a complete minimal task plugin, verify request construction, response submission, and polling status, and debug with fixtures and sandbox.
Before You Start Developing
Plugins are single-file, synchronous ECMAScript modules. You cannot use import, require, async, await, fetch, file system, or environment variables. Network requests, authentication parsing, task saving, and billing are handled by the host.
The current Plugin API v1 is still evolving. The type declarations and JSON Schema serve as the contract basis and are validated against the target host version.
Minimal Complete Example
Save the following code as plugin.js. It demonstrates a task plugin named demo-task: submitting to /jobs, reading the returned id, and then querying the status via /jobs/{id}.
Example Upstream
api.example.com
and the request/response formats here are for demonstration purposes and are not directly callable real services. When integrating with actual vendors, you will need to replace the upstream address, authentication, and protocol handling, and configure channels and prices.
export const meta = {
apiVersion: 1,
key: 'demo-task',
name: 'Demo Task',
version: '1.0.0',
author: { name: 'Example Author' },
description: {
en: 'A minimal asynchronous task adapter',
zh: '最小异步任务适配示例',
},
models: ['demo-model'],
fetchMode: 'per_task',
baseUrl: 'https://api.example.com',
auth: 'api_key',
usageSchema: {
requests: {
type: 'number',
unit: 'count',
description: { en: 'Number of generation requests', zh: '生成请求数量' },
},
},
usageExamples: [{ label: 'One request', facts: { requests: 1 } }],
};
export function buildSubmitRequest(ctx) {
const input = ctx.requestBody || {};
if (typeof input.prompt !== 'string' || !input.prompt.trim()) {
throw new Error('prompt must be a non-empty string');
}
return {
url: ctx.baseUrl.replace(/\/$/, '') + '/jobs',
method: 'POST',
headers: {
Authorization: ctx.authHeader,
'Content-Type': 'application/json',
},
body: {
model: ctx.upstreamModel || ctx.model,
prompt: input.prompt,
},
};
}
export function parseSubmitResponse(ctx, response) {
if (response.statusCode < 200 || response.statusCode >= 300) {
throw new Error('The upstream service rejected the task');
}
const body = response.body;
if (!body || typeof body.id !== 'string' || !body.id) {
throw new Error('The upstream response has no task id');
}
return { taskId: body.id, taskData: body };
}
export function buildQueryRequest(ctx) {
return {
url:
ctx.baseUrl.replace(/\/$/, '') +
'/jobs/' +
encodeURIComponent(ctx.taskId),
method: 'GET',
headers: { Authorization: ctx.authHeader },
};
}
export function parseTaskResult(ctx, body, response) {
const statuses = {
queued: 'QUEUED',
running: 'IN_PROGRESS',
succeeded: 'SUCCESS',
failed: 'FAILURE',
};
const status =
body && Object.hasOwn(statuses, body.status)
? statuses[body.status]
: 'UNKNOWN';
return { status };
}
export function extractUsage(ctx) {
return { requests: 1 };
}This example submits via the generic interface POST /v1/tasks/demo-task, with a JSON body containing model: "demo-model" and prompt. It does not declare native routes, Video/Responses protocols, or artifact hooks, which need to be implemented as needed in actual plugins.
Lifecycle and Data
- The host authenticates and selects a channel, then passes the standardized request to
buildSubmitRequest. - The host validates the descriptor's URL, sends an HTTP request, and then calls
parseSubmitResponse. - The plugin returns the upstream task ID and persistent data; the host generates a public task ID and saves the task.
- The host periodically calls
buildQueryRequestandparseTaskResultuntil the task reaches a final state or is cleaned up due to failure. - The host reads usage facts and completes billing according to the saved billing configuration.
TaskQueryContext.taskId is the upstream ID, and publicTaskId is the New API public ID; the polling context does not have requestBody. Data that needs to be preserved across polls should use state and should not rely on module global variables.
Task.Data saves the most recent upstream snapshot; it is updated with each successful parsing round. Unknown statuses should return UNKNOWN, not default to IN_PROGRESS, otherwise abnormal tasks might continuously occupy resources.
Compilation and Fixture Testing
Validate the source code using the New API executable that includes the plugin CLI:
new-api plugin lint plugin.js
new-api plugin test plugin.js --fixture golden.jsonSave the following content as golden.json, covering model mapping, invalid input, and unknown states:
{
"cases": [
{
"name": "mapped model is sent upstream",
"hook": "buildSubmitRequest",
"args": [
{
"model": "public-alias",
"upstreamModel": "demo-model",
"baseUrl": "https://api.example.com",
"authHeader": "Bearer example-key",
"requestBody": { "prompt": "A quiet garden" }
}
],
"expected": {
"url": "https://api.example.com/jobs",
"method": "POST",
"headers": {
"Authorization": "Bearer example-key",
"Content-Type": "application/json"
},
"body": { "model": "demo-model", "prompt": "A quiet garden" }
}
},
{
"name": "reject an empty prompt",
"hook": "buildSubmitRequest",
"args": [{ "requestBody": { "prompt": " " } }],
"expectedError": "prompt must be a non-empty string"
},
{
"name": "preserve the upstream task id",
"hook": "parseSubmitResponse",
"args": [{}, { "statusCode": 200, "body": { "id": "vendor-123" } }],
"expected": { "taskId": "vendor-123", "taskData": { "id": "vendor-123" } }
},
{
"name": "unknown status is not in progress",
"hook": "parseTaskResult",
"args": [
{},
{ "status": "unexpected" },
{ "status": 200, "headers": {} }
],
"expected": { "status": "UNKNOWN" }
}
]
}Fixtures only execute deterministic synchronous hooks and do not send requests upstream. Formal plugins should also cover submission errors, successful and failed final states, polling context, batch behavior, zero usage values, protocol rendering, and artifact reading.
Debugging in the Admin Page
Root can select a hook and input a JSON array of parameters in the "Sandbox" of the installed plugin details. For example, when debugging buildSubmitRequest, input:
[
{
"model": "demo-model",
"baseUrl": "https://api.example.com",
"authHeader": "Bearer example-key",
"requestBody": { "prompt": "A quiet garden" }
}
]Sandbox calls only run the selected synchronous function and do not execute the HTTP request descriptor it returns. Therefore, a successful output only indicates that the hook's behavior matches the input, but does not prove that the upstream request will definitely succeed.
Extended Capabilities
- Batch Query: Declare
fetchMode: "batch"and implement batch construction and parsing hooks; each result must include the corresponding task ID. - Native Interface: Implement vendor-specific entry points via
meta.routesandnativedecoders and renderers. - Host Protocol: Declare
meta.protocolsand implement protocol hooks for Video or Responses. - Artifacts: Implement
listArtifactsandbuildContentRequestin pairs. - Submit-and-Complete or Upstream SSE: Implement immediate final states, SSE snapshots, or incremental event hooks according to capability declarations.
For detailed constraints, see API v1 Reference. When preparing for release, add versions, logs, and generate an index according to the Publishing Specification.
How is this guide?
Last updated on