New APINew API
User GuideInstallationAPI ReferenceAI ApplicationsSkillsHelp & SupportBusiness Cooperation

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.

plugin.js
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

  1. The host authenticates and selects a channel, then passes the standardized request to buildSubmitRequest.
  2. The host validates the descriptor's URL, sends an HTTP request, and then calls parseSubmitResponse.
  3. The plugin returns the upstream task ID and persistent data; the host generates a public task ID and saves the task.
  4. The host periodically calls buildQueryRequest and parseTaskResult until the task reaches a final state or is cleaned up due to failure.
  5. 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.json

Save the following content as golden.json, covering model mapping, invalid input, and unknown states:

golden.json
{
  "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.routes and native decoders and renderers.
  • Host Protocol: Declare meta.protocols and implement protocol hooks for Video or Responses.
  • Artifacts: Implement listArtifacts and buildContentRequest in 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